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.
- package/README.md +235 -190
- package/README.zh-CN.md +212 -173
- package/lib/client.js +174 -174
- package/lib/client.js.map +3 -3
- package/lib/index.js +6 -2
- package/lib/modeling/ansatz-bridge.js +108 -0
- package/lib/modeling/bin-store.js +15 -1
- package/lib/modeling/constraints.js +85 -0
- package/lib/modeling/document.js +22 -5
- package/lib/modeling/modeling-worker.cjs +350 -235
- package/lib/modeling/occt-adapter.cjs +617 -271
- package/lib/modeling/occt-bridge.cjs +250 -0
- package/lib/modeling/registry.js +219 -0
- package/lib/routes.js +70 -0
- package/lib/tools/cad-constraint.js +335 -0
- package/lib/tools/cad-model.js +595 -58
- package/lib/types/modeling/ansatz-bridge.d.ts +24 -0
- package/lib/types/modeling/bin-store.d.ts +2 -0
- package/lib/types/modeling/client.d.ts +50 -1
- package/lib/types/modeling/constraints.d.ts +63 -0
- package/lib/types/modeling/document.d.ts +11 -1
- package/lib/types/modeling/registry.d.ts +61 -0
- package/lib/types/routes.d.ts +11 -0
- package/lib/types/tools/cad-constraint.d.ts +25 -0
- package/lib/types/tools/cad-model.d.ts +5 -2
- package/package.json +4 -3
- package/cordis.patch.yml +0 -3
- package/lib/modeling/hlr.cjs +0 -328
|
@@ -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 {
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
})
|
|
147
|
-
return {
|
|
148
|
-
}
|
|
149
|
-
case '
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
}
|
|
156
|
-
case '
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
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
|
+
})
|