dsh-cad 0.7.0 → 0.8.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 +11 -1
- package/README.zh-CN.md +9 -1
- package/lib/client.js +1 -1
- package/lib/client.js.map +2 -2
- package/lib/modeling/modeling-worker.cjs +54 -15
- package/lib/modeling/occt-bridge.cjs +157 -0
- package/lib/tools/cad-model.js +68 -0
- package/lib/types/modeling/client.d.ts +12 -0
- package/lib/types/tools/cad-model.d.ts +1 -1
- package/package.json +2 -2
|
@@ -37,7 +37,10 @@ function assertUsable(shape, what, hint) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** bodyId → { shape, name } — the live document.
|
|
40
|
+
/** bodyId → { shape, name, step? } — the live document. A body whose solid
|
|
41
|
+
* was produced by an occt.ts-only op (shell/draft) is HOSTED: `step` carries
|
|
42
|
+
* the STEP bytes as the source of truth and `shape` is null, because
|
|
43
|
+
* opencascade.js in this build cannot read geometry back. */
|
|
41
44
|
const bodies = new Map()
|
|
42
45
|
let nextBodyNumber = 1
|
|
43
46
|
|
|
@@ -98,8 +101,7 @@ async function applyOp(op) {
|
|
|
98
101
|
return { bodyId, name: bodies.get(bodyId).name, mesh: meshOf(shape, bodies.get(bodyId).name) }
|
|
99
102
|
}
|
|
100
103
|
case 'chamfer': {
|
|
101
|
-
const body =
|
|
102
|
-
if (body === undefined) throw new Error(`unknown body: ${op.target}`)
|
|
104
|
+
const body = requireShape(op.target, 'chamfer')
|
|
103
105
|
const shape = adapter.chamferAll(body.shape, op.distance)
|
|
104
106
|
assertUsable(shape, 'chamfer', '距离可能超出相邻面')
|
|
105
107
|
body.shape = shape
|
|
@@ -153,14 +155,31 @@ async function applyOp(op) {
|
|
|
153
155
|
bodies.set(bodyId, { shape, name: op.name ?? bodyId })
|
|
154
156
|
return { bodyId, name: bodies.get(bodyId).name, mesh: meshOf(shape, bodies.get(bodyId).name) }
|
|
155
157
|
}
|
|
158
|
+
case 'shell': {
|
|
159
|
+
const body = bodies.get(op.target)
|
|
160
|
+
if (body === undefined) throw new Error(`unknown body: ${op.target}`)
|
|
161
|
+
const stepIn = body.step ?? adapter.exportFile(body.shape, 'step')
|
|
162
|
+
const bridge = await getOcctBridge()
|
|
163
|
+
if (bridge === null) throw new Error('shell requires the occt.ts kernel (>= 0.3.0) — reinstall dsh-cad so the dependency pulls it')
|
|
164
|
+
const out = bridge.hostedSolidOp('shell', stepIn, { thickness: op.thickness, openNormals: op.openNormals, faces: op.faces, name: body.name })
|
|
165
|
+
body.step = out.step
|
|
166
|
+
body.shape = null
|
|
167
|
+
return { bodyId: op.target, name: body.name, volume: out.volume, mesh: out.mesh }
|
|
168
|
+
}
|
|
169
|
+
case 'draft': {
|
|
170
|
+
const body = bodies.get(op.target)
|
|
171
|
+
if (body === undefined) throw new Error(`unknown body: ${op.target}`)
|
|
172
|
+
const stepIn = body.step ?? adapter.exportFile(body.shape, 'step')
|
|
173
|
+
const bridge = await getOcctBridge()
|
|
174
|
+
if (bridge === null) throw new Error('draft requires the occt.ts kernel (>= 0.3.0) — reinstall dsh-cad so the dependency pulls it')
|
|
175
|
+
const out = bridge.hostedSolidOp('draft', stepIn, { angle: op.angle, direction: op.direction, faces: op.faces, name: body.name })
|
|
176
|
+
body.step = out.step
|
|
177
|
+
body.shape = null
|
|
178
|
+
return { bodyId: op.target, name: body.name, volume: out.volume, mesh: out.mesh }
|
|
179
|
+
}
|
|
156
180
|
case 'boolean': {
|
|
157
|
-
const target =
|
|
158
|
-
|
|
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
|
-
})
|
|
181
|
+
const target = requireShape(op.target, 'boolean')
|
|
182
|
+
const tools = op.tools.map((id) => requireShape(id, 'boolean').shape)
|
|
164
183
|
const shape = adapter.boolean(op.op, target.shape, tools)
|
|
165
184
|
target.shape = shape
|
|
166
185
|
// Consumed tool bodies are removed from the document (CAD convention).
|
|
@@ -171,15 +190,13 @@ async function applyOp(op) {
|
|
|
171
190
|
return { bodyId: op.target, name: target.name, removed, mesh: meshOf(shape, target.name) }
|
|
172
191
|
}
|
|
173
192
|
case 'fillet': {
|
|
174
|
-
const body =
|
|
175
|
-
if (body === undefined) throw new Error(`unknown body: ${op.target}`)
|
|
193
|
+
const body = requireShape(op.target, 'fillet')
|
|
176
194
|
const { shape, edges } = adapter.filletAll(body.shape, op.radius ?? 1)
|
|
177
195
|
body.shape = shape
|
|
178
196
|
return { bodyId: op.target, name: body.name, edges, mesh: meshOf(shape, body.name) }
|
|
179
197
|
}
|
|
180
198
|
case 'transform': {
|
|
181
|
-
const body =
|
|
182
|
-
if (body === undefined) throw new Error(`unknown body: ${op.target}`)
|
|
199
|
+
const body = requireShape(op.target, 'transform')
|
|
183
200
|
const shape = adapter.transform(body.shape, {
|
|
184
201
|
translate: op.translate,
|
|
185
202
|
rotate: op.rotate,
|
|
@@ -191,6 +208,7 @@ async function applyOp(op) {
|
|
|
191
208
|
case 'tessellate_all': {
|
|
192
209
|
const meshes = []
|
|
193
210
|
for (const [bodyId, body] of bodies) {
|
|
211
|
+
if (body.shape === null || body.shape === undefined) continue // hosted bodies re-mesh via their ops
|
|
194
212
|
meshes.push({ bodyId, name: body.name, ...meshOf(body.shape, body.name) })
|
|
195
213
|
}
|
|
196
214
|
return { meshes }
|
|
@@ -198,6 +216,11 @@ async function applyOp(op) {
|
|
|
198
216
|
case 'export': {
|
|
199
217
|
const body = bodies.get(op.target)
|
|
200
218
|
if (body === undefined) throw new Error(`unknown body: ${op.target}`)
|
|
219
|
+
if (body.step !== undefined) {
|
|
220
|
+
// Hosted body: the STEP bytes ARE the parametric exchange form.
|
|
221
|
+
if (op.format !== 'step') throw new Error('stl export of a hosted body requires tessellation — export .step, or re-export before shelling')
|
|
222
|
+
return { bytes: body.step.buffer.slice(body.step.byteOffset, body.step.byteOffset + body.step.byteLength) }
|
|
223
|
+
}
|
|
201
224
|
const bytes = adapter.exportFile(body.shape, op.format)
|
|
202
225
|
return { bytes: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) }
|
|
203
226
|
}
|
|
@@ -258,7 +281,13 @@ async function applyOp(op) {
|
|
|
258
281
|
case 'volume': {
|
|
259
282
|
const body = bodies.get(op.target)
|
|
260
283
|
if (body === undefined) throw new Error(`unknown body: ${op.target}`)
|
|
261
|
-
|
|
284
|
+
if (body.step !== undefined) {
|
|
285
|
+
// Hosted body: ask the occt.ts kernel directly.
|
|
286
|
+
const bridge = await getOcctBridge()
|
|
287
|
+
if (bridge === null) throw new Error('volume on a hosted body requires the occt.ts kernel')
|
|
288
|
+
return { volume: bridge.volumeOf(body.step) }
|
|
289
|
+
}
|
|
290
|
+
return { volume: Math.abs(adapter.volume(body.shape)) }
|
|
262
291
|
}
|
|
263
292
|
case 'delete': {
|
|
264
293
|
if (!bodies.delete(op.target)) throw new Error(`unknown body: ${op.target}`)
|
|
@@ -306,6 +335,16 @@ async function getOcctBridge() {
|
|
|
306
335
|
return occtBridge
|
|
307
336
|
}
|
|
308
337
|
|
|
338
|
+
/** Reject ops that need an opencascade.js BRep on a hosted body. */
|
|
339
|
+
function requireShape(bodyId, what) {
|
|
340
|
+
const body = bodies.get(bodyId)
|
|
341
|
+
if (body === undefined) throw new Error(`unknown body: ${bodyId}`)
|
|
342
|
+
if (body.shape === undefined || body.shape === null) {
|
|
343
|
+
throw new Error(`${bodyId} is hosted on the occt.ts kernel (created by shell/draft); ${what} needs the opencascade.js BRep and cannot read hosted geometry back — model the feature before shelling, or reconstruct the body`)
|
|
344
|
+
}
|
|
345
|
+
return body
|
|
346
|
+
}
|
|
347
|
+
|
|
309
348
|
async function drawingViews(op) {
|
|
310
349
|
const body = bodies.get(op.target)
|
|
311
350
|
if (body === undefined) throw new Error(`unknown body: ${op.target}`)
|
|
@@ -143,6 +143,149 @@ async function hiddenLineViews(mod, stepBytes, views) {
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
|
|
147
|
+
// ── occt.ts-hosted solid operations (shell / draft / boolean / transform) ────
|
|
148
|
+
//
|
|
149
|
+
// opencascade.js cannot read geometry BACK (STEPControl_Reader and BRepTools
|
|
150
|
+
// readers are opaque no-method shells in that build), so a solid produced by
|
|
151
|
+
// an occt.ts-only op cannot return to the opencascade.js modeling session.
|
|
152
|
+
// Instead the result stays hosted as STEP bytes on the occt.ts side: every
|
|
153
|
+
// hosted op re-reads the STEP, operates, and writes it back, while meshes for
|
|
154
|
+
// display/volume come from the occt.ts tessellator. opencascade.js-exclusive
|
|
155
|
+
// ops (fillet/chamfer/loft/sweep/revolve) refuse hosted bodies with a clear
|
|
156
|
+
// error instead of silently operating on stale geometry.
|
|
157
|
+
|
|
158
|
+
/** Read STEP bytes into an occt.ts shape (caller deletes). */
|
|
159
|
+
function readShape(mod, stepBytes) {
|
|
160
|
+
const ptr = mod._malloc(stepBytes.length)
|
|
161
|
+
try {
|
|
162
|
+
mod.HEAPU8.set(stepBytes, ptr)
|
|
163
|
+
const shape = mod.readStep(ptr, stepBytes.length, 'mm')
|
|
164
|
+
if (shape.isNull()) throw new Error(`occt.ts readStep: ${mod.lastError()}`)
|
|
165
|
+
return shape
|
|
166
|
+
} finally {
|
|
167
|
+
mod._free(ptr)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Write an occt.ts shape to STEP bytes. */
|
|
172
|
+
function toStepBytes(mod, shape) {
|
|
173
|
+
const text = mod.writeStep(shape, 'mm')
|
|
174
|
+
return Buffer.from(text, 'utf8')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Tessellate an occt.ts shape into the worker mesh format. */
|
|
178
|
+
function tessellateMesh(mod, shape, name, linearDeflection = 0.3) {
|
|
179
|
+
const data = mod.tessellate(shape, linearDeflection, 20, false)
|
|
180
|
+
try {
|
|
181
|
+
const positions = new Float32Array(mod.HEAPU8.buffer, data.positionsPtr(), data.positionCount() * 3).slice()
|
|
182
|
+
const indices = new Uint32Array(mod.HEAPU8.buffer, data.indicesPtr(), data.indexCount()).slice()
|
|
183
|
+
const triangles = indices.length / 3
|
|
184
|
+
// Flat per-triangle normals (the mechanical-CAD look the viewer expects).
|
|
185
|
+
const normals = new Float32Array(positions.length)
|
|
186
|
+
for (let i = 0; i + 2 < indices.length; i += 3) {
|
|
187
|
+
const a = indices[i] * 3, b = indices[i + 1] * 3, c = indices[i + 2] * 3
|
|
188
|
+
const ux = positions[c] - positions[a], uy = positions[c + 1] - positions[a + 1], uz = positions[c + 2] - positions[a + 2]
|
|
189
|
+
const vx = positions[b] - positions[a], vy = positions[b + 1] - positions[a + 1], vz = positions[b + 2] - positions[a + 2]
|
|
190
|
+
let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx
|
|
191
|
+
const len = Math.hypot(nx, ny, nz) || 1
|
|
192
|
+
nx /= len; ny /= len; nz /= len
|
|
193
|
+
for (const corner of [a, b, c]) {
|
|
194
|
+
normals[corner] = nx; normals[corner + 1] = ny; normals[corner + 2] = nz
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { name, positions, normals, indices, vertexCount: positions.length / 3, triangleCount: triangles }
|
|
198
|
+
} finally {
|
|
199
|
+
data.delete()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Faces (1-based indices) whose outward plane-normal matches a target list. */
|
|
204
|
+
function matchFaces(mod, shape, openNormals) {
|
|
205
|
+
if (!Array.isArray(openNormals) || openNormals.length === 0) return []
|
|
206
|
+
const desc = JSON.parse(mod.describe(shape))
|
|
207
|
+
const faces = Array.isArray(desc.faces) ? desc.faces : []
|
|
208
|
+
const picked = []
|
|
209
|
+
for (const target of openNormals) {
|
|
210
|
+
const tlen = Math.hypot(target[0], target[1], target[2])
|
|
211
|
+
if (tlen < 1e-12) continue
|
|
212
|
+
const tn = [target[0] / tlen, target[1] / tlen, target[2] / tlen]
|
|
213
|
+
let best = null
|
|
214
|
+
let bestScore = 0.9
|
|
215
|
+
for (const face of faces) {
|
|
216
|
+
if (face.surface !== 'plane' || !Array.isArray(face.normal)) continue
|
|
217
|
+
const dot = face.normal[0] * tn[0] + face.normal[1] * tn[1] + face.normal[2] * tn[2]
|
|
218
|
+
// |dot| ≈ 1 means parallel; the OUTER face along ±target has the larger
|
|
219
|
+
// origin·target projection (a box carries both ±Z faces).
|
|
220
|
+
if (Math.abs(dot) < 0.9) continue
|
|
221
|
+
const outward = Math.sign(dot) >= 0 ? 1 : -1
|
|
222
|
+
const proj = (face.origin[0] * tn[0] + face.origin[1] * tn[1] + face.origin[2] * tn[2]) * outward
|
|
223
|
+
const score = Math.abs(dot) + proj * 1e-3
|
|
224
|
+
if (score > bestScore) { bestScore = score; best = face.index }
|
|
225
|
+
}
|
|
226
|
+
if (best !== null) picked.push(best)
|
|
227
|
+
}
|
|
228
|
+
return picked
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** int32 face-index array into wasm memory; returns {ptr, count} or null. */
|
|
232
|
+
function faceListPtr(mod, faces) {
|
|
233
|
+
if (!Array.isArray(faces) || faces.length === 0) return { ptr: 0, count: 0 }
|
|
234
|
+
const ptr = mod._malloc(faces.length * 4)
|
|
235
|
+
for (let i = 0; i < faces.length; i++) {
|
|
236
|
+
mod.HEAPU32[ptr / 4 + i] = faces[i]
|
|
237
|
+
}
|
|
238
|
+
return { ptr, count: faces.length }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Run one occt.ts-hosted solid operation on STEP bytes:
|
|
243
|
+
* shell(stepBytes, thickness, openNormals) → { step, mesh, volume, faces }
|
|
244
|
+
* draft(stepBytes, angleDegrees, direction, faces?) → { step, mesh, volume }
|
|
245
|
+
* The STEP bytes of the RESULT are the hosted body's new source of truth.
|
|
246
|
+
*/
|
|
247
|
+
function hostedSolidOp(mod, kind, stepBytes, params) {
|
|
248
|
+
const shape = readShape(mod, stepBytes)
|
|
249
|
+
try {
|
|
250
|
+
let result
|
|
251
|
+
if (kind === 'shell') {
|
|
252
|
+
const faces = Array.isArray(params.faces) && params.faces.length > 0
|
|
253
|
+
? params.faces
|
|
254
|
+
: matchFaces(mod, shape, params.openNormals)
|
|
255
|
+
const list = faceListPtr(mod, faces)
|
|
256
|
+
try {
|
|
257
|
+
result = mod.shell(shape, Math.abs(params.thickness), list.ptr, list.count)
|
|
258
|
+
} finally {
|
|
259
|
+
if (list.ptr !== 0) mod._free(list.ptr)
|
|
260
|
+
}
|
|
261
|
+
if (result.isNull()) throw new Error(`shell failed: ${mod.lastError()}`)
|
|
262
|
+
} else if (kind === 'draft') {
|
|
263
|
+
const faces = Array.isArray(params.faces) && params.faces.length > 0 ? params.faces : []
|
|
264
|
+
const list = faceListPtr(mod, faces)
|
|
265
|
+
const d = params.direction ?? [0, 0, 1]
|
|
266
|
+
try {
|
|
267
|
+
result = mod.draft(shape, (params.angle * Math.PI) / 180, d[0], d[1], d[2], 0, 0, 1, list.ptr, list.count, faces.length === 0)
|
|
268
|
+
} finally {
|
|
269
|
+
if (list.ptr !== 0) mod._free(list.ptr)
|
|
270
|
+
}
|
|
271
|
+
if (result.isNull()) throw new Error(`draft failed: ${mod.lastError()}`)
|
|
272
|
+
} else {
|
|
273
|
+
throw new Error(`unknown hosted op: ${kind}`)
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
return {
|
|
277
|
+
step: toStepBytes(mod, result),
|
|
278
|
+
mesh: tessellateMesh(mod, result, params.name ?? 'hosted'),
|
|
279
|
+
volume: Math.abs(result.volume()),
|
|
280
|
+
}
|
|
281
|
+
} finally {
|
|
282
|
+
result.delete()
|
|
283
|
+
}
|
|
284
|
+
} finally {
|
|
285
|
+
shape.delete()
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
146
289
|
/**
|
|
147
290
|
* Create the bridge, or null when the dist is absent/unloadable (the caller
|
|
148
291
|
* falls back to the mesh HLR). The heavy wasm load is lazy and cached.
|
|
@@ -154,11 +297,25 @@ async function createOcctBridge(options = {}) {
|
|
|
154
297
|
if (typeof mod.hiddenLines !== 'function' || typeof mod.readStep !== 'function') {
|
|
155
298
|
throw new Error(`occt.ts dist at ${distDir} lacks hiddenLines/readStep`)
|
|
156
299
|
}
|
|
300
|
+
for (const required of ['shell', 'draft', 'describe', 'writeStep', 'tessellate']) {
|
|
301
|
+
if (typeof mod[required] !== 'function') {
|
|
302
|
+
throw new Error(`occt.ts dist at ${distDir} lacks ${required} (need occt.ts >= 0.3.0)`)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
157
305
|
return {
|
|
158
306
|
kernel: 'occt.ts',
|
|
159
307
|
distDir,
|
|
160
308
|
occtVersion: mod.occtVersion(),
|
|
161
309
|
hiddenLineViews: (stepBytes, views) => hiddenLineViews(mod, stepBytes, views),
|
|
310
|
+
hostedSolidOp: (kind, stepBytes, params) => hostedSolidOp(mod, kind, stepBytes, params),
|
|
311
|
+
volumeOf: (stepBytes) => {
|
|
312
|
+
const shape = readShape(mod, stepBytes)
|
|
313
|
+
try {
|
|
314
|
+
return Math.abs(shape.volume())
|
|
315
|
+
} finally {
|
|
316
|
+
shape.delete()
|
|
317
|
+
}
|
|
318
|
+
},
|
|
162
319
|
}
|
|
163
320
|
}
|
|
164
321
|
|
package/lib/tools/cad-model.js
CHANGED
|
@@ -597,6 +597,70 @@ export function createModelTools(deps) {
|
|
|
597
597
|
presentCall: (args) => ({ card: 'generic', title: `CAD chamfer ${String(args.target)}`, kind: 'other' }),
|
|
598
598
|
presentResult: () => ({ card: 'generic', title: 'CAD chamfer' }),
|
|
599
599
|
});
|
|
600
|
+
const cadShell = defineTool({
|
|
601
|
+
name: 'cad_shell',
|
|
602
|
+
description: 'Shell (抽壳): hollow a solid into a wall of `thickness` mm (the wall grows inward, the outer skin is preserved). `open` lists outward NORMALS [x,y,z] whose faces become openings (e.g. [[0,0,1]] opens the top of a box; empty = sealed hollow). ' +
|
|
603
|
+
'Runs on the occt.ts kernel (>= 0.3.0): the result becomes a HOSTED body — volume/export(.step)/display keep working, but opencascade.js-only edits (fillet/chamfer/loft/sweep/revolve/boolean) on it are refused, so do those before shelling.',
|
|
604
|
+
parameters: {
|
|
605
|
+
target: bodyTarget,
|
|
606
|
+
thickness: { type: 'number', required: true, description: 'Wall thickness (mm, positive).' },
|
|
607
|
+
open: { type: 'array', items: { type: 'array', items: { type: 'number' } }, description: 'Outward normals of the faces to open, e.g. [[0,0,1]].' },
|
|
608
|
+
},
|
|
609
|
+
output: {
|
|
610
|
+
schema: {
|
|
611
|
+
type: 'object',
|
|
612
|
+
additionalProperties: false,
|
|
613
|
+
properties: {
|
|
614
|
+
bodyId: { type: 'string', required: true },
|
|
615
|
+
...requiredCounts,
|
|
616
|
+
...commonOptional,
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
620
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
621
|
+
},
|
|
622
|
+
isConcurrencySafe: () => false,
|
|
623
|
+
async execute(args, exec) {
|
|
624
|
+
await resolveDoc(exec);
|
|
625
|
+
const op = { kind: 'shell', target: args.target, thickness: args.thickness, ...(Array.isArray(args.open) ? { openNormals: args.open } : {}) };
|
|
626
|
+
const result = await runModelOp(op);
|
|
627
|
+
return syncScene(op, result);
|
|
628
|
+
},
|
|
629
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD shell ${String(args.target)}`, kind: 'other' }),
|
|
630
|
+
presentResult: () => ({ card: 'generic', title: 'CAD shell' }),
|
|
631
|
+
});
|
|
632
|
+
const cadDraft = defineTool({
|
|
633
|
+
name: 'cad_draft',
|
|
634
|
+
description: 'Draft (拔模): tilt the walls of a solid by `angle` degrees for mold release — faces pivot about a neutral plane, dimensions there unchanged. `direction` is the pull direction (default +Z); by default the faces parallel to it (the walls) are drafted. ' +
|
|
635
|
+
'Runs on the occt.ts kernel (>= 0.3.0): the result becomes a HOSTED body (same caveats as cad_shell).',
|
|
636
|
+
parameters: {
|
|
637
|
+
target: bodyTarget,
|
|
638
|
+
angle: { type: 'number', required: true, description: 'Draft angle in degrees (positive opens the walls toward the pull direction).' },
|
|
639
|
+
direction: { type: 'array', items: { type: 'number' }, description: 'Pull direction [x,y,z] (default +Z).' },
|
|
640
|
+
},
|
|
641
|
+
output: {
|
|
642
|
+
schema: {
|
|
643
|
+
type: 'object',
|
|
644
|
+
additionalProperties: false,
|
|
645
|
+
properties: {
|
|
646
|
+
bodyId: { type: 'string', required: true },
|
|
647
|
+
...requiredCounts,
|
|
648
|
+
...commonOptional,
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
652
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
653
|
+
},
|
|
654
|
+
isConcurrencySafe: () => false,
|
|
655
|
+
async execute(args, exec) {
|
|
656
|
+
await resolveDoc(exec);
|
|
657
|
+
const op = { kind: 'draft', target: args.target, angle: args.angle, ...(Array.isArray(args.direction) ? { direction: args.direction } : {}) };
|
|
658
|
+
const result = await runModelOp(op);
|
|
659
|
+
return syncScene(op, result);
|
|
660
|
+
},
|
|
661
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD draft ${String(args.target)} ${String(args.angle ?? '')}°`, kind: 'other' }),
|
|
662
|
+
presentResult: () => ({ card: 'generic', title: 'CAD draft' }),
|
|
663
|
+
});
|
|
600
664
|
const cadPattern = defineTool({
|
|
601
665
|
name: 'cad_pattern',
|
|
602
666
|
description: 'Pattern (阵列): replicate a body into `count` total placements. Linear: `delta` [dx,dy,dz] spacing per copy. Circular: rotate copies around a principal axis (+X/+Y/+Z) through `at` over `angle` degrees (default 360). Creates new bodies (target·1 … target·(count−1)); fuse afterwards for a single body.',
|
|
@@ -1303,6 +1367,8 @@ export function createModelTools(deps) {
|
|
|
1303
1367
|
cadExtrude,
|
|
1304
1368
|
cadRevolve,
|
|
1305
1369
|
cadChamfer,
|
|
1370
|
+
cadShell,
|
|
1371
|
+
cadDraft,
|
|
1306
1372
|
cadPattern,
|
|
1307
1373
|
cadLoft,
|
|
1308
1374
|
cadSweep,
|
|
@@ -1329,6 +1395,8 @@ export const MODEL_TOOL_NAMES = [
|
|
|
1329
1395
|
'cad_extrude_profile',
|
|
1330
1396
|
'cad_revolve',
|
|
1331
1397
|
'cad_chamfer',
|
|
1398
|
+
'cad_shell',
|
|
1399
|
+
'cad_draft',
|
|
1332
1400
|
'cad_pattern',
|
|
1333
1401
|
'cad_loft',
|
|
1334
1402
|
'cad_sweep',
|
|
@@ -81,6 +81,18 @@ export type ModelOp = {
|
|
|
81
81
|
kind: 'chamfer';
|
|
82
82
|
target: string;
|
|
83
83
|
distance: number;
|
|
84
|
+
} | {
|
|
85
|
+
kind: 'shell';
|
|
86
|
+
target: string;
|
|
87
|
+
thickness: number;
|
|
88
|
+
openNormals?: Array<[number, number, number]>;
|
|
89
|
+
faces?: number[];
|
|
90
|
+
} | {
|
|
91
|
+
kind: 'draft';
|
|
92
|
+
target: string;
|
|
93
|
+
angle: number;
|
|
94
|
+
direction?: [number, number, number];
|
|
95
|
+
faces?: number[];
|
|
84
96
|
} | {
|
|
85
97
|
kind: 'pattern';
|
|
86
98
|
target: string;
|
|
@@ -13,4 +13,4 @@ export interface ModelToolDeps {
|
|
|
13
13
|
}
|
|
14
14
|
/** Build the whole tool family over the document registry + worker + stores. */
|
|
15
15
|
export declare function createModelTools(deps: ModelToolDeps): ToolDefinition[];
|
|
16
|
-
export declare const MODEL_TOOL_NAMES: readonly ["cad_create_prim", "cad_extrude_profile", "cad_revolve", "cad_chamfer", "cad_pattern", "cad_loft", "cad_sweep", "cad_boolean", "cad_fillet", "cad_transform", "cad_export", "cad_delete", "cad_volume", "cad_drawing", "cad_assembly_insert", "cad_assembly_move", "cad_assembly_remove", "cad_docs", "cad_doc_new", "cad_doc_open", "cad_doc_rename", "cad_doc_delete", "cad_constraint", "cad_solve", "cad_motion"];
|
|
16
|
+
export declare const MODEL_TOOL_NAMES: readonly ["cad_create_prim", "cad_extrude_profile", "cad_revolve", "cad_chamfer", "cad_shell", "cad_draft", "cad_pattern", "cad_loft", "cad_sweep", "cad_boolean", "cad_fillet", "cad_transform", "cad_export", "cad_delete", "cad_volume", "cad_drawing", "cad_assembly_insert", "cad_assembly_move", "cad_assembly_remove", "cad_docs", "cad_doc_new", "cad_doc_open", "cad_doc_rename", "cad_doc_delete", "cad_constraint", "cad_solve", "cad_motion"];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cad",
|
|
3
3
|
"description": "CAD visualization plugin for DeepSeek Harness: cad_view / cad_info tools with an embedded 3D (STL/OBJ/STEP/IGES) and 2D (DXF/SVG) viewer card in the Web UI",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "LAU-MARS",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"ansatz-wasm": "^0.2.0",
|
|
71
71
|
"dxf-parser": "^1.1.2",
|
|
72
72
|
"occt-import-js": "^0.0.23",
|
|
73
|
-
"occt.ts": "^0.
|
|
73
|
+
"occt.ts": "^0.3.0",
|
|
74
74
|
"opencascade.js": "^1.1.1",
|
|
75
75
|
"three": "^0.185.1"
|
|
76
76
|
},
|