"use client"; import {useEffect,useRef,useState} from 'react'; import type {CadSource} from '../../cad-features/index.mjs'; import type {SourceRecoveryRequest,SourceTarget} from '../../independent-slot-source/index.mjs'; import type {PrepareSlotReviewResult} from '../../slot-review/index.mjs'; import type {ClearancePlanRequest,ClearancePlanResult} from '../../slot-review/clearance-plan.mjs'; import type {ReviewedSlotGoal} from '../../independent-slot-check/transaction.mjs'; import type {SlotEvidenceWorkerOutcome} from '../../independent-slot-check/evidence-worker-client.mjs'; import {hashReviewedGoal,runSlotWork,testSlotBytes} from './slot-session'; import './slot-panel.css'; type Prepared=Extract; type Plan=Extract; type Point=[number,number,number]; type Field=[string,string,string?]; const setupFields:Field[]=[['origin','Origin in source XYZ (mm)','0, 0, 0'],['x','Setup X axis','1, 0, 0'],['y','Setup Y axis','0, 1, 0'],['z','Setup Z axis','0, 0, 1'],['initial','Initial tool-tip XYZ (mm)'],['stockMin','Stock minimum XYZ (mm)'],['stockMax','Stock maximum XYZ (mm)']]; const toolFields:Field[]=[['radius','Cutter radius (mm)'],['flute','Flute length (mm)'],['shankRadius','Shank radius (mm)'],['shankLength','Shank length (mm)'],['uncertainty','Declared uncertainty (mm)'],['clearance','Fixture clearance (mm)']]; const permissionFields:Field[]=[['remainingRadial','Permitted radial stock remaining (mm)'],['overcut','Permitted wall overcut (mm)'],['remainingAxial','Permitted axial stock remaining (mm)'],['entryExtension','Entry envelope extension (mm)'],['exitExtension','Exit envelope extension (mm)'],['breakthrough','Required breakthrough (mm)'],['feedMin','Reviewed minimum feed (mm/min)'],['feedMax','Reviewed maximum feed (mm/min)'],['plungeMax','Reviewed maximum plunge (mm/min)'],['rpmMin','Reviewed minimum spindle (rpm)'],['rpmMax','Reviewed maximum spindle (rpm)']]; const processFields:Field[]=[['rpm','Spindle speed (rpm)'],['feed','Cut feed (mm/min)'],['plunge','Plunge feed (mm/min)'],['stepdown','Maximum stepdown (mm)'],['stepover','Maximum stepover (mm)'],['roughStock','Rough wall stock (mm)'],['finalStock','Final wall stock (mm)'],['safeZ','Safe tool-tip Z (mm)'],['approach','Approach clearance (mm)']]; const labels=new Map([...setupFields,...toolFields,...permissionFields,...processFields].map(([key,label])=>[key,label])); const initialFields=()=>({x:'1, 0, 0',y:'0, 1, 0',z:'0, 0, 1',stockState:'raw',direction:'ccw',holders:'',fixtures:'[]'} as Record); const fmt=(value:number)=>Number(value.toFixed(6)).toString(); function numberField(fields:Record,key:string){const text=fields[key]?.trim();if(!text||!/^[-+]?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?$/i.test(text)||!Number.isFinite(Number(text)))throw new Error(`Enter ${labels.get(key)??key}.`);return Number(text);} function point(fields:Record,key:string):Point{const values=fields[key]?.split(',').map(s=>s.trim());if(!values||values.length!==3||values.some(v=>!v||!Number.isFinite(Number(v))))throw new Error(`Enter three comma-separated values for ${labels.get(key)}.`);return [Number(values[0]),Number(values[1]),Number(values[2])];} function jsonField(fields:Record,key:string):T{try{if((fields[key]?.length??0)>32768)throw 0;return JSON.parse(fields[key]) as T;}catch{throw new Error(`Enter valid ${key} JSON using the shown shape.`);}} function download(name:string,text:string,type='application/json'){const url=URL.createObjectURL(new Blob([text],{type})),a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);} export default function SlotPanel({file,source,options,sourceSignal,onHighlight}:{file:File;source:CadSource;options:SourceRecoveryRequest['options'];sourceSignal:AbortSignal;onHighlight:(target:SourceTarget)=>void}){ const [prepared,setPrepared]=useState(null),[selected,setSelected]=useState>({}),[fields,setFields]=useState(initialFields); const [review,setReview]=useState<{goal:ReviewedSlotGoal;sha256:string}|null>(null),[plan,setPlan]=useState(null),[nc,setNc]=useState(''),[outcome,setOutcome]=useState(null); const [busy,setBusy]=useState(''),[error,setError]=useState(''),[confirmed,setConfirmed]=useState(false); const [feedbackStep,setFeedbackStep]=useState(1); const epoch=useRef(0),controller=useRef(null); useEffect(()=>{const abort=()=>{epoch.current++;controller.current?.abort();};sourceSignal.addEventListener('abort',abort,{once:true});if(sourceSignal.aborted)abort();return()=>{sourceSignal.removeEventListener('abort',abort);abort();};},[sourceSignal]); function invalidate(level:'review'|'plan'|'test'='review'){epoch.current++;controller.current?.abort();controller.current=null;setBusy('');setError('');setOutcome(null);if(level!=='test'){setPlan(null);setNc('');}if(level==='review'){setReview(null);setConfirmed(false);}} function edit(key:string,value:string,generation=false){invalidate(generation?'plan':'review');setFields(current=>({...current,[key]:value}));} function begin(label:string,step:number){epoch.current++;controller.current?.abort();const current=epoch.current,c=new AbortController();controller.current=c;setFeedbackStep(step);setBusy(label);setError('');setOutcome(null);return {current,c};} function finish(current:number){if(current===epoch.current){controller.current=null;setBusy('');}} async function prepare(){invalidate();setPrepared(null);setSelected({});const {current,c}=begin('Recovering exact slot targets from the original STEP…',1);try{const result=await runSlotWork(file,{kind:'prepare',request:{schema_version:'step-slot-source-request-0.1',source,options}},c.signal);if(current!==epoch.current)return;if(result.kind!=='prepare')throw new Error('Unexpected preparation response.');if(result.result.status!=='prepared')throw new Error(`Slot preparation: ${result.result.diagnostic}. This strategy needs closed paired openings in one exact mm solid.`);setPrepared(result.result);}catch(e){if(current===epoch.current)setError(e instanceof Error?e.message:'Slot preparation failed.');}finally{finish(current);}} function align(target:SourceTarget){const entrance=target.entrances.find(e=>e.face_id===selected[target.id]);if(!entrance){setFeedbackStep(1);setError('Select the entrance before aligning coordinates.');return;}const delta=entrance.cap_centers_mm[1].map((v,i)=>v-entrance.cap_centers_mm[0][i]),length=Math.hypot(...delta),x=delta.map(v=>v/length),z=entrance.outward_normal,y=[z[1]*x[2]-z[2]*x[1],z[2]*x[0]-z[0]*x[2],z[0]*x[1]-z[1]*x[0]];invalidate();setFields(current=>({...current,origin:entrance.cap_centers_mm[0].join(', '),x:x.join(', '),y:y.join(', '),z:z.join(', ')}));} async function reviewGoal(){ if(!prepared||!confirmed)return;const {current,c}=begin('Binding reviewed declarations to this source…',2); try{ const n=(key:string)=>numberField(fields,key),targets=prepared.source_report.targets.filter(target=>Object.hasOwn(selected,target.id)); if(!targets.length||targets.length>16)throw new Error('Select between 1 and 16 supported slot targets.'); const goal:ReviewedSlotGoal={schema_version:'reviewed-slot-goal-0.1',reviewed:true,source_sha256:source.sha256,source_byte_length:source.byte_length,source_namespace_sha256:prepared.source_report.namespace_sha256, setup:{origin_source_mm:point(fields,'origin'),x_axis:point(fields,'x'),y_axis:point(fields,'y'),z_axis:point(fields,'z')},initial_tip_mm:point(fields,'initial'),stock:{min_mm:point(fields,'stockMin'),max_mm:point(fields,'stockMax'),state:fields.stockState==='intermediate'?'intermediate':'raw'}, tool:{kind:'flat_end_mill',radius_mm:n('radius'),flute_length_mm:n('flute'),shank_radius_mm:n('shankRadius'),shank_length_mm:n('shankLength'),holder_segments:jsonField(fields,'holders'),center_cutting:true},fixtures:{reviewed:true,boxes:jsonField(fields,'fixtures')},uncertainty_mm:n('uncertainty'),clearance_mm:n('clearance'), process:{feed_min_mm_min:n('feedMin'),feed_max_mm_min:n('feedMax'),plunge_max_mm_min:n('plungeMax'),spindle_min_rpm:n('rpmMin'),spindle_max_rpm:n('rpmMax')}, targets:targets.map(target=>{const entry=target.entrances.find(e=>e.face_id===selected[target.id]),mapping=prepared.correspondences.find(m=>m.source_target_id===target.id);if(!entry||mapping?.status!=='unique'||!mapping.entries.some(e=>e.entry_face_id===entry.face_id))throw new Error(`Select a supported entrance for ${target.id}.`);return{id:'goal-'+target.id,face_ids:[...target.face_ids],entry_face_id:entry.face_id,permissions:{remaining_radial_mm:n('remainingRadial'),wall_overcut_mm:n('overcut'),remaining_axial_mm:n('remainingAxial'),entry_extension_mm:n('entryExtension'),exit_extension_mm:n('exitExtension'),breakthrough_mm:n('breakthrough')}};})}; const sha256=await hashReviewedGoal(goal,c.signal);if(current!==epoch.current)return;setReview({goal,sha256});setPlan(null);setNc(''); }catch(e){if(current===epoch.current)setError(e instanceof Error?e.message:'Goal review failed.');}finally{finish(current);} } async function generate(){if(!review)return;const {current,c}=begin('Generating a stock-retaining slot candidate…',3);setPlan(null);setNc('');try{const n=(key:string)=>numberField(fields,key),request:ClearancePlanRequest={schema_version:'reviewed-slot-clearance-request-0.1',source,options,goal:review.goal,expected_goal_sha256:review.sha256,process:{spindle_rpm:n('rpm'),feed_mm_min:n('feed'),plunge_mm_min:n('plunge'),max_stepdown_mm:n('stepdown'),max_stepover_mm:n('stepover'),radial_allowance_mm:n('roughStock'),cut_direction:fields.direction==='cw'?'cw':'ccw',safe_z_mm:n('safeZ'),approach_clearance_mm:n('approach')},strategy:{final_wall_stock_mm:n('finalStock')}}; const result=await runSlotWork(file,{kind:'plan',request},c.signal);if(current!==epoch.current)return;if(result.kind!=='plan')throw new Error('Unexpected generation response.');if(result.result.status!=='candidate')throw new Error(`Generation stopped at ${result.result.stage}: ${result.result.diagnostic}. Check setup, tool reach, stock and the independently reviewed limits; no limits were changed.`);setPlan(result.result);setNc(result.result.nc_text); }catch(e){if(current===epoch.current)setError(e instanceof Error?e.message:'Slot generation failed.');}finally{finish(current);}} async function test(){if(!plan)return;const {current,c}=begin('Checking final bytes: exit depth, material coverage and protected geometry…',4);try{const result=await testSlotBytes(file,plan.check_request,nc,c.signal);if(current!==epoch.current)return;setOutcome(result);}catch(e){if(current===epoch.current)setError(e instanceof Error?e.message:'Independent slot test failed.');}finally{finish(current);}} const inputs=(rows:Field[],generation=false)=>
{rows.map(([key,label,placeholder])=>)}
; const bundle=outcome?.status==='completed'?outcome.bundle:null,full=bundle?.slot_check,exit=bundle?.exit_depth; const feedback=(step:number)=>feedbackStep===step&&<>{busy&&
{busy}60-second limit; an incomplete check is never accepted as a pass.
}{error&&

{error}

}; return

Reviewed slot machining

Exact paired openings · one declared setup · compact virtual G/M code

Selected operations only

Original STEP stays in this browser. Declare the stock and tool assembly; review the intended removal before generating. This does not machine the whole part or qualify a controller.

{feedback(1)} {prepared&&<>

1. Select the intended removal

{prepared.source_report.targets.length} recovered target(s); {prepared.source_report.inventory.unresolved_face_ids.length} other face(s) remain outside this strategy. Highlighting geometry does not select an operation.

{prepared.source_report.targets.map(target=>{const mapping=prepared.correspondences.find(m=>m.source_target_id===target.id),chosen=Object.hasOwn(selected,target.id);return

Local thickness {fmt(target.plane_separation_mm)} mm

{chosen&&<>}{mapping?.status!=='unique'&&

Complete source correspondence unavailable. Not selectable.

}
;})}

2. Declare and review the setup

Coordinates below use the declared local frame. A source-derived alignment is only a coordinate suggestion. Stock is not inferred from finished-part bounds.

Coordinate frame and positioned stock{inputs(setupFields)}
Complete tool and fixtures{inputs(toolFields)}