"use client"; /* eslint-disable @next/next/no-html-link-for-pages -- Full navigation avoids the independently reproduced vinext Link runtime failure. */ import { useEffect, useMemo, useRef, useState } from "react"; import type { Analysis } from "./types"; import DepthPanel, {type DepthSettings} from "./depth-panel"; import {clearCadWorkspace,loadCadWorkspace,saveCadWorkspace,type CadWorkspaceSnapshot} from './workspace-store'; import {loadDepthJob,loadDepthTestSummary,type DepthTestSummary} from './depth-store'; import ManufacturingPacketIntake from "./manufacturing-packet"; import SiteHeader from "../SiteHeader"; import GuidedLesson,{showLessonTarget} from '../GuidedLesson'; import type { DocumentEvidence } from "../../document-evidence/index.mjs"; import { compareDocumentEvidence } from "../../document-evidence/index.mjs"; import CadViewer, { type ViewMesh } from "./viewer"; import PdfViewer from "./pdf-viewer"; import { readPdf, sha256 } from "./pdf"; import "./workbench.css"; import modelWorkerUrl from "./analysis.worker.ts?worker&url"; import {PUBLIC_CASE_ASSETS,loadPublicCaseAssets} from "../../public-cases/index.mjs"; import ExactOpenings, {exactOpeningSelection} from './exact-openings'; import SlotPanel from './slot-panel'; type Drawing={file:File;url:string;evidence:DocumentEvidence|null;sha256:string;modelHash:string|null}; type Selection={id:string;label:string;meshIds:string[];data:unknown}; type StepEncodingChoice=""|"windows-1254"|"iso-8859-1"; const fmt=(n:number)=>n.toLocaleString("en-US",{maximumFractionDigits:4}); function FeatureEvidence({selection}:{selection:Selection}){ const raw=selection.data&&typeof selection.data==='object'?selection.data as Record:{}; const frame=raw.frame&&typeof raw.frame==='object'?raw.frame as Record:null; const origin=frame?.origin_mm&&typeof frame.origin_mm==='object'?frame.origin_mm as Record:null; const position=Array.isArray(raw.entry_position_mm)?raw.entry_position_mm:Array.isArray(raw.axis_origin_mm)?raw.axis_origin_mm:origin&&['x','y','z'].every(key=>typeof origin[key]==='number')?[origin.x,origin.y,origin.z]:null; const evidence=Array.isArray(raw.evidence)?raw.evidence.filter((item):item is string=>typeof item==='string'):[]; return

{selection.label}

Selected geometry is highlighted in the model. Candidate recognition is not machining approval.

Feature ID
{selection.id}
Geometry basis
{raw.kind==='local_obround_opening'?'Analytical STEP B-rep':typeof raw.basis==='string'?raw.basis:'Source inspection'}
Source surfaces
{selection.meshIds.length} highlighted
{position&&<>
Candidate location
{position.map(value=>typeof value==='number'?fmt(value):String(value)).join(', ')} mm
}{typeof raw.max_fit_error_mm==='number'&&<>
Maximum fit error
{fmt(raw.max_fit_error_mm)} mm
}
{evidence.length>0&&<>

Recognition evidence

    {evidence.map((item,i)=>
  • {item}
  • )}
}
Raw source facts · JSON
{JSON.stringify(selection.data,null,2)}
; } function ReferenceImage({url,name}:{url:string;name:string}){ // Local Blob URLs must not be sent to a server-side image optimizer. // eslint-disable-next-line @next/next/no-img-element return {name}/; } function download(name:string,data:unknown){const url=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:"application/json"})),a=document.createElement("a");a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);} export default function CadWorkbench(){ const [guideVisible,setGuideVisible]=useState(false),guideSurface=useRef(null); const [analysis,setAnalysis]=useState(null),[error,setError]=useState(""),[busy,setBusy]=useState(""),[selection,setSelection]=useState(null),[view,setView]=useState("iso:0"),[drawings,setDrawings]=useState([]),[drawingIndex,setDrawingIndex]=useState(-1),[documentBusy,setDocumentBusy]=useState(false); const [stepEncoding,setStepEncoding]=useState(""),[encodingNotice,setEncodingNotice]=useState(""); const task=useRef(null),timer=useRef|null>(null),objectUrls=useRef([]),loadSerial=useRef(0),modelFile=useRef(null); const [replacementKernel,setReplacementKernel]=useState<{filename:string;bytes:ArrayBuffer}|null>(null),[kernelBusy,setKernelBusy]=useState(false); const kernelFile=useRef(null),documentController=useRef(null),kernelLoadSerial=useRef(0); const [caseBusy,setCaseBusy]=useState(false),caseController=useRef(null),initialCase=useRef(false); const documentSerial=useRef(0),stepSourceSelected=useRef(false); const activeModel=useRef(null),restoreSerial=useRef(0),autoRestore=useRef(false); const slotSource=useRef(new AbortController()),[slotEpoch,setSlotEpoch]=useState(0); const [snapshot,setSnapshot]=useState(null),[restoreBusy,setRestoreBusy]=useState(false),[restoredSettings,setRestoredSettings]=useState|undefined>(),[restoredJobId,setRestoredJobId]=useState(''),[priorTest,setPriorTest]=useState(null); useEffect(()=>()=>{slotSource.current.abort();caseController.current?.abort();documentController.current?.abort();restoreSerial.current++;loadSerial.current++;documentSerial.current++;kernelLoadSerial.current++;task.current?.terminate();if(timer.current)clearTimeout(timer.current);objectUrls.current.forEach(u=>URL.revokeObjectURL(u));},[]); useEffect(()=>{const serial=restoreSerial.current;void loadCadWorkspace().then(value=>{if(serial===restoreSerial.current)setSnapshot(value);}).catch(()=>{});},[]); useEffect(()=>{if(snapshot&&!autoRestore.current&&new URLSearchParams(window.location.search).get('restore')==='1'){autoRestore.current=true;void restoreWorkspace(snapshot);} // Restore only the explicitly saved browser-local source snapshot. // eslint-disable-next-line react-hooks/exhaustive-deps },[snapshot]); useEffect(()=>{if(!restoredJobId)return;const serial=restoreSerial.current;void loadDepthJob(restoredJobId).then(loadDepthTestSummary).then(summary=>{if(serial===restoreSerial.current)setPriorTest(summary);}).catch(()=>{});},[restoredJobId]); useEffect(()=>{if(!initialCase.current&&new URLSearchParams(window.location.search).get("case")==="300-1963-a1"){initialCase.current=true;void Promise.resolve().then(()=>{if(new URLSearchParams(window.location.search).get('guide')==='panel')setGuideVisible(true);void openPublicCase();});} // URL selection is consumed once on page entry, never after a user replaces a source. // eslint-disable-next-line react-hooks/exhaustive-deps },[]); function navigateLesson(target:string){const selector:Record={source:'.cad-source-row',features:'.cad-feature-list-title',plan:'#depth-plan',test:'.depth-output'};return showLessonTarget(guideSurface.current,selector[target]);} function inspectFeature(feature:Selection){setSelection(feature);setDrawingIndex(-1);if(window.matchMedia('(max-width:700px)').matches)showLessonTarget(guideSurface.current,'.cad-center');} function invalidateRestore(){restoreSerial.current++;setRestoreBusy(false);} function cancel(preserveRestore=false){slotSource.current.abort();slotSource.current=new AbortController();setSlotEpoch(value=>value+1);if(!preserveRestore)invalidateRestore();loadSerial.current++;task.current?.terminate();task.current=null;if(timer.current)clearTimeout(timer.current);setBusy("");} function resetWorkspace(){ setGuideVisible(false); restoreSerial.current++;setRestoreBusy(false);activeModel.current=null;setSnapshot(null);setRestoredSettings(undefined);setRestoredJobId('');setPriorTest(null);void clearCadWorkspace().catch(()=>{}); cancel();documentSerial.current++;documentController.current?.abort();documentController.current=null;kernelLoadSerial.current++;caseController.current?.abort();caseController.current=null; setCaseBusy(false);setDocumentBusy(false);setKernelBusy(false);setReplacementKernel(null);setAnalysis(null);setSelection(null);setDrawings([]);setDrawingIndex(-1);setError("");setEncodingNotice("");setStepEncoding("");stepSourceSelected.current=false;setView("iso:"+Date.now()); objectUrls.current.forEach(u=>URL.revokeObjectURL(u));objectUrls.current=[]; if(modelFile.current)modelFile.current.value="";if(kernelFile.current)kernelFile.current.value=""; // Reset clears this session's source/kernel selections; immutable review packages/history are never deleted. } function removeDrawing(index:number){const d=drawings[index];if(!d)return;documentSerial.current++;documentController.current?.abort();documentController.current=null;setDocumentBusy(false);URL.revokeObjectURL(d.url);objectUrls.current=objectUrls.current.filter(u=>u!==d.url);setDrawings(current=>current.filter((_,i)=>i!==index));setDrawingIndex(-1);} function changeStepEncoding(value:StepEncodingChoice){ if(value===stepEncoding)return;invalidateRestore();setStepEncoding(value);setEncodingNotice(""); if(stepSourceSelected.current){cancel();setAnalysis(null);setSelection(null);setError("");setEncodingNotice("STEP decoder changed. Prior analysis was cleared; reopen the STEP file to analyze with this explicit selection.");} } async function openModel(file?:File,encoding:StepEncodingChoice=stepEncoding,restoring=false){ if(!file)return;if(kernelBusy){setError("Wait for the replacement kernel selection to finish, then reopen the model.");return;}cancel(restoring);activeModel.current=null;if(!restoring){setRestoredSettings(undefined);setRestoredJobId('');setPriorTest(null);}setAnalysis(null);setSelection(null);setError("");if(!restoring)setEncodingNotice("");setDrawingIndex(-1); stepSourceSelected.current=/\.(step|stp)$/i.test(file.name); if(!/\.(sldprt|step|stp)$/i.test(file.name)){setError("3D analysis accepts .SLDPRT, .STEP or .STP. Add PDF/PNG in the evidence panel.");return;} if(file.size>20*1024*1024){setError("Model exceeds the 20 MiB limit.");return;} const serial=loadSerial.current;setBusy("Reading model and checking geometry…"); try{const bytes=await file.arrayBuffer();if(serial!==loadSerial.current)return;const expectedHash=await sha256(bytes.slice(0));if(serial!==loadSerial.current)return;const worker=new Worker(modelWorkerUrl,{type:"module"});task.current=worker; const finish=()=>{worker.terminate();if(task.current===worker)task.current=null;if(timer.current)clearTimeout(timer.current);if(serial===loadSerial.current)setBusy("");}; worker.onmessage=(event:MessageEvent<{ok:boolean;result?:Analysis;error?:string}>)=>{ if(serial!==loadSerial.current)return;finish(); if(event.data.ok&&event.data.result){ const result=event.data.result,source=result.report.source; if(source.sha256!==expectedHash||source.byte_length!==file.size||source.filename!==file.name){setError('Original model identity changed during analysis; no result was accepted.');return;} if(result.mode==='step_brep'&&result.report.schema_version==='cad-features-1.2'&&(Object.hasOwn(result,'depth_features')||Object.hasOwn(result,'exact_openings')||result.kernel_source?.kind!=='bundled'||result.kernel_source?.package!=='occt-wasm@3.4.0')){setError('Normalized inspection contained unsupported operation or kernel evidence; no result was accepted.');return;} if(result.mode==='step_brep'&&result.exact_openings){const opening=result.exact_openings;if(opening.source.sha256!==source.sha256||opening.source.byte_length!==source.byte_length||opening.source.filename!==source.filename||opening.step_input.encoding!==result.report.step_input.encoding||opening.step_input.selection!==result.report.step_input.selection||opening.step_input.bom!==result.report.step_input.bom){setError('Exact-opening evidence does not match the current model and decoder; no result was accepted.');return;}} activeModel.current=file;setAnalysis(result); }else setError(event.data.error??"The model could not be analyzed."); }; worker.onerror=()=>{if(serial!==loadSerial.current)return;finish();setError("The CAD worker could not run. Use a current browser with WebAssembly support; the original file was not changed.");}; timer.current=setTimeout(()=>{if(serial!==loadSerial.current)return;finish();setError("Analysis exceeded 60 seconds and was stopped. No partial result was accepted.");},60000); worker.postMessage({filename:file.name,bytes,kernelBytes:replacementKernel?.bytes,kernelName:replacementKernel?.filename,stepEncoding:stepSourceSelected.current?encoding||undefined:undefined},[bytes]); }catch(e){if(serial===loadSerial.current){setBusy("");setError(e instanceof Error?e.message:"Model read failed.");}} } async function saveWorkspace(jobId:string,settings:DepthSettings){ if(!analysis||!activeModel.current)throw new Error('Original model bytes are unavailable; download the review package before leaving.'); const next:CadWorkspaceSnapshot={id:'latest',model:activeModel.current,model_sha256:analysis.report.source.sha256,drawings:drawings.map(d=>({file:d.file,sha256:d.sha256,modelHash:d.modelHash})),step_encoding:stepEncoding,settings,job_id:jobId,saved_at:new Date().toISOString()}; await saveCadWorkspace(next);setSnapshot(next); } async function restoreWorkspace(saved:CadWorkspaceSnapshot){ setGuideVisible(false); cancel();const serial=restoreSerial.current;activeModel.current=null;setAnalysis(null);setSelection(null);setRestoreBusy(true);setPriorTest(null);setError(''); try{ if(!(saved.model instanceof File)||saved.model.size>20*1024*1024||!(/\.(?:step|stp|sldprt)$/i.test(saved.model.name)))throw new Error('Saved model source is invalid; reopen the original file.'); if(await sha256(await saved.model.arrayBuffer())!==saved.model_sha256)throw new Error('Saved model source hash mismatch; reopen the original file.'); if(serial!==restoreSerial.current)return; const prepared:Drawing[]=[]; for(const item of saved.drawings){if(!(item.file instanceof File)||item.file.size>20*1024*1024||!(/\.(?:pdf|png|jpe?g)$/i.test(item.file.name)))throw new Error('Saved drawing source is invalid.');const evidence=/\.pdf$/i.test(item.file.name)?await readPdf(item.file):null;const hash=evidence?.source.sha256??await sha256(await item.file.arrayBuffer());if(hash!==item.sha256)throw new Error('Saved drawing source hash mismatch; reopen the original file.');prepared.push({file:item.file,url:'',evidence,sha256:hash,modelHash:item.modelHash});} if(serial!==restoreSerial.current)return; objectUrls.current.forEach(url=>URL.revokeObjectURL(url));objectUrls.current=[]; for(const drawing of prepared){drawing.url=URL.createObjectURL(drawing.file);objectUrls.current.push(drawing.url);} setDrawings(prepared);setDrawingIndex(-1);setStepEncoding(saved.step_encoding);setRestoredSettings(saved.settings);setRestoredJobId(saved.job_id); void openModel(saved.model,saved.step_encoding,true);setEncodingNotice('Original source bytes restored from this browser and reanalyzed. Generated NC and test evidence remain in the immutable review job; no previous analysis result was trusted.'); }catch(caught){if(serial===restoreSerial.current)setError(caught instanceof Error?caught.message:'Browser-local source restore failed.');} finally{if(serial===restoreSerial.current)setRestoreBusy(false);} } async function selectKernel(file?:File){const serial=++kernelLoadSerial.current;cancel();setAnalysis(null);setSelection(null);setReplacementKernel(null);setError("");setKernelBusy(!!file);if(!file)return;try{if(file.size>32*1024*1024||file.size<8)throw new Error("Kernel must be a compatible WASM file, at most 32 MiB.");const bytes=await file.arrayBuffer();if(serial!==kernelLoadSerial.current)return;const header=new Uint8Array(bytes,0,4);if(header[0]!==0||header[1]!==97||header[2]!==115||header[3]!==109)throw new Error("File does not have a WASM header.");setReplacementKernel({filename:file.name,bytes});}catch(e){if(serial===kernelLoadSerial.current)setError(e instanceof Error?e.message:"Kernel could not be read.");}finally{if(serial===kernelLoadSerial.current)setKernelBusy(false);}} async function openPublicCase(){ if(caseController.current||busy||documentBusy||kernelBusy)return; cancel();setAnalysis(null);setSelection(null);setDrawings([]);setDrawingIndex(-1);setError("");setCaseBusy(true); objectUrls.current.forEach(u=>URL.revokeObjectURL(u));objectUrls.current=[]; const controller=new AbortController();caseController.current=controller; const deadline=setTimeout(()=>controller.abort(),60000); try{ const files=await loadPublicCaseAssets(PUBLIC_CASE_ASSETS,{signal:controller.signal}); const native=files.find(f=>f.asset.role==="native_part");if(!native)throw new Error("Public case has no native part."); const prepared:Drawing[]=[]; for(const {asset,bytes} of files){if(asset.role==="native_part")continue;if(controller.signal.aborted)throw new Error("Case loading cancelled.");const file=new File([bytes],asset.filename,{type:asset.role==="reference_png"?"image/png":"application/pdf"}),evidence=asset.role==="reference_png"?null:await readPdf(file,controller.signal);prepared.push({file,url:"",evidence,sha256:asset.sha256,modelHash:native.asset.sha256});} if(controller.signal.aborted)throw new Error("Case loading cancelled."); for(const d of prepared){d.url=URL.createObjectURL(d.file);objectUrls.current.push(d.url);}setDrawings(prepared); await openModel(new File([native.bytes],native.asset.filename)); }catch(e){if(caseController.current===controller){cancel();setAnalysis(null);setDrawings([]);setSelection(null);setError(e instanceof Error?e.message:"Public case could not be loaded.");}} finally{clearTimeout(deadline);if(caseController.current===controller){caseController.current=null;setCaseBusy(false);}} } async function addDrawings(files:FileList|null){ if(!files||!files.length||documentBusy||caseBusy)return;const serial=++documentSerial.current,controller=new AbortController(),startIndex=drawings.length;documentController.current=controller;setDocumentBusy(true);setError(""); try{if(drawings.length+files.length>6)throw new Error("Review up to six supplementary drawings per package.");const prepared:Drawing[]=[]; for(const file of Array.from(files)){if(!/\.(pdf|png|jpe?g)$/i.test(file.name)||file.size>20*1024*1024)throw new Error("Supplementary files must be PDF/PNG/JPG, at most 20 MiB each.");const evidence=/\.pdf$/i.test(file.name)?await readPdf(file,controller.signal):null,hash=evidence?.source.sha256??await sha256(await file.arrayBuffer());if(serial!==documentSerial.current||controller.signal.aborted)return;prepared.push({file,url:"",evidence,sha256:hash,modelHash:null});} if(serial!==documentSerial.current||controller.signal.aborted)return;for(const d of prepared){d.url=URL.createObjectURL(d.file);objectUrls.current.push(d.url);}setDrawings(current=>[...current,...prepared]);setDrawingIndex(startIndex+prepared.length-1); }catch(e){if(serial===documentSerial.current&&!controller.signal.aborted)setError(e instanceof Error?e.message:"Drawing extraction failed.");}finally{if(serial===documentSerial.current){documentController.current=null;setDocumentBusy(false);}} } const dimensions=useMemo(()=>!analysis?[]:analysis.mode==="native_cache"?analysis.report.model.dimensions_mm:Object.values(analysis.report.model.dimensions_mm),[analysis]); const meshes=useMemo(()=>{if(!analysis)return[];if(analysis.mode==="native_cache")return analysis.report.meshes.map(m=>({id:m.id,positions:m.positions_mm,normals:m.normals,indices:m.triangles}));const m=analysis.report.mesh;if(!m)return[];return m.face_groups.map(g=>({id:g.face_id,positions:m.positions,normals:m.normals,indices:m.indices.slice(g.triangle_start*3,(g.triangle_start+g.triangle_count)*3)}));},[analysis]); const features=useMemo(()=>{if(!analysis)return[];if(analysis.depth_features?.features.length){const all=analysis.depth_features.features,linked=new Set(all.flatMap(f=>f.kind==="counterbore_candidate"?f.segment_feature_ids:f.kind==="obround_recess_candidate"?f.linked_bore_feature_ids:[]));return all.filter(f=>!linked.has(f.id)).map((f,index)=>{const links=f.kind==="obround_recess_candidate"?all.filter(b=>f.linked_bore_feature_ids.includes(b.id)).flatMap(b=>b.mesh_ids):[],position='entry_position_mm' in f?f.entry_position_mm:f.axis_origin_mm,at=` · X${fmt(position[0])} Y${fmt(position[1])}`;return{id:f.id,label:f.kind==="counterbore_candidate"?`Counterbore ${String(index+1).padStart(2,'0')}${at} · Ø${fmt(f.small_bore_diameter_mm)} / ${fmt(f.recess_diameter_mm)} · depth ${fmt(f.recess_depth_mm)}`:f.kind==="obround_recess_candidate"?`Obround recess ${String(index+1).padStart(2,'0')}${at} · ${fmt(f.width_mm)} × ${fmt(f.length_mm)} · depth ${fmt(f.depth_mm)}`:f.kind==="obround_wall_candidate"?`Obround wall ${String(index+1).padStart(2,'0')}${at} · ${fmt(f.width_mm)} × ${fmt(f.length_mm)} · exposed ${fmt(f.depth_mm)} · opening unverified`:`Bore segment ${String(index+1).padStart(2,'0')}${at} · Ø${fmt(f.diameter_mm)} · exposed ${fmt(f.depth_mm)}`,meshIds:[...f.mesh_ids,...links],data:f};});}if(analysis.mode==="native_cache")return analysis.cache_features.features.map(f=>({id:f.id,label:f.kind.replaceAll("_"," ")+(f.diameter_mm?` · Ø${fmt(f.diameter_mm)}`:""),meshIds:f.mesh_ids,data:f}));return analysis.report.features.map(f=>({id:f.id,label:f.kind.replaceAll("_"," ")+` candidate`+(f.dimensions.diameter_mm?` · Ø${fmt(f.dimensions.diameter_mm)}`:""),meshIds:f.face_ids,data:f}));},[analysis]); const surfaces=useMemo(()=>{if(!analysis)return[];if(analysis.mode==="native_cache"){const classified=new Map((analysis.depth_features?.surfaces??analysis.cache_features.surfaces).flatMap(s=>s.mesh_ids.map(id=>[id,s] as const)));return analysis.report.meshes.map(m=>({id:m.id,label:classified.get(m.id)?.kind.replaceAll("_"," ")??"Unclassified cache surface",meshIds:[m.id],data:classified.get(m.id)??{id:m.id,status:"unclassified",vertex_count:m.positions_mm.length/3,triangle_count:m.triangles.length/3}}));}return analysis.report.faces.map(f=>({id:f.id,label:f.surface_type+" · "+f.id,meshIds:[f.id],data:f}));},[analysis]); const exactFeatures=useMemo(()=>analysis?.mode==='step_brep'?analysis.exact_openings?.candidates.map(exactOpeningSelection)??[]:[],[analysis]); const unbound=analysis?drawings.filter(d=>d.modelHash!==analysis.report.source.sha256):[]; const selectedIds=selection?.meshIds??[],evidence=analysis?drawings.flatMap(d=>d.evidence&&d.modelHash===analysis.report.source.sha256?[d.evidence]:[]):[],findings=analysis?compareDocumentEvidence(evidence,dimensions):[]; const warnings=analysis?[...analysis.report.warnings,...(analysis.mode==="native_cache"?analysis.cache_features.limitations:analysis.report.limitations),...(analysis.depth_features?.limitations??[]),...(analysis.depth_error?[analysis.depth_error]:[])]:[]; const drawing=drawings[drawingIndex]; const normalizedReport=analysis?.mode==='step_brep'&&analysis.report.schema_version==='cad-features-1.2'?analysis.report:null; const slotAvailable=analysis?.mode==='step_brep'&&analysis.report.schema_version==='cad-features-1.1'&&analysis.kernel_source?.kind==='bundled'&&!!activeModel.current; function exportReport(){if(!analysis&&!drawings.length)return;const stem=analysis?.report.source.filename.replace(/\.[^.]+$/,"")??"document-review";download(stem+"-review.json",{schema_version:"cam-review-package-1.0",model_analysis:analysis,documents:drawings.flatMap(d=>d.evidence?[d.evidence]:[]),images:drawings.filter(d=>!d.evidence).map(d=>({filename:d.file.name,sha256:d.sha256,byte_length:d.file.size})),source_associations:drawings.map(d=>({filename:d.file.name,sha256:d.sha256,byte_length:d.file.size,media_type:d.evidence?"application/pdf":"image",associated_model_sha256:d.modelHash,association_status:!analysis?"not_associated":d.modelHash===analysis.report.source.sha256?"confirmed":"unconfirmed"})),cross_source_findings:findings,scope:"Browser-local recognition and document evidence; images do not establish scale or depth; not an approved manufacturing program",machine_release:{eligible:false}});} return
{guideVisible&&setGuideVisible(false)}/>}
{analysis?.mode==="native_cache"?"SLDPRT saved display geometry · approximate feature candidates, not exact B-rep":analysis?.mode==="step_brep"?"STEP solid geometry · analytical surfaces and bounded feature candidates":"Single-part STEP and native SLDPRT inspection"}Machine release unavailable
{normalizedReport&&
Inspection only · {normalizedReport.features.length} analytical feature candidate(s) · 0 executable intents

Source units: {normalizedReport.step_units.source_unit}. Geometry is displayed in millimetres using the verified importer, without rewriting the original file or applying a second scale. Feature recognition does not yet establish a supported machining strategy for this import path.

} {analysis?.mode==="native_cache"&&analysis.depth_features?.features.length===0&&
Saved display mesh only · 0 supported depth intents

No bounded machining feature was recovered from this cache. If SOLIDWORKS can open the part, save a separate STEP AP214/AP242 copy and select it with Open 3D model to check for an exact solid. Keep the original; verify the export against its dimensions and drawing revision. Exact import alone does not enable machining.

} {analysis?.mode==="native_cache"&&!!analysis.depth_features?.features.some(f=>f.kind==="obround_wall_candidate")&&
{analysis.depth_features.features.filter(f=>f.kind==="obround_wall_candidate").length} obround wall candidate(s) recognized · not yet cutting operations

Two curved ends and two tangent side walls are matched per candidate. To inspect the solid rather than its display cache, export a separate STEP AP214/AP242 copy from SOLIDWORKS and reopen it here. The slot opening, local blank thickness and accessible setup still need proof before G/M generation.

} {analysis?.mode==="step_brep"&&analysis.report.features.length===0&&exactFeatures.length===0&&
STEP solid imported · 0 supported exact feature candidates · 0 executable depth intents

The matching STEP provides an exact solid for inspection, but its current surface coverage does not match a supported feature strategy. Approximate tessellation-only candidates are withheld from executable depth intents, so Generate remains unavailable. All exact faces and unsupported surfaces remain inspectable below; this import does not authorize machining.

}
{analysis ? analysis.report.source.filename : drawings.length ? "Document-only review" : 'From geometry to reviewed motion'}Browser-local analysis. Original files are not uploaded.
{analysis ? <>Go to operation plan{slotAvailable&&Reviewed slot machining} : }
{snapshot&&!analysis&&!busy&&!restoreBusy&&
Browser-local source snapshot available

Saved {new Date(snapshot.saved_at).toLocaleString()}. Restoring checks original bytes and reruns analysis; prior generated NC remains in its immutable test job.

Open saved NC/test job
} {restoredJobId&&analysis&&
Original source reanalyzed. Settings restored; generated NC and test evidence were not reused as current analysis. {priorTest&&Saved job prior local test: {priorTest.verdict} · {new Date(priorTest.tested_at).toLocaleString()} (rerun for full evidence).}Open original saved NC/test job
} {error&&
{error}
}{busy&&
{busy}
}{restoreBusy&&
Verifying browser-local source bytes…
}{caseBusy&&
Loading and checking public case files…
}
{["iso","front","back","side"].map(v=>)}Drag to rotate · scroll to zoom · right-drag to pan
{drawing?
{drawing.evidence?:}
:!analysis&&drawings.length>0?
DOCUMENT REVIEW ONLY

No model selected

Review PDF text or inspect reference images without converting 2D content into 3D dimensions, scale, depth or machining code.

Bind sources to a model only after confirming they belong together.

:setSelection(surfaces.find(s=>s.meshIds.includes(id))??null)}/>}
{dimensions.length?<>Model bounds: {dimensions.map(fmt).join(" × ")} mm{meshes.length} visible surfaces:drawings.length?"Document-only review. No model geometry or machining values are inferred.":"No model substituted. Open an original CAD file to begin."}
{analysis&&{setSelection(exactOpeningSelection(candidate));setDrawingIndex(-1);}}/>} {analysis?.mode==='step_brep'&&analysis.report.schema_version==='cad-features-1.1'&&analysis.kernel_source?.kind==='bundled'&&activeModel.current&&{setSelection({id:target.id,label:target.id,meshIds:target.face_ids,data:target});setDrawingIndex(-1);}}/>}

Cross-source review

{!analysis&&drawings.length>0&&
Evidence only

PDF text and image references are retained locally for review. Without a confirmed model, no cross-source comparison or machining output is produced.

}{unbound.length>0&&
Confirm the source package

{unbound.length} retained drawing(s) have not been associated with this model. Their declarations are excluded from model comparison until you confirm they belong together.

}{!findings.length?

{analysis?"Add optional engineering and stock/schedule PDFs to compare available declarations. PDF symbols and missing units still require review.":"No model comparison is available."}

:findings.map((f,i)=>
{f.severity}

{f.detail}

)}
{analysis&&d.sha256+":"+d.modelHash).join("|")} analysis={analysis} findings={findings} initialSettings={restoredSettings} saveWorkspace={saveWorkspace} onSelectFeature={id=>{const feature=features.find(item=>item.id===id),source=analysis.depth_features?.features.find(item=>item.id===id);if(feature||source){setSelection(feature??{id,label:source!.kind.replaceAll('_',' ')+` · ${source!.id}`,meshIds:source!.mesh_ids,data:source});setDrawingIndex(-1);}}}/>} {analysis&&}
; }