dsh-cad 0.2.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,235 +1,350 @@
1
- /**
2
- * Modeling worker entry (plain CJS — the emscripten loader must not be pulled
3
- * through ESM; see the M0 notes on ERR_AMBIGUOUS_MODULE_SYNTAX).
4
- *
5
- * Protocol (main → worker): {jobId, op} where op replays one document
6
- * operation. (worker → main): {jobId, ok, result} with transferable buffers
7
- * for tessellation data.
8
- */
9
- 'use strict'
10
-
11
- const { parentPort } = require('node:worker_threads')
12
- const path = require('node:path')
13
- const fs = require('node:fs')
14
- const { createRequire } = require('node:module')
15
- const { createAdapter } = require('./occt-adapter.cjs')
16
- const { projectViews } = require('./hlr.cjs')
17
-
18
- const require3 = createRequire(__filename)
19
- // The ES6 emscripten build reads a global __dirname when its factory runs.
20
- if (typeof globalThis.__dirname === 'undefined') globalThis.__dirname = __dirname
21
-
22
- const loaderPath = require3.resolve('opencascade.js/dist/opencascade.wasm.js')
23
- const loaderModule = require3(loaderPath)
24
- const wasmBinary = fs.readFileSync(path.join(path.dirname(loaderPath), 'opencascade.wasm.wasm'))
25
-
26
- let adapter = null
27
- let occt = null
28
-
29
- /** bodyId → { shape, name } — the live document. */
30
- const bodies = new Map()
31
- let nextBodyNumber = 1
32
-
33
- /** instanceId → { instanceId, bodyId, name, translate, rotate } — the assembly. */
34
- const instances = new Map()
35
-
36
- const triplet = (value) => (Array.isArray(value) && value.length === 3 ? value : [0, 0, 0])
37
-
38
- function instancesList() {
39
- const list = []
40
- for (const instance of instances.values()) {
41
- list.push({
42
- instanceId: instance.instanceId,
43
- bodyId: instance.bodyId,
44
- name: instance.name,
45
- translate: triplet(instance.translate),
46
- rotate: triplet(instance.rotate),
47
- })
48
- }
49
- return list
50
- }
51
-
52
- function meshOf(shape, name) {
53
- const { positions, indices } = adapter.tessellate(shape)
54
- const normals = adapter.faceNormals(positions, indices)
55
- return {
56
- name,
57
- positions,
58
- normals,
59
- indices,
60
- vertexCount: positions.length / 3,
61
- triangleCount: indices.length / 3,
62
- }
63
- }
64
-
65
- function applyOp(op) {
66
- switch (op.kind) {
67
- case 'create_prim': {
68
- const bodyId = op.bodyId
69
- const shape = adapter.makePrim(op.prim, op.params ?? {})
70
- bodies.set(bodyId, { shape, name: op.name ?? bodyId })
71
- return { bodyId, name: bodies.get(bodyId).name, mesh: meshOf(shape, bodies.get(bodyId).name) }
72
- }
73
- case 'extrude_profile': {
74
- const bodyId = op.bodyId
75
- const shape = adapter.makeExtrudedProfile(op.points, op.height ?? 10, op.base ?? 0)
76
- bodies.set(bodyId, { shape, name: op.name ?? bodyId })
77
- return { bodyId, name: bodies.get(bodyId).name, mesh: meshOf(shape, bodies.get(bodyId).name) }
78
- }
79
- case 'boolean': {
80
- const target = bodies.get(op.target)
81
- if (target === undefined) throw new Error(`unknown body: ${op.target}`)
82
- const tools = op.tools.map((id) => {
83
- const tool = bodies.get(id)
84
- if (tool === undefined) throw new Error(`unknown body: ${id}`)
85
- return tool.shape
86
- })
87
- const shape = adapter.boolean(op.op, target.shape, tools)
88
- target.shape = shape
89
- // Consumed tool bodies are removed from the document (CAD convention).
90
- const removed = []
91
- for (const id of op.tools) {
92
- if (id !== op.target && bodies.delete(id)) removed.push(id)
93
- }
94
- return { bodyId: op.target, name: target.name, removed, mesh: meshOf(shape, target.name) }
95
- }
96
- case 'fillet': {
97
- const body = bodies.get(op.target)
98
- if (body === undefined) throw new Error(`unknown body: ${op.target}`)
99
- const { shape, edges } = adapter.filletAll(body.shape, op.radius ?? 1)
100
- body.shape = shape
101
- return { bodyId: op.target, name: body.name, edges, mesh: meshOf(shape, body.name) }
102
- }
103
- case 'transform': {
104
- const body = bodies.get(op.target)
105
- if (body === undefined) throw new Error(`unknown body: ${op.target}`)
106
- const shape = adapter.transform(body.shape, {
107
- translate: op.translate,
108
- rotate: op.rotate,
109
- mirror: op.mirror,
110
- })
111
- body.shape = shape
112
- return { bodyId: op.target, name: body.name, mesh: meshOf(shape, body.name) }
113
- }
114
- case 'tessellate_all': {
115
- const meshes = []
116
- for (const [bodyId, body] of bodies) {
117
- meshes.push({ bodyId, name: body.name, ...meshOf(body.shape, body.name) })
118
- }
119
- return { meshes }
120
- }
121
- case 'export': {
122
- const body = bodies.get(op.target)
123
- if (body === undefined) throw new Error(`unknown body: ${op.target}`)
124
- const bytes = adapter.exportFile(body.shape, op.format)
125
- return { bytes: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) }
126
- }
127
- case 'drawing': {
128
- const body = bodies.get(op.target)
129
- if (body === undefined) throw new Error(`unknown body: ${op.target}`)
130
- // Finer tessellation for drawings: hidden-line views are judged at
131
- // silhouette accuracy, not screen-approximation accuracy.
132
- const { positions, indices } = adapter.tessellate(body.shape, 0.1)
133
- const views = Array.isArray(op.views) && op.views.length > 0 ? op.views : DEFAULT_DRAWING_VIEWS
134
- return { views: projectViews({ positions, indices }, views).views }
135
- }
136
- case 'assembly_insert': {
137
- const body = bodies.get(op.bodyId)
138
- if (body === undefined) throw new Error(`unknown body: ${op.bodyId}`)
139
- if (instances.has(op.instanceId)) throw new Error(`instance already exists: ${op.instanceId}`)
140
- instances.set(op.instanceId, {
141
- instanceId: op.instanceId,
142
- bodyId: op.bodyId,
143
- name: op.name ?? body.name,
144
- translate: triplet(op.translate),
145
- rotate: triplet(op.rotate),
146
- })
147
- return { instanceId: op.instanceId, bodyId: op.bodyId, instances: instancesList() }
148
- }
149
- case 'assembly_transform': {
150
- const instance = instances.get(op.instanceId)
151
- if (instance === undefined) throw new Error(`unknown instance: ${op.instanceId}`)
152
- if (op.translate !== undefined) instance.translate = triplet(op.translate)
153
- if (op.rotate !== undefined) instance.rotate = triplet(op.rotate)
154
- return { instanceId: op.instanceId, instances: instancesList() }
155
- }
156
- case 'assembly_remove': {
157
- if (!instances.delete(op.instanceId)) throw new Error(`unknown instance: ${op.instanceId}`)
158
- return { instanceId: op.instanceId, removed: op.instanceId, instances: instancesList() }
159
- }
160
- case 'export_assembly': {
161
- if (instances.size === 0) throw new Error('the assembly is empty')
162
- const builder = new occt.BRep_Builder()
163
- const compound = new occt.TopoDS_Compound()
164
- builder.MakeCompound(compound)
165
- let added = 0
166
- for (const instance of instances.values()) {
167
- const body = bodies.get(instance.bodyId)
168
- if (body === undefined) continue // stale instance of a consumed body
169
- builder.Add(compound, adapter.transform(body.shape, {
170
- translate: instance.translate,
171
- rotate: instance.rotate,
172
- }))
173
- added++
174
- }
175
- if (added === 0) throw new Error('no instance references a live body')
176
- const bytes = adapter.exportFile(compound, op.format)
177
- return { bytes: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), instances: instancesList() }
178
- }
179
- case 'volume': {
180
- const body = bodies.get(op.target)
181
- if (body === undefined) throw new Error(`unknown body: ${op.target}`)
182
- return { volume: adapter.volume(body.shape) }
183
- }
184
- case 'delete': {
185
- if (!bodies.delete(op.target)) throw new Error(`unknown body: ${op.target}`)
186
- return { deleted: op.target }
187
- }
188
- case 'reset': {
189
- bodies.clear()
190
- instances.clear()
191
- nextBodyNumber = 1
192
- return { cleared: true }
193
- }
194
- default:
195
- throw new Error(`unknown op: ${op.kind}`)
196
- }
197
- }
198
-
199
- /** Standard third/first-angle drawing views (GB first-angle arrangement). */
200
- const DEFAULT_DRAWING_VIEWS = [
201
- { name: 'front', dir: [0, -1, 0], xDir: [1, 0, 0] },
202
- { name: 'top', dir: [0, 0, 1], xDir: [1, 0, 0] },
203
- { name: 'left', dir: [-1, 0, 0], xDir: [0, 1, 0] },
204
- { name: 'iso', dir: [1, -1, 1], xDir: [1, 1, 0] },
205
- ]
206
-
207
- const initOpenCascade = loaderModule.default ?? loaderModule
208
- initOpenCascade({ wasmBinary }).then((instance) => {
209
- occt = instance
210
- adapter = createAdapter(occt)
211
- parentPort.on('message', (message) => {
212
- const transfers = []
213
- try {
214
- const result = applyOp(message.op)
215
- // Collect transferable mesh buffers.
216
- const collect = (mesh) => {
217
- if (mesh === undefined) return
218
- transfers.push(mesh.positions.buffer, mesh.indices.buffer, mesh.normals.buffer)
219
- }
220
- if (result.mesh !== undefined) collect(result.mesh)
221
- for (const mesh of result.meshes ?? []) collect(mesh)
222
- if (result.bytes !== undefined) transfers.push(result.bytes)
223
- parentPort.postMessage({ jobId: message.jobId, ok: true, result }, transfers)
224
- } catch (error) {
225
- parentPort.postMessage({
226
- jobId: message.jobId,
227
- ok: false,
228
- error: error instanceof Error ? error.message : String(error),
229
- })
230
- }
231
- })
232
- parentPort.postMessage({ jobId: 0, ok: true, result: { ready: true } })
233
- }).catch((error) => {
234
- parentPort.postMessage({ jobId: 0, ok: false, error: `modeling kernel init failed: ${error.message}` })
235
- })
1
+ /**
2
+ * Modeling worker entry (plain CJS — the emscripten loader must not be pulled
3
+ * through ESM; see the M0 notes on ERR_AMBIGUOUS_MODULE_SYNTAX).
4
+ *
5
+ * Protocol (main → worker): {jobId, op} where op replays one document
6
+ * operation. (worker → main): {jobId, ok, result} with transferable buffers
7
+ * for tessellation data.
8
+ */
9
+ 'use strict'
10
+
11
+ const { parentPort } = require('node:worker_threads')
12
+ const path = require('node:path')
13
+ const fs = require('node:fs')
14
+ const { createRequire } = require('node:module')
15
+ const { createAdapter } = require('./occt-adapter.cjs')
16
+ const { createOcctBridge } = require('./occt-bridge.cjs')
17
+
18
+ const require3 = createRequire(__filename)
19
+ // The ES6 emscripten build reads a global __dirname when its factory runs.
20
+ if (typeof globalThis.__dirname === 'undefined') globalThis.__dirname = __dirname
21
+
22
+ const loaderPath = require3.resolve('opencascade.js/dist/opencascade.wasm.js')
23
+ const loaderModule = require3(loaderPath)
24
+ const wasmBinary = fs.readFileSync(path.join(path.dirname(loaderPath), 'opencascade.wasm.wasm'))
25
+
26
+ let adapter = null
27
+ let occt = null
28
+
29
+ /**
30
+ * Reject a degenerate result BEFORE registering it: the kernel cannot
31
+ * tessellate a self-intersecting BRep (it fails deep inside the mesher with
32
+ * a cryptic error), and an unrenderable body in the document helps no one.
33
+ */
34
+ function assertUsable(shape, what, hint) {
35
+ if (adapter.isValid(shape) === false) {
36
+ throw new Error(`the ${what} produced an invalid shape (self-intersecting or degenerate)${hint === undefined ? '' : `: ${hint}`}`)
37
+ }
38
+ }
39
+
40
+ /** bodyId → { shape, name } — the live document. */
41
+ const bodies = new Map()
42
+ let nextBodyNumber = 1
43
+
44
+ /** instanceId → { instanceId, bodyId, name, translate, rotate } — the assembly. */
45
+ const instances = new Map()
46
+
47
+ const triplet = (value) => (Array.isArray(value) && value.length === 3 ? value : [0, 0, 0])
48
+
49
+ function instancesList() {
50
+ const list = []
51
+ for (const instance of instances.values()) {
52
+ list.push({
53
+ instanceId: instance.instanceId,
54
+ bodyId: instance.bodyId,
55
+ name: instance.name,
56
+ translate: triplet(instance.translate),
57
+ rotate: triplet(instance.rotate),
58
+ })
59
+ }
60
+ return list
61
+ }
62
+
63
+ function meshOf(shape, name) {
64
+ const { positions, indices } = adapter.tessellate(shape)
65
+ const normals = adapter.faceNormals(positions, indices)
66
+ return {
67
+ name,
68
+ positions,
69
+ normals,
70
+ indices,
71
+ vertexCount: positions.length / 3,
72
+ triangleCount: indices.length / 3,
73
+ }
74
+ }
75
+
76
+ async function applyOp(op) {
77
+ switch (op.kind) {
78
+ case 'create_prim': {
79
+ const bodyId = op.bodyId
80
+ const shape = adapter.makePrim(op.prim, op.params ?? {})
81
+ bodies.set(bodyId, { shape, name: op.name ?? bodyId })
82
+ return { bodyId, name: bodies.get(bodyId).name, mesh: meshOf(shape, bodies.get(bodyId).name) }
83
+ }
84
+ case 'extrude_profile': {
85
+ const bodyId = op.bodyId
86
+ // `profile` (segments/circle object) supersedes the legacy flat `points`;
87
+ // both forms flow through the same curve-aware builder.
88
+ const profile = op.profile ?? op.points
89
+ const shape = adapter.extrudeProfile2D(profile, op.height ?? 10, op.base ?? 0)
90
+ bodies.set(bodyId, { shape, name: op.name ?? bodyId })
91
+ return { bodyId, name: bodies.get(bodyId).name, mesh: meshOf(shape, bodies.get(bodyId).name) }
92
+ }
93
+ case 'revolve': {
94
+ const bodyId = op.bodyId
95
+ const shape = adapter.makeRevolve(op.profile, { axis: op.axis, at: op.at, angle: op.angle })
96
+ assertUsable(shape, 'revolve', '检查轮廓是否全部位于轴的一侧且共面')
97
+ bodies.set(bodyId, { shape, name: op.name ?? bodyId })
98
+ return { bodyId, name: bodies.get(bodyId).name, mesh: meshOf(shape, bodies.get(bodyId).name) }
99
+ }
100
+ case 'chamfer': {
101
+ const body = bodies.get(op.target)
102
+ if (body === undefined) throw new Error(`unknown body: ${op.target}`)
103
+ const shape = adapter.chamferAll(body.shape, op.distance)
104
+ assertUsable(shape, 'chamfer', '距离可能超出相邻面')
105
+ body.shape = shape
106
+ return { bodyId: op.target, name: body.name, mesh: meshOf(shape, body.name) }
107
+ }
108
+ case 'pattern': {
109
+ const body = bodies.get(op.target)
110
+ if (body === undefined) throw new Error(`unknown body: ${op.target}`)
111
+ const count = Math.max(1, Math.trunc(op.count))
112
+ const created = []
113
+ for (let i = 1; i < count; i++) {
114
+ const id = `${op.target}p${i}`
115
+ let shape
116
+ if (op.mode === 'circular') {
117
+ // Rotation about a principal axis through `at`: translate in, rotate,
118
+ // translate back (transform applies Euler rotations about the origin).
119
+ const axis = op.axis ?? [0, 0, 1]
120
+ const at = op.at ?? [0, 0, 0]
121
+ const step = ((op.angle ?? 360) / count) * i
122
+ const principal = Math.abs(axis[0]) > 0.9 ? [step, 0, 0]
123
+ : Math.abs(axis[1]) > 0.9 ? [0, step, 0]
124
+ : Math.abs(axis[2]) > 0.9 ? [0, 0, step]
125
+ : null
126
+ if (principal === null) throw new Error('circular pattern axes must be principal (+X/+Y/+Z)')
127
+ shape = adapter.transform(adapter.transform(
128
+ adapter.transform(body.shape, { translate: [-at[0], -at[1], -at[2]] }),
129
+ { rotate: principal },
130
+ ), { translate: [at[0], at[1], at[2]] })
131
+ } else {
132
+ shape = adapter.transform(body.shape, {
133
+ translate: [op.delta[0] * i, op.delta[1] * i, op.delta[2] * i],
134
+ })
135
+ }
136
+ const name = `${body.name}·${i}`
137
+ bodies.set(id, { shape, name })
138
+ created.push({ bodyId: id, name, mesh: meshOf(shape, name) })
139
+ }
140
+ return { bodyId: op.target, created }
141
+ }
142
+ case 'loft': {
143
+ const bodyId = op.bodyId
144
+ const shape = adapter.makeLoft(op.sections, { solid: op.solid, ruled: op.ruled })
145
+ assertUsable(shape, 'loft', '检查各截面是否闭合、点数是否合理')
146
+ bodies.set(bodyId, { shape, name: op.name ?? bodyId })
147
+ return { bodyId, name: bodies.get(bodyId).name, mesh: meshOf(shape, bodies.get(bodyId).name) }
148
+ }
149
+ case 'sweep': {
150
+ const bodyId = op.bodyId
151
+ const shape = adapter.makeSweep(op.profile, op.path)
152
+ assertUsable(shape, 'sweep', '路径上的尖角配合较大截面会自交——把拐角改为圆滑/倒角路径')
153
+ bodies.set(bodyId, { shape, name: op.name ?? bodyId })
154
+ return { bodyId, name: bodies.get(bodyId).name, mesh: meshOf(shape, bodies.get(bodyId).name) }
155
+ }
156
+ case 'boolean': {
157
+ const target = bodies.get(op.target)
158
+ if (target === undefined) throw new Error(`unknown body: ${op.target}`)
159
+ const tools = op.tools.map((id) => {
160
+ const tool = bodies.get(id)
161
+ if (tool === undefined) throw new Error(`unknown body: ${id}`)
162
+ return tool.shape
163
+ })
164
+ const shape = adapter.boolean(op.op, target.shape, tools)
165
+ target.shape = shape
166
+ // Consumed tool bodies are removed from the document (CAD convention).
167
+ const removed = []
168
+ for (const id of op.tools) {
169
+ if (id !== op.target && bodies.delete(id)) removed.push(id)
170
+ }
171
+ return { bodyId: op.target, name: target.name, removed, mesh: meshOf(shape, target.name) }
172
+ }
173
+ case 'fillet': {
174
+ const body = bodies.get(op.target)
175
+ if (body === undefined) throw new Error(`unknown body: ${op.target}`)
176
+ const { shape, edges } = adapter.filletAll(body.shape, op.radius ?? 1)
177
+ body.shape = shape
178
+ return { bodyId: op.target, name: body.name, edges, mesh: meshOf(shape, body.name) }
179
+ }
180
+ case 'transform': {
181
+ const body = bodies.get(op.target)
182
+ if (body === undefined) throw new Error(`unknown body: ${op.target}`)
183
+ const shape = adapter.transform(body.shape, {
184
+ translate: op.translate,
185
+ rotate: op.rotate,
186
+ mirror: op.mirror,
187
+ })
188
+ body.shape = shape
189
+ return { bodyId: op.target, name: body.name, mesh: meshOf(shape, body.name) }
190
+ }
191
+ case 'tessellate_all': {
192
+ const meshes = []
193
+ for (const [bodyId, body] of bodies) {
194
+ meshes.push({ bodyId, name: body.name, ...meshOf(body.shape, body.name) })
195
+ }
196
+ return { meshes }
197
+ }
198
+ case 'export': {
199
+ const body = bodies.get(op.target)
200
+ if (body === undefined) throw new Error(`unknown body: ${op.target}`)
201
+ const bytes = adapter.exportFile(body.shape, op.format)
202
+ return { bytes: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) }
203
+ }
204
+ case 'drawing': {
205
+ return drawingViews(op)
206
+ }
207
+ case 'assembly_insert': {
208
+ const body = bodies.get(op.bodyId)
209
+ if (body === undefined) throw new Error(`unknown body: ${op.bodyId}`)
210
+ if (instances.has(op.instanceId)) throw new Error(`instance already exists: ${op.instanceId}`)
211
+ instances.set(op.instanceId, {
212
+ instanceId: op.instanceId,
213
+ bodyId: op.bodyId,
214
+ name: op.name ?? body.name,
215
+ translate: triplet(op.translate),
216
+ rotate: triplet(op.rotate),
217
+ })
218
+ return { instanceId: op.instanceId, bodyId: op.bodyId, instances: instancesList() }
219
+ }
220
+ case 'assembly_transform': {
221
+ const instance = instances.get(op.instanceId)
222
+ if (instance === undefined) throw new Error(`unknown instance: ${op.instanceId}`)
223
+ if (op.translate !== undefined) instance.translate = triplet(op.translate)
224
+ if (op.rotate !== undefined) instance.rotate = triplet(op.rotate)
225
+ return { instanceId: op.instanceId, instances: instancesList() }
226
+ }
227
+ case 'assembly_remove': {
228
+ if (!instances.delete(op.instanceId)) throw new Error(`unknown instance: ${op.instanceId}`)
229
+ return { instanceId: op.instanceId, removed: op.instanceId, instances: instancesList() }
230
+ }
231
+ case 'assembly_list': {
232
+ return { instances: instancesList() }
233
+ }
234
+ case 'constraints': {
235
+ // Persist-only op: the constraint model lives in the document log and
236
+ // is solved on the main thread (Ansatz wasm); the worker stores nothing.
237
+ return { stored: true, entities: op.model.entities.length, constraints: op.model.constraints.length }
238
+ }
239
+ case 'export_assembly': {
240
+ if (instances.size === 0) throw new Error('the assembly is empty')
241
+ const builder = new occt.BRep_Builder()
242
+ const compound = new occt.TopoDS_Compound()
243
+ builder.MakeCompound(compound)
244
+ let added = 0
245
+ for (const instance of instances.values()) {
246
+ const body = bodies.get(instance.bodyId)
247
+ if (body === undefined) continue // stale instance of a consumed body
248
+ builder.Add(compound, adapter.transform(body.shape, {
249
+ translate: instance.translate,
250
+ rotate: instance.rotate,
251
+ }))
252
+ added++
253
+ }
254
+ if (added === 0) throw new Error('no instance references a live body')
255
+ const bytes = adapter.exportFile(compound, op.format)
256
+ return { bytes: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), instances: instancesList() }
257
+ }
258
+ case 'volume': {
259
+ const body = bodies.get(op.target)
260
+ if (body === undefined) throw new Error(`unknown body: ${op.target}`)
261
+ return { volume: adapter.volume(body.shape) }
262
+ }
263
+ case 'delete': {
264
+ if (!bodies.delete(op.target)) throw new Error(`unknown body: ${op.target}`)
265
+ return { deleted: op.target }
266
+ }
267
+ case 'reset': {
268
+ bodies.clear()
269
+ instances.clear()
270
+ nextBodyNumber = 1
271
+ return { cleared: true }
272
+ }
273
+ default:
274
+ throw new Error(`unknown op: ${op.kind}`)
275
+ }
276
+ }
277
+
278
+ /** Standard third/first-angle drawing views (GB first-angle arrangement). */
279
+ const DEFAULT_DRAWING_VIEWS = [
280
+ { name: 'front', dir: [0, -1, 0], xDir: [1, 0, 0] },
281
+ { name: 'top', dir: [0, 0, 1], xDir: [1, 0, 0] },
282
+ { name: 'left', dir: [-1, 0, 0], xDir: [0, 1, 0] },
283
+ { name: 'iso', dir: [1, -1, 1], xDir: [1, 1, 0] },
284
+ ]
285
+
286
+ /**
287
+ * The occt.ts kernel (true OCCT hidden-line removal) — the one and only
288
+ * drawing engine. Lazily loaded on the first drawing op and cached,
289
+ * including failures, so a missing dist costs one filesystem probe per
290
+ * worker, not per drawing.
291
+ */
292
+ let occtBridge = null
293
+ let occtBridgeTried = false
294
+ async function getOcctBridge() {
295
+ if (occtBridgeTried) return occtBridge
296
+ occtBridgeTried = true
297
+ try {
298
+ occtBridge = await createOcctBridge()
299
+ if (occtBridge !== null) {
300
+ console.log(`[dsh-cad] occt.ts kernel loaded (OCCT ${occtBridge.occtVersion}) from ${occtBridge.distDir}`)
301
+ }
302
+ } catch (error) {
303
+ console.warn(`[dsh-cad] occt.ts kernel failed to load: ${error instanceof Error ? error.message : error}`)
304
+ occtBridge = null
305
+ }
306
+ return occtBridge
307
+ }
308
+
309
+ async function drawingViews(op) {
310
+ const body = bodies.get(op.target)
311
+ if (body === undefined) throw new Error(`unknown body: ${op.target}`)
312
+ const views = Array.isArray(op.views) && op.views.length > 0 ? op.views : DEFAULT_DRAWING_VIEWS
313
+ const bridge = await getOcctBridge()
314
+ if (bridge === null) {
315
+ throw new Error('the occt.ts kernel is required for engineering drawings but was not found — install it (npm i occt.ts) or point DSH_OCCTJS_DIST at a dist directory')
316
+ }
317
+ const stepBytes = adapter.exportFile(body.shape, 'step')
318
+ const projected = await bridge.hiddenLineViews(stepBytes, views)
319
+ return { views: projected.views }
320
+ }
321
+
322
+ const initOpenCascade = loaderModule.default ?? loaderModule
323
+ initOpenCascade({ wasmBinary }).then((instance) => {
324
+ occt = instance
325
+ adapter = createAdapter(occt)
326
+ parentPort.on('message', async (message) => {
327
+ const transfers = []
328
+ try {
329
+ const result = await applyOp(message.op)
330
+ // Collect transferable mesh buffers.
331
+ const collect = (mesh) => {
332
+ if (mesh === undefined) return
333
+ transfers.push(mesh.positions.buffer, mesh.indices.buffer, mesh.normals.buffer)
334
+ }
335
+ if (result.mesh !== undefined) collect(result.mesh)
336
+ for (const mesh of result.meshes ?? []) collect(mesh)
337
+ if (result.bytes !== undefined) transfers.push(result.bytes)
338
+ parentPort.postMessage({ jobId: message.jobId, ok: true, result }, transfers)
339
+ } catch (error) {
340
+ parentPort.postMessage({
341
+ jobId: message.jobId,
342
+ ok: false,
343
+ error: error instanceof Error ? error.message : String(error),
344
+ })
345
+ }
346
+ })
347
+ parentPort.postMessage({ jobId: 0, ok: true, result: { ready: true } })
348
+ }).catch((error) => {
349
+ parentPort.postMessage({ jobId: 0, ok: false, error: `modeling kernel init failed: ${error.message}` })
350
+ })