atproto Thingiverse but good
0

Configure Feed

Select the types of activity you want to include in your feed.

polymodel / public / step_worker.js
7.4 kB 224 lines
1const DEFAULT_STEP_URL = '/models/EInk_click_v101.step'; 2const DEFAULT_LIN_DEFLECTION = 0.1; 3const DEFAULT_ANG_DEFLECTION = 0.1; 4 5let openCascadePromise; 6 7self.postMessage({ kind: 'ready' }); 8 9self.onmessage = async (event) => { 10 const msg = event.data || {}; 11 if (msg.command !== 'convert-step') { 12 return; 13 } 14 15 const timings = []; 16 const startedAt = performance.now(); 17 18 try { 19 const stepUrl = msg.stepUrl || DEFAULT_STEP_URL; 20 const linDeflection = Number(msg.linDeflection ?? DEFAULT_LIN_DEFLECTION); 21 const angDeflection = Number(msg.angDeflection ?? DEFAULT_ANG_DEFLECTION); 22 23 mark(timings, 'fetch:start'); 24 const stepBytes = new Uint8Array(await (await fetch(stepUrl)).arrayBuffer()); 25 mark(timings, 'fetch:end'); 26 27 mark(timings, 'init:start'); 28 const oc = await loadOpenCascade(); 29 validateRequiredBindings(oc); 30 mark(timings, 'init:end'); 31 32 mark(timings, 'read:start'); 33 writeFile(oc, '/model.step', stepBytes); 34 const doc = readStepIntoDocument(oc, '/model.step'); 35 mark(timings, 'read:end'); 36 37 mark(timings, 'tessellate:start'); 38 const shape = firstFreeShape(oc, doc); 39 if (oc.BRepTools?.Clean && shape) { 40 oc.BRepTools.Clean(shape, false); 41 } 42 if (shape) { 43 new oc.BRepMesh_IncrementalMesh_2(shape, linDeflection, false, angDeflection, false); 44 } 45 mark(timings, 'tessellate:end'); 46 47 mark(timings, 'export:start'); 48 const glbBytes = exportDocumentToGlb(oc, doc, '/model.glb'); 49 mark(timings, 'export:end'); 50 51 self.postMessage({ 52 kind: 'converted', 53 timings, 54 totalMs: performance.now() - startedAt, 55 stepBytes: stepBytes.byteLength, 56 glbBytes: glbBytes.byteLength, 57 buffer: glbBytes.buffer, 58 crossOriginIsolated: self.crossOriginIsolated === true, 59 hardwareConcurrency: navigator.hardwareConcurrency || null, 60 }, [glbBytes.buffer]); 61 } catch (error) { 62 self.postMessage({ 63 kind: 'error', 64 timings, 65 totalMs: performance.now() - startedAt, 66 message: error instanceof Error ? error.message : String(error), 67 stack: error instanceof Error ? error.stack : undefined, 68 crossOriginIsolated: self.crossOriginIsolated === true, 69 hardwareConcurrency: navigator.hardwareConcurrency || null, 70 }); 71 } 72}; 73 74async function loadOpenCascade() { 75 if (!openCascadePromise) { 76 openCascadePromise = (async () => { 77 const openCascadeUrl = new URL('/vendor/opencascade/opencascade.js', self.location.origin).href; 78 const module = await import(openCascadeUrl); 79 const init = module.default || module.initOpenCascade || module.init; 80 if (typeof init !== 'function') { 81 throw new Error('OpenCascade module did not export an init function'); 82 } 83 const options = { 84 locateFile(path) { 85 if (path.endsWith('.wasm')) { 86 return '/vendor/opencascade/opencascade.wasm'; 87 } 88 return `/vendor/opencascade/${path}`; 89 }, 90 }; 91 return init(options); 92 })(); 93 } 94 return openCascadePromise; 95} 96 97function writeFile(oc, path, bytes) { 98 if (oc.FS.analyzePath(path).exists) { 99 oc.FS.unlink(path); 100 } 101 oc.FS.writeFile(path, bytes); 102} 103 104function validateRequiredBindings(oc) { 105 const required = [ 106 ['STEPCAFControl_Reader_1', 'STEPCAFControl_Reader'], 107 ['TDocStd_Document'], 108 ['Handle_TDocStd_Document_2'], 109 ['TCollection_ExtendedString_1'], 110 ['XCAFDoc_DocumentTool'], 111 ['XCAFDoc_ShapeTool'], 112 ['TDF_LabelSequence_1'], 113 ['BRepMesh_IncrementalMesh_2', 'BRepMesh_IncrementalMesh'], 114 ['RWGltf_CafWriter'], 115 ['TCollection_AsciiString_2'], 116 ['TColStd_IndexedDataMapOfStringString_1'], 117 ]; 118 const missing = required 119 .filter((alternatives) => !alternatives.some((name) => oc[name] !== undefined)) 120 .map((alternatives) => alternatives.join(' or ')); 121 if (missing.length > 0) { 122 throw new Error(`OpenCascade.js build is missing required STEP→GLB bindings: ${missing.join(', ')}`); 123 } 124} 125 126function readStepIntoDocument(oc, path) { 127 const reader = constructFirst(oc, ['STEPCAFControl_Reader_1', 'STEPCAFControl_Reader']); 128 const readStatus = reader.ReadFile(path); 129 if (readStatus !== undefined && isFailureStatus(oc, readStatus)) { 130 throw new Error(`STEP read failed with status ${readStatus}`); 131 } 132 133 const doc = constructDocument(oc); 134 const handle = construct(oc, 'Handle_TDocStd_Document_2', doc); 135 const progress = optionalConstruct(oc, 'Message_ProgressRange_1'); 136 const transferred = progress 137 ? callFirst(reader, ['Transfer_1', 'Transfer'], [handle, progress]) 138 : callFirst(reader, ['Transfer_1', 'Transfer'], [handle]); 139 if (transferred === false) { 140 throw new Error('STEP transfer into XDE document failed'); 141 } 142 return doc; 143} 144 145function constructDocument(oc) { 146 const name = construct(oc, 'TCollection_ExtendedString_1'); 147 return construct(oc, 'TDocStd_Document', name); 148} 149 150function firstFreeShape(oc, doc) { 151 if (!oc.XCAFDoc_DocumentTool?.ShapeTool) { 152 return undefined; 153 } 154 const shapeToolHandle = oc.XCAFDoc_DocumentTool.ShapeTool(doc.Main()); 155 const shapeTool = shapeToolHandle?.get ? shapeToolHandle.get() : shapeToolHandle; 156 const labels = construct(oc, 'TDF_LabelSequence_1'); 157 shapeTool.GetFreeShapes(labels); 158 if (labels.Length && labels.Length() < 1) { 159 return undefined; 160 } 161 const label = labels.First(); 162 return callFirst(oc.XCAFDoc_ShapeTool, ['GetShape_2', 'GetShape'], [label]); 163} 164 165function exportDocumentToGlb(oc, doc, outputPath) { 166 if (!oc.RWGltf_CafWriter) { 167 throw new Error('RWGltf_CafWriter is missing from this OpenCascade.js build'); 168 } 169 if (oc.FS.analyzePath(outputPath).exists) { 170 oc.FS.unlink(outputPath); 171 } 172 const writer = new oc.RWGltf_CafWriter(construct(oc, 'TCollection_AsciiString_2', outputPath), true); 173 const handle = construct(oc, 'Handle_TDocStd_Document_2', doc); 174 const metadata = construct(oc, 'TColStd_IndexedDataMapOfStringString_1'); 175 const progress = optionalConstruct(oc, 'Message_ProgressRange_1'); 176 const exported = progress 177 ? callFirst(writer, ['Perform_2', 'Perform'], [handle, metadata, progress]) 178 : callFirst(writer, ['Perform_2', 'Perform'], [handle, metadata]); 179 if (exported === false) { 180 throw new Error('RWGltf_CafWriter failed to export GLB'); 181 } 182 return oc.FS.readFile(outputPath, { encoding: 'binary' }); 183} 184 185function construct(oc, name, ...args) { 186 const ctor = oc[name]; 187 if (typeof ctor !== 'function') { 188 throw new Error(`${name} is missing from this OpenCascade.js build`); 189 } 190 return new ctor(...args); 191} 192 193function optionalConstruct(oc, name, ...args) { 194 return typeof oc[name] === 'function' ? new oc[name](...args) : undefined; 195} 196 197function constructFirst(oc, names, ...args) { 198 for (const name of names) { 199 if (typeof oc[name] === 'function') { 200 return new oc[name](...args); 201 } 202 } 203 throw new Error(`${names.join(' or ')} is missing from this OpenCascade.js build`); 204} 205 206function callFirst(target, names, args) { 207 for (const name of names) { 208 if (typeof target?.[name] === 'function') { 209 return target[name](...args); 210 } 211 } 212 throw new Error(`missing method ${names.join(' or ')}`); 213} 214 215function isFailureStatus(oc, status) { 216 if (typeof oc.IFSelect_ReturnStatus?.IFSelect_RetDone === 'number') { 217 return status !== oc.IFSelect_ReturnStatus.IFSelect_RetDone; 218 } 219 return false; 220} 221 222function mark(timings, label) { 223 timings.push({ label, ms: performance.now() }); 224}