dsh-cad 0.2.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 +245 -190
- package/README.zh-CN.md +220 -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 +389 -235
- package/lib/modeling/occt-adapter.cjs +617 -271
- package/lib/modeling/occt-bridge.cjs +407 -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 +663 -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 +62 -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
package/lib/tools/cad-model.js
CHANGED
|
@@ -6,6 +6,7 @@ import { composeAssemblyMeshes } from '../modeling/assembly.js';
|
|
|
6
6
|
import { buildDrawingSheet, drawingToDxf, drawingToSvg } from '../modeling/drawing.js';
|
|
7
7
|
import { resolveWorkspacePath } from './util.js';
|
|
8
8
|
import { toDcPrtDocument } from '../feature_script/dc_prt.js';
|
|
9
|
+
import { createConstraintTools } from './cad-constraint.js';
|
|
9
10
|
/** Mirror a worker mesh with its raw typed arrays (binary-transport ready). */
|
|
10
11
|
function mirrorMesh(mesh, bodyId) {
|
|
11
12
|
return {
|
|
@@ -15,17 +16,22 @@ function mirrorMesh(mesh, bodyId) {
|
|
|
15
16
|
indices: mesh.indices,
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
|
-
/** Build the whole tool family over
|
|
19
|
+
/** Build the whole tool family over the document registry + worker + stores. */
|
|
19
20
|
export function createModelTools(deps) {
|
|
20
|
-
|
|
21
|
+
// The active document and its derived caches are swapped as a unit whenever
|
|
22
|
+
// a session's binding points elsewhere (see resolveDoc). meshCache /
|
|
23
|
+
// drawingSheets are cleared and rebuilt by replay.
|
|
24
|
+
let document = new ModelDocument(deps.workspaceRoot);
|
|
21
25
|
/** bodyId → raw worker mesh mirror (binary scene source, zero encoding). */
|
|
22
26
|
const meshCache = new Map();
|
|
23
27
|
/** Reset epoch after the last sync — out-of-band replays (cad_view on a
|
|
24
28
|
* .dcprt) bump the epoch and force a re-replay before the next op. */
|
|
25
29
|
let syncedEpoch = -1;
|
|
26
30
|
/** drawingId → rebuilt sheet (backing cad_export .svg/.dxf). */
|
|
27
|
-
|
|
31
|
+
let drawingSheets = new Map();
|
|
28
32
|
let lastDrawingId = null;
|
|
33
|
+
/** Live constraint-model state (rebuilt from the op log on replay). */
|
|
34
|
+
const constraintState = { model: null, lastSuggestions: [] };
|
|
29
35
|
/**
|
|
30
36
|
* Standard drawing views (GB first-angle): 主视图 front, 俯视图 top,
|
|
31
37
|
* 左视图 left, 轴测 iso. Same constants as the worker's fallback.
|
|
@@ -46,34 +52,44 @@ export function createModelTools(deps) {
|
|
|
46
52
|
const salted = (hex.slice(0, 12) + salt + hex.slice(13)).slice(0, 32);
|
|
47
53
|
return `${salted.slice(0, 8)}-${salted.slice(8, 12)}-${salted.slice(12, 16)}-${salted.slice(16, 20)}-${salted.slice(20, 32)}`;
|
|
48
54
|
};
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
format: 'drawing',
|
|
67
|
-
entities: sheet.entities,
|
|
68
|
-
bounds: sheet.bounds,
|
|
69
|
-
layers: sheet.layers,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
55
|
+
/**
|
|
56
|
+
* Replay a document's ops into the worker and rebuild the derived caches
|
|
57
|
+
* (mesh mirror, drawing sheets, constraint model). The worker is always
|
|
58
|
+
* reset first — also for empty documents, so a previous document's shapes
|
|
59
|
+
* never leak in. Absorbs the old restoreOnce body.
|
|
60
|
+
*/
|
|
61
|
+
async function replayActiveDoc() {
|
|
62
|
+
await runModelOp({ kind: 'reset' });
|
|
63
|
+
// Derived state belongs to the outgoing document: clear before replay so
|
|
64
|
+
// a document without constraints/drawings never inherits the previous one's.
|
|
65
|
+
constraintState.model = null;
|
|
66
|
+
for (const op of document.doc.ops) {
|
|
67
|
+
try {
|
|
68
|
+
const result = await runModelOp(op);
|
|
69
|
+
// A replayed constraint model returns to live state.
|
|
70
|
+
if (op.kind === 'constraints') {
|
|
71
|
+
constraintState.model = op.model;
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
// A replayed drawing re-generates its sheet and re-publishes the
|
|
74
|
+
// scene so the stable viewId keeps serving after restarts.
|
|
75
|
+
if (op.kind === 'drawing' && op.sceneViewId !== undefined && result.views !== undefined) {
|
|
76
|
+
const sheet = buildDrawingSheet({ partName: op.name ?? op.target, views: result.views, paper: op.paper });
|
|
77
|
+
drawingSheets.set(op.sceneViewId, { sheet, partName: op.name ?? op.target });
|
|
78
|
+
lastDrawingId = op.sceneViewId;
|
|
79
|
+
await deps.sceneStore.putAt(op.sceneViewId, {
|
|
80
|
+
kind: '2d',
|
|
81
|
+
format: 'drawing',
|
|
82
|
+
entities: sheet.entities,
|
|
83
|
+
bounds: sheet.bounds,
|
|
84
|
+
layers: sheet.layers,
|
|
85
|
+
});
|
|
75
86
|
}
|
|
76
87
|
}
|
|
88
|
+
catch {
|
|
89
|
+
// A single stale op must not block recovery; later ops may be independent.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (document.doc.ops.length > 0) {
|
|
77
93
|
const all = await runModelOp({ kind: 'tessellate_all' });
|
|
78
94
|
meshCache.clear();
|
|
79
95
|
for (const mesh of all.meshes ?? []) {
|
|
@@ -82,6 +98,62 @@ export function createModelTools(deps) {
|
|
|
82
98
|
}
|
|
83
99
|
syncedEpoch = workerResetEpoch();
|
|
84
100
|
}
|
|
101
|
+
/** Swap the active document: restore from disk, reset the worker, replay. */
|
|
102
|
+
async function activateDocument(next) {
|
|
103
|
+
await next.restore();
|
|
104
|
+
document = next;
|
|
105
|
+
meshCache.clear();
|
|
106
|
+
drawingSheets = new Map();
|
|
107
|
+
lastDrawingId = null;
|
|
108
|
+
await replayActiveDoc();
|
|
109
|
+
}
|
|
110
|
+
/** Session key from the tool run context (`_default` outside a session). */
|
|
111
|
+
const sessionKeyOf = (exec) => {
|
|
112
|
+
const agent = exec?.agent;
|
|
113
|
+
return typeof agent?.id === 'string' && agent.id !== '' ? agent.id : '_default';
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Per-op entry point (replaces restoreOnce): resolve the calling session's
|
|
117
|
+
* active document, switching documents (reset + replay) when its binding
|
|
118
|
+
* points elsewhere, then re-sync when the worker was reset out of band.
|
|
119
|
+
*/
|
|
120
|
+
async function resolveDoc(exec) {
|
|
121
|
+
const sessionId = sessionKeyOf(exec);
|
|
122
|
+
const bound = await deps.registry.bindingOf(sessionId);
|
|
123
|
+
if (bound === null) {
|
|
124
|
+
// Upgrade continuity: the first unbound session inherits the migrated
|
|
125
|
+
// legacy document (if any) instead of starting from scratch.
|
|
126
|
+
const legacy = await deps.registry.claimLegacyFor(sessionId);
|
|
127
|
+
if (legacy !== null) {
|
|
128
|
+
const doc = await deps.registry.open(legacy);
|
|
129
|
+
if (doc !== null) {
|
|
130
|
+
await activateDocument(doc);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Unbound session (new conversation): start on a fresh empty document.
|
|
135
|
+
const fresh = await deps.registry.create();
|
|
136
|
+
await deps.registry.bind(sessionId, fresh.doc.docId);
|
|
137
|
+
await activateDocument(fresh);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (bound !== document.doc.docId) {
|
|
141
|
+
const next = await deps.registry.open(bound);
|
|
142
|
+
if (next === null) {
|
|
143
|
+
// Manifest entry without a document file — recover with a fresh doc.
|
|
144
|
+
const fresh = await deps.registry.create();
|
|
145
|
+
await deps.registry.bind(sessionId, fresh.doc.docId);
|
|
146
|
+
await activateDocument(fresh);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
await activateDocument(next);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (syncedEpoch !== workerResetEpoch()) {
|
|
153
|
+
await document.restore();
|
|
154
|
+
await replayActiveDoc();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
85
157
|
async function syncScene(op, result, filePath) {
|
|
86
158
|
if (result.mesh !== undefined && result.bodyId !== undefined) {
|
|
87
159
|
meshCache.set(result.bodyId, mirrorMesh(result.mesh, result.bodyId));
|
|
@@ -94,6 +166,7 @@ export function createModelTools(deps) {
|
|
|
94
166
|
meshCache.delete(id);
|
|
95
167
|
const nameEntry = result.bodyId !== undefined && result.name !== undefined ? { bodyId: result.bodyId, name: result.name } : null;
|
|
96
168
|
await document.record(op, nameEntry);
|
|
169
|
+
await deps.registry.touch(document.doc.docId, { opCount: document.doc.version, bodyCount: meshCache.size });
|
|
97
170
|
const meshes = [...meshCache.values()];
|
|
98
171
|
const triangles = meshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0);
|
|
99
172
|
const sceneUrlBase = deps.ensureSceneRoute();
|
|
@@ -134,6 +207,7 @@ export function createModelTools(deps) {
|
|
|
134
207
|
const instances = result.instances ?? [];
|
|
135
208
|
const meshes = composeAssemblyMeshes(meshCache, instances);
|
|
136
209
|
await document.record(op, null);
|
|
210
|
+
await deps.registry.touch(document.doc.docId, { opCount: document.doc.version, bodyCount: meshCache.size });
|
|
137
211
|
const sceneUrlBase = deps.ensureSceneRoute();
|
|
138
212
|
if (sceneUrlBase !== null) {
|
|
139
213
|
// Publish even when empty — removing the last instance must refresh the tab.
|
|
@@ -248,7 +322,7 @@ export function createModelTools(deps) {
|
|
|
248
322
|
};
|
|
249
323
|
const renderDrawing = (value) => {
|
|
250
324
|
const lines = [
|
|
251
|
-
`工程图 ${String(value.target)}: 主视图/俯视图/左视图/轴测 · 比例 ${String(value.scaleText ?? '')} · ${String(value.entities)} 实体 (version ${String(value.version)})`,
|
|
325
|
+
`工程图 ${String(value.target)}: 主视图/俯视图/左视图/轴测 · OCCT 真实消隐 · 比例 ${String(value.scaleText ?? '')} · ${String(value.entities)} 实体 (version ${String(value.version)})`,
|
|
252
326
|
];
|
|
253
327
|
if (value.filePath !== undefined)
|
|
254
328
|
lines.push(`written: ${String(value.filePath)}`);
|
|
@@ -328,8 +402,8 @@ export function createModelTools(deps) {
|
|
|
328
402
|
presentationMeta: (_args, value) => metaOf(value),
|
|
329
403
|
},
|
|
330
404
|
isConcurrencySafe: () => false,
|
|
331
|
-
async execute(args) {
|
|
332
|
-
await
|
|
405
|
+
async execute(args, exec) {
|
|
406
|
+
await resolveDoc(exec);
|
|
333
407
|
const bodyId = nextBodyId();
|
|
334
408
|
const params = args;
|
|
335
409
|
const op = { kind: 'create_prim', bodyId, prim: args.kind, params, name: args.name };
|
|
@@ -341,9 +415,11 @@ export function createModelTools(deps) {
|
|
|
341
415
|
});
|
|
342
416
|
const cadExtrude = defineTool({
|
|
343
417
|
name: 'cad_extrude_profile',
|
|
344
|
-
description: 'Create a solid by extruding a closed
|
|
418
|
+
description: 'Create a solid by extruding a closed profile in the XY plane along +Z (mm). `profile` accepts CURVE SEGMENTS — {start: [x,y], segments: [{type: "line"|"arc"|"bspline", …}]} (arc: to/center/ccw; bspline: through-points, sampled smooth curve) — or {circle: {center, radius}} for a round profile. ' +
|
|
419
|
+
'The legacy flat `points` polygon loop (≥3 points, auto-closed) still works. Use cad_boolean for holes.',
|
|
345
420
|
parameters: {
|
|
346
|
-
points: { type: 'array',
|
|
421
|
+
points: { type: 'array', items: { type: 'number' }, description: 'Legacy flat [x0,y0,x1,y1,…] polygon loop (mm).' },
|
|
422
|
+
profile: { type: 'json', description: 'Curve-segment profile: {start:[x,y], segments:[…]} or {circle:{center:[x,y], radius}}.' },
|
|
347
423
|
height: { type: 'number', description: 'Extrusion height (mm, default 10).' },
|
|
348
424
|
base: { type: 'number', description: 'Z of the profile plane (mm, default 0).' },
|
|
349
425
|
name: { type: 'string', description: 'Optional display name.' },
|
|
@@ -362,19 +438,292 @@ export function createModelTools(deps) {
|
|
|
362
438
|
presentationMeta: (_args, value) => metaOf(value),
|
|
363
439
|
},
|
|
364
440
|
isConcurrencySafe: () => false,
|
|
365
|
-
async execute(args) {
|
|
366
|
-
await
|
|
367
|
-
if (args.points.length < 6 || args.points.length % 2 !== 0) {
|
|
368
|
-
throw new Error('
|
|
441
|
+
async execute(args, exec) {
|
|
442
|
+
await resolveDoc(exec);
|
|
443
|
+
if (args.profile === undefined && (args.points === undefined || args.points.length < 6 || args.points.length % 2 !== 0)) {
|
|
444
|
+
throw new Error('provide a curve-segment `profile` object or a flat points array of ≥3 [x,y] pairs');
|
|
369
445
|
}
|
|
370
446
|
const bodyId = nextBodyId();
|
|
371
|
-
const op = { kind: 'extrude_profile', bodyId, points: args.points, height: args.height, base: args.base, name: args.name };
|
|
447
|
+
const op = { kind: 'extrude_profile', bodyId, ...(args.profile !== undefined ? { profile: args.profile } : { points: args.points }), height: args.height, base: args.base, name: args.name };
|
|
372
448
|
const result = await runModelOp(op);
|
|
373
449
|
return syncScene(op, result);
|
|
374
450
|
},
|
|
375
451
|
presentCall: () => ({ card: 'generic', title: 'CAD extrude profile', kind: 'other' }),
|
|
376
452
|
presentResult: () => ({ card: 'generic', title: 'CAD extrude' }),
|
|
377
453
|
});
|
|
454
|
+
const cadLoft = defineTool({
|
|
455
|
+
name: 'cad_loft',
|
|
456
|
+
description: 'Loft (放样): skin a solid through successive closed sections. `sections` is a list of ≥2 loops, each a flat [x0,y0,z0, x1,y1,z1, …] triplet list in its own plane (≥3 points, auto-closed), ordered along the loft. ' +
|
|
457
|
+
'Sections may differ in shape and point count (e.g. a square lofted to a hexagon). `ruled` keeps the sides straight instead of smoothing. ' +
|
|
458
|
+
'Each result passes a BRepCheck validity gate: a self-intersecting or degenerate loft is rejected with a clear message instead of storing an unusable body.',
|
|
459
|
+
parameters: {
|
|
460
|
+
sections: {
|
|
461
|
+
type: 'array',
|
|
462
|
+
required: true,
|
|
463
|
+
description: 'Ordered sections, each a flat [x,y,z,…] loop (≥9 numbers).',
|
|
464
|
+
items: { type: 'array', items: { type: 'number' }, description: 'One closed section loop.' },
|
|
465
|
+
},
|
|
466
|
+
ruled: { type: 'boolean', description: 'Straight (ruled) sides instead of a smoothed surface (default false).' },
|
|
467
|
+
solid: { type: 'boolean', description: 'Cap the ends into a solid (default true).' },
|
|
468
|
+
name: { type: 'string', description: 'Optional display name.' },
|
|
469
|
+
},
|
|
470
|
+
output: {
|
|
471
|
+
schema: {
|
|
472
|
+
type: 'object',
|
|
473
|
+
additionalProperties: false,
|
|
474
|
+
properties: {
|
|
475
|
+
bodyId: { type: 'string', required: true },
|
|
476
|
+
...requiredCounts,
|
|
477
|
+
...commonOptional,
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
481
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
482
|
+
},
|
|
483
|
+
isConcurrencySafe: () => false,
|
|
484
|
+
async execute(args, exec) {
|
|
485
|
+
await resolveDoc(exec);
|
|
486
|
+
if (!Array.isArray(args.sections) || args.sections.length < 2)
|
|
487
|
+
throw new Error('sections must list at least 2 closed loops');
|
|
488
|
+
const bodyId = nextBodyId();
|
|
489
|
+
const op = { kind: 'loft', bodyId, sections: args.sections, solid: args.solid, ruled: args.ruled, name: args.name };
|
|
490
|
+
const result = await runModelOp(op);
|
|
491
|
+
return syncScene(op, result);
|
|
492
|
+
},
|
|
493
|
+
presentCall: () => ({ card: 'generic', title: 'CAD loft', kind: 'other' }),
|
|
494
|
+
presentResult: () => ({ card: 'generic', title: 'CAD loft' }),
|
|
495
|
+
});
|
|
496
|
+
const cadSweep = defineTool({
|
|
497
|
+
name: 'cad_sweep',
|
|
498
|
+
description: 'Sweep (扫掠): pipe a closed 2D profile along a 3D path. `profile` is a flat [x0,y0, x1,y1, …] outline (≥3 points, auto-closed) placed in the plane PERPENDICULAR TO THE PATH\'S START TANGENT, so the outline\'s 2D axes map onto that plane — no manual orientation needed. ' +
|
|
499
|
+
'`path` is a flat [x0,y0,z0, …] polyline. Straight, collinear and gently curved paths give exact solids; a SHARP direction change with a section large relative to the corner self-intersects — such a result is REJECTED by the BRepCheck validity gate (the error names the cause), so round or chamfer the corners in the path.',
|
|
500
|
+
parameters: {
|
|
501
|
+
profile: { type: 'json', required: true, description: 'Closed outline on the start plane: flat [x,y,…] array, {start, segments:[…]} curve chain, or {circle:{center,radius}}.' },
|
|
502
|
+
path: { type: 'array', required: true, items: { type: 'number' }, description: 'Sweep path as [x,y,z,…] triplets (≥6 numbers).' },
|
|
503
|
+
name: { type: 'string', description: 'Optional display name.' },
|
|
504
|
+
},
|
|
505
|
+
output: {
|
|
506
|
+
schema: {
|
|
507
|
+
type: 'object',
|
|
508
|
+
additionalProperties: false,
|
|
509
|
+
properties: {
|
|
510
|
+
bodyId: { type: 'string', required: true },
|
|
511
|
+
...requiredCounts,
|
|
512
|
+
...commonOptional,
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
516
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
517
|
+
},
|
|
518
|
+
isConcurrencySafe: () => false,
|
|
519
|
+
async execute(args, exec) {
|
|
520
|
+
await resolveDoc(exec);
|
|
521
|
+
const bodyId = nextBodyId();
|
|
522
|
+
const op = { kind: 'sweep', bodyId, profile: args.profile, path: args.path, name: args.name };
|
|
523
|
+
const result = await runModelOp(op);
|
|
524
|
+
return syncScene(op, result);
|
|
525
|
+
},
|
|
526
|
+
presentCall: () => ({ card: 'generic', title: 'CAD sweep', kind: 'other' }),
|
|
527
|
+
presentResult: () => ({ card: 'generic', title: 'CAD sweep' }),
|
|
528
|
+
});
|
|
529
|
+
const cadRevolve = defineTool({
|
|
530
|
+
name: 'cad_revolve',
|
|
531
|
+
description: 'Revolve (旋转): sweep a closed 2D profile around an axis into a solid of revolution — shafts, wheels, vases, cones, spheres. Profile coordinates are (radius, height) relative to the axis: with the default +Z axis, profile [x,y] means radius x at height y. ' +
|
|
532
|
+
'Accepts the same curve-segment profiles as cad_extrude_profile (lines/arcs/bspline/circle), so rounded rims are exact. `angle` in degrees (default 360).',
|
|
533
|
+
parameters: {
|
|
534
|
+
profile: { type: 'json', required: true, description: 'Profile in (radius, height): flat [r0,h0, r1,h1,…] loop, {start, segments:[…]}, or {circle:{center,radius}}.' },
|
|
535
|
+
angle: { type: 'number', description: 'Sweep angle in degrees (default 360).' },
|
|
536
|
+
axis: { type: 'array', items: { type: 'number' }, description: 'Revolve axis direction [x,y,z] (default +Z).' },
|
|
537
|
+
at: { type: 'array', items: { type: 'number' }, description: 'A point the axis passes through [x,y,z] (default origin).' },
|
|
538
|
+
name: { type: 'string', description: 'Optional display name.' },
|
|
539
|
+
},
|
|
540
|
+
output: {
|
|
541
|
+
schema: {
|
|
542
|
+
type: 'object',
|
|
543
|
+
additionalProperties: false,
|
|
544
|
+
properties: {
|
|
545
|
+
bodyId: { type: 'string', required: true },
|
|
546
|
+
...requiredCounts,
|
|
547
|
+
...commonOptional,
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
551
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
552
|
+
},
|
|
553
|
+
isConcurrencySafe: () => false,
|
|
554
|
+
async execute(args, exec) {
|
|
555
|
+
await resolveDoc(exec);
|
|
556
|
+
const bodyId = nextBodyId();
|
|
557
|
+
const op = {
|
|
558
|
+
kind: 'revolve', bodyId, profile: args.profile,
|
|
559
|
+
...(args.angle !== undefined ? { angle: (args.angle * Math.PI) / 180 } : {}),
|
|
560
|
+
...(Array.isArray(args.axis) ? { axis: args.axis } : {}),
|
|
561
|
+
...(Array.isArray(args.at) ? { at: args.at } : {}),
|
|
562
|
+
...(args.name !== undefined ? { name: args.name } : {}),
|
|
563
|
+
};
|
|
564
|
+
const result = await runModelOp(op);
|
|
565
|
+
return syncScene(op, result);
|
|
566
|
+
},
|
|
567
|
+
presentCall: () => ({ card: 'generic', title: 'CAD revolve', kind: 'other' }),
|
|
568
|
+
presentResult: () => ({ card: 'generic', title: 'CAD revolve' }),
|
|
569
|
+
});
|
|
570
|
+
const cadChamfer = defineTool({
|
|
571
|
+
name: 'cad_chamfer',
|
|
572
|
+
description: 'Chamfer (倒角): bevel every sharp edge of a body with one equal distance (mm). Fails when the distance exceeds the adjacent faces.',
|
|
573
|
+
parameters: {
|
|
574
|
+
target: bodyTarget,
|
|
575
|
+
distance: { type: 'number', required: true, description: 'Chamfer distance (mm).' },
|
|
576
|
+
},
|
|
577
|
+
output: {
|
|
578
|
+
schema: {
|
|
579
|
+
type: 'object',
|
|
580
|
+
additionalProperties: false,
|
|
581
|
+
properties: {
|
|
582
|
+
bodyId: { type: 'string', required: true },
|
|
583
|
+
...requiredCounts,
|
|
584
|
+
...commonOptional,
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
588
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
589
|
+
},
|
|
590
|
+
isConcurrencySafe: () => false,
|
|
591
|
+
async execute(args, exec) {
|
|
592
|
+
await resolveDoc(exec);
|
|
593
|
+
const op = { kind: 'chamfer', target: args.target, distance: args.distance };
|
|
594
|
+
const result = await runModelOp(op);
|
|
595
|
+
return syncScene(op, result);
|
|
596
|
+
},
|
|
597
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD chamfer ${String(args.target)}`, kind: 'other' }),
|
|
598
|
+
presentResult: () => ({ card: 'generic', title: 'CAD chamfer' }),
|
|
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
|
+
});
|
|
664
|
+
const cadPattern = defineTool({
|
|
665
|
+
name: 'cad_pattern',
|
|
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.',
|
|
667
|
+
parameters: {
|
|
668
|
+
target: bodyTarget,
|
|
669
|
+
mode: { type: 'string', required: true, enum: ['linear', 'circular'], description: 'Pattern mode.' },
|
|
670
|
+
count: { type: 'number', required: true, description: 'Total placements (≥2).' },
|
|
671
|
+
delta: { type: 'array', items: { type: 'number' }, description: 'linear: spacing [dx,dy,dz] per copy (mm).' },
|
|
672
|
+
axis: { type: 'array', items: { type: 'number' }, description: 'circular: principal axis [x,y,z] (default +Z).' },
|
|
673
|
+
at: { type: 'array', items: { type: 'number' }, description: 'circular: axis point [x,y,z] (default origin).' },
|
|
674
|
+
angle: { type: 'number', description: 'circular: total sweep in degrees (default 360).' },
|
|
675
|
+
},
|
|
676
|
+
output: {
|
|
677
|
+
schema: {
|
|
678
|
+
type: 'object',
|
|
679
|
+
additionalProperties: false,
|
|
680
|
+
properties: {
|
|
681
|
+
bodyId: { type: 'string', required: true },
|
|
682
|
+
created: { type: 'array', items: { type: 'string' }, required: true, description: 'Ids of the newly created copy bodies.' },
|
|
683
|
+
...requiredCounts,
|
|
684
|
+
...commonOptional,
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) + '\ncreated: ' + value.created.join(', ') }],
|
|
688
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
689
|
+
},
|
|
690
|
+
isConcurrencySafe: () => false,
|
|
691
|
+
async execute(args, exec) {
|
|
692
|
+
await resolveDoc(exec);
|
|
693
|
+
if (args.mode === 'linear' && !Array.isArray(args.delta))
|
|
694
|
+
throw new Error('linear patterns need delta: [dx,dy,dz]');
|
|
695
|
+
const op = {
|
|
696
|
+
kind: 'pattern', target: args.target, mode: args.mode, count: args.count,
|
|
697
|
+
...(Array.isArray(args.delta) ? { delta: args.delta } : {}),
|
|
698
|
+
...(Array.isArray(args.axis) ? { axis: args.axis } : {}),
|
|
699
|
+
...(Array.isArray(args.at) ? { at: args.at } : {}),
|
|
700
|
+
...(args.angle !== undefined ? { angle: args.angle } : {}),
|
|
701
|
+
};
|
|
702
|
+
const result = await runModelOp(op);
|
|
703
|
+
const created = (result.created ?? []).map((entry) => {
|
|
704
|
+
if (entry.mesh !== undefined)
|
|
705
|
+
meshCache.set(entry.bodyId, mirrorMesh(entry.mesh, entry.bodyId));
|
|
706
|
+
return entry.bodyId;
|
|
707
|
+
});
|
|
708
|
+
await document.record(op, null);
|
|
709
|
+
const meshes = [...meshCache.values()];
|
|
710
|
+
const sceneUrlBase = deps.ensureSceneRoute();
|
|
711
|
+
if (sceneUrlBase !== null && meshes.length > 0) {
|
|
712
|
+
await deps.store.publish(document.doc.docId, meshes);
|
|
713
|
+
}
|
|
714
|
+
const value = {
|
|
715
|
+
bodyId: args.target,
|
|
716
|
+
created,
|
|
717
|
+
triangles: meshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0),
|
|
718
|
+
bodies: meshes.length,
|
|
719
|
+
version: document.doc.version,
|
|
720
|
+
...(sceneUrlBase !== null ? { sceneUrl: `${sceneUrlBase.replace('/scene', '/bin')}/${document.doc.docId}?v=${document.doc.version}` } : {}),
|
|
721
|
+
};
|
|
722
|
+
return value;
|
|
723
|
+
},
|
|
724
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD ${String(args.mode ?? '')} pattern ×${String(args.count ?? '')}`, kind: 'other' }),
|
|
725
|
+
presentResult: () => ({ card: 'generic', title: 'CAD pattern' }),
|
|
726
|
+
});
|
|
378
727
|
const cadBoolean = defineTool({
|
|
379
728
|
name: 'cad_boolean',
|
|
380
729
|
description: 'Boolean-combine bodies: fuse (union), cut (subtract tools from target), common (intersection). Consumed tool bodies are removed; the result keeps the target bodyId. Classic pattern for holes: cut a cylinder from a plate.',
|
|
@@ -397,8 +746,8 @@ export function createModelTools(deps) {
|
|
|
397
746
|
presentationMeta: (_args, value) => metaOf(value),
|
|
398
747
|
},
|
|
399
748
|
isConcurrencySafe: () => false,
|
|
400
|
-
async execute(args) {
|
|
401
|
-
await
|
|
749
|
+
async execute(args, exec) {
|
|
750
|
+
await resolveDoc(exec);
|
|
402
751
|
const op = { kind: 'boolean', op: args.op, target: args.target, tools: args.tools };
|
|
403
752
|
const result = await runModelOp(op);
|
|
404
753
|
return syncScene(op, result);
|
|
@@ -427,8 +776,8 @@ export function createModelTools(deps) {
|
|
|
427
776
|
presentationMeta: (_args, value) => metaOf(value),
|
|
428
777
|
},
|
|
429
778
|
isConcurrencySafe: () => false,
|
|
430
|
-
async execute(args) {
|
|
431
|
-
await
|
|
779
|
+
async execute(args, exec) {
|
|
780
|
+
await resolveDoc(exec);
|
|
432
781
|
const op = { kind: 'fillet', target: args.target, radius: args.radius };
|
|
433
782
|
const result = await runModelOp(op);
|
|
434
783
|
return syncScene(op, result);
|
|
@@ -459,8 +808,8 @@ export function createModelTools(deps) {
|
|
|
459
808
|
presentationMeta: (_args, value) => metaOf(value),
|
|
460
809
|
},
|
|
461
810
|
isConcurrencySafe: () => false,
|
|
462
|
-
async execute(args) {
|
|
463
|
-
await
|
|
811
|
+
async execute(args, exec) {
|
|
812
|
+
await resolveDoc(exec);
|
|
464
813
|
const op = {
|
|
465
814
|
kind: 'transform',
|
|
466
815
|
target: args.target,
|
|
@@ -514,8 +863,8 @@ export function createModelTools(deps) {
|
|
|
514
863
|
},
|
|
515
864
|
},
|
|
516
865
|
isConcurrencySafe: () => true,
|
|
517
|
-
async execute(args) {
|
|
518
|
-
await
|
|
866
|
+
async execute(args, exec) {
|
|
867
|
+
await resolveDoc(exec);
|
|
519
868
|
const resolved = resolveWorkspacePath(args.path, deps.workspaceRoot);
|
|
520
869
|
const lower = args.path.toLowerCase();
|
|
521
870
|
// Assembly export: compound of transformed instances via the worker.
|
|
@@ -611,8 +960,8 @@ export function createModelTools(deps) {
|
|
|
611
960
|
presentationMeta: (_args, value) => metaOf(value),
|
|
612
961
|
},
|
|
613
962
|
isConcurrencySafe: () => false,
|
|
614
|
-
async execute(args) {
|
|
615
|
-
await
|
|
963
|
+
async execute(args, exec) {
|
|
964
|
+
await resolveDoc(exec);
|
|
616
965
|
const op = { kind: 'delete', target: args.target };
|
|
617
966
|
const result = await runModelOp(op);
|
|
618
967
|
return syncScene(op, result);
|
|
@@ -640,8 +989,8 @@ export function createModelTools(deps) {
|
|
|
640
989
|
presentationMeta: (_args, value) => metaOf(value),
|
|
641
990
|
},
|
|
642
991
|
isConcurrencySafe: () => true,
|
|
643
|
-
async execute(args) {
|
|
644
|
-
await
|
|
992
|
+
async execute(args, exec) {
|
|
993
|
+
await resolveDoc(exec);
|
|
645
994
|
const op = { kind: 'volume', target: args.target };
|
|
646
995
|
const result = await runModelOp(op);
|
|
647
996
|
return syncScene(op, result);
|
|
@@ -668,8 +1017,8 @@ export function createModelTools(deps) {
|
|
|
668
1017
|
presentationMeta: (_args, value) => drawingMetaOf(value),
|
|
669
1018
|
},
|
|
670
1019
|
isConcurrencySafe: () => false,
|
|
671
|
-
async execute(args) {
|
|
672
|
-
await
|
|
1020
|
+
async execute(args, exec) {
|
|
1021
|
+
await resolveDoc(exec);
|
|
673
1022
|
const target = args.target ?? lastBodyId();
|
|
674
1023
|
if (target === undefined)
|
|
675
1024
|
throw new Error('nothing to draw — create a body first');
|
|
@@ -717,8 +1066,8 @@ export function createModelTools(deps) {
|
|
|
717
1066
|
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
718
1067
|
},
|
|
719
1068
|
isConcurrencySafe: () => false,
|
|
720
|
-
async execute(args) {
|
|
721
|
-
await
|
|
1069
|
+
async execute(args, exec) {
|
|
1070
|
+
await resolveDoc(exec);
|
|
722
1071
|
if (!meshCache.has(args.bodyId))
|
|
723
1072
|
throw new Error(`unknown body: ${args.bodyId}`);
|
|
724
1073
|
const instanceId = nextInstanceId();
|
|
@@ -750,8 +1099,8 @@ export function createModelTools(deps) {
|
|
|
750
1099
|
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
751
1100
|
},
|
|
752
1101
|
isConcurrencySafe: () => false,
|
|
753
|
-
async execute(args) {
|
|
754
|
-
await
|
|
1102
|
+
async execute(args, exec) {
|
|
1103
|
+
await resolveDoc(exec);
|
|
755
1104
|
const op = {
|
|
756
1105
|
kind: 'assembly_transform',
|
|
757
1106
|
instanceId: args.instanceId,
|
|
@@ -776,8 +1125,8 @@ export function createModelTools(deps) {
|
|
|
776
1125
|
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
777
1126
|
},
|
|
778
1127
|
isConcurrencySafe: () => false,
|
|
779
|
-
async execute(args) {
|
|
780
|
-
await
|
|
1128
|
+
async execute(args, exec) {
|
|
1129
|
+
await resolveDoc(exec);
|
|
781
1130
|
const op = { kind: 'assembly_remove', instanceId: args.instanceId };
|
|
782
1131
|
const result = await runModelOp(op);
|
|
783
1132
|
return syncAssembly(op, result);
|
|
@@ -785,9 +1134,244 @@ export function createModelTools(deps) {
|
|
|
785
1134
|
presentCall: (args) => ({ card: 'generic', title: `移除 ${String(args.instanceId)}`, kind: 'delete' }),
|
|
786
1135
|
presentResult: () => ({ card: 'generic', title: '装配移除' }),
|
|
787
1136
|
});
|
|
1137
|
+
// ── document (file space) tools ───────────────────────────────────────────
|
|
1138
|
+
const docRefParam = { type: 'string', required: true, description: 'Document id or exact name (list with cad_docs).' };
|
|
1139
|
+
const renderDocs = (value) => {
|
|
1140
|
+
const docs = Array.isArray(value.docs) ? value.docs : [];
|
|
1141
|
+
const active = typeof value.activeDoc === 'string' ? value.activeDoc : '';
|
|
1142
|
+
const lines = docs.map((doc) => `${doc.id === active ? '● ' : ' '}${String(doc.name)} · ${Number(doc.bodies)} 体 · ${String(doc.updatedAt).slice(0, 16).replace('T', ' ')} · ${String(doc.id)}`);
|
|
1143
|
+
return lines.length > 0 ? ['工作区文档:', ...lines].join('\n') : '工作区还没有建模文档';
|
|
1144
|
+
};
|
|
1145
|
+
const cadDocs = defineTool({
|
|
1146
|
+
name: 'cad_docs',
|
|
1147
|
+
description: 'List the workspace modeling documents (the file space): id, name, body count and update time, marking the one active for this session. ' +
|
|
1148
|
+
'Call before cad_doc_open / cad_doc_delete to discover ids and names.',
|
|
1149
|
+
parameters: {},
|
|
1150
|
+
output: {
|
|
1151
|
+
schema: {
|
|
1152
|
+
type: 'object',
|
|
1153
|
+
additionalProperties: false,
|
|
1154
|
+
properties: {
|
|
1155
|
+
docs: {
|
|
1156
|
+
type: 'array',
|
|
1157
|
+
required: true,
|
|
1158
|
+
items: {
|
|
1159
|
+
type: 'object',
|
|
1160
|
+
additionalProperties: false,
|
|
1161
|
+
properties: {
|
|
1162
|
+
id: { type: 'string', required: true },
|
|
1163
|
+
name: { type: 'string', required: true },
|
|
1164
|
+
bodies: { type: 'number', required: true },
|
|
1165
|
+
ops: { type: 'number', required: true },
|
|
1166
|
+
updatedAt: { type: 'string', required: true },
|
|
1167
|
+
active: { type: 'boolean' },
|
|
1168
|
+
},
|
|
1169
|
+
},
|
|
1170
|
+
},
|
|
1171
|
+
activeDoc: { type: 'string', description: 'Active document id for this session (absent: none).' },
|
|
1172
|
+
},
|
|
1173
|
+
},
|
|
1174
|
+
render: (_args, value) => [{ type: 'text', text: renderDocs(value) }],
|
|
1175
|
+
},
|
|
1176
|
+
isConcurrencySafe: () => true,
|
|
1177
|
+
async execute(_args, exec) {
|
|
1178
|
+
const sessionId = sessionKeyOf(exec);
|
|
1179
|
+
const docs = await deps.registry.list();
|
|
1180
|
+
const activeDoc = await deps.registry.bindingOf(sessionId);
|
|
1181
|
+
return {
|
|
1182
|
+
docs: docs.map((doc) => ({ id: doc.id, name: doc.name, bodies: doc.bodyCount, ops: doc.opCount, updatedAt: doc.updatedAt, ...(doc.id === activeDoc ? { active: true } : {}) })),
|
|
1183
|
+
...(activeDoc === null ? {} : { activeDoc }),
|
|
1184
|
+
};
|
|
1185
|
+
},
|
|
1186
|
+
presentCall: () => ({ card: 'generic', title: '文档列表', kind: 'read' }),
|
|
1187
|
+
presentResult: () => ({ card: 'generic', title: '文档列表' }),
|
|
1188
|
+
});
|
|
1189
|
+
const cadDocNew = defineTool({
|
|
1190
|
+
name: 'cad_doc_new',
|
|
1191
|
+
description: 'Create a new modeling document and make it this session\'s active modeling target (previous documents stay in the file space). ' +
|
|
1192
|
+
'Name it when the task has a clear subject (e.g. 发动机 / 齿轮箱) — start multi-part projects with this tool so each project keeps its own document.',
|
|
1193
|
+
parameters: {
|
|
1194
|
+
name: { type: 'string', description: 'Document name (default: 未命名 N).' },
|
|
1195
|
+
},
|
|
1196
|
+
output: {
|
|
1197
|
+
schema: {
|
|
1198
|
+
type: 'object',
|
|
1199
|
+
additionalProperties: false,
|
|
1200
|
+
properties: {
|
|
1201
|
+
docId: { type: 'string', required: true, description: 'New document id.' },
|
|
1202
|
+
name: { type: 'string', required: true, description: 'Document name.' },
|
|
1203
|
+
bodies: { type: 'number', required: true, description: 'Bodies (0 for a fresh document).' },
|
|
1204
|
+
version: { type: 'number', required: true, description: 'Document version.' },
|
|
1205
|
+
},
|
|
1206
|
+
},
|
|
1207
|
+
render: (_args, value) => [{ type: 'text', text: `新建文档 ${String(value.name)}(后续建模操作都写入该文档)` }],
|
|
1208
|
+
},
|
|
1209
|
+
isConcurrencySafe: () => false,
|
|
1210
|
+
async execute(args, exec) {
|
|
1211
|
+
const sessionId = sessionKeyOf(exec);
|
|
1212
|
+
const doc = await deps.registry.create(args.name);
|
|
1213
|
+
await deps.registry.bind(sessionId, doc.doc.docId);
|
|
1214
|
+
await activateDocument(doc);
|
|
1215
|
+
const name = (await deps.registry.list()).find((meta) => meta.id === doc.doc.docId)?.name ?? doc.doc.docId;
|
|
1216
|
+
return { docId: doc.doc.docId, name, bodies: 0, version: doc.doc.version };
|
|
1217
|
+
},
|
|
1218
|
+
presentCall: (args) => ({ card: 'generic', title: `新建文档 ${String(args.name ?? '')}`.trim(), kind: 'other' }),
|
|
1219
|
+
presentResult: () => ({ card: 'generic', title: '新建文档' }),
|
|
1220
|
+
});
|
|
1221
|
+
const cadDocOpen = defineTool({
|
|
1222
|
+
name: 'cad_doc_open',
|
|
1223
|
+
description: 'Open an existing modeling document and make it this session\'s active modeling target (its bodies load back exactly). ' +
|
|
1224
|
+
'Use when the user asks to continue an earlier model (e.g. “打开发动机文档”) — resolve the id/name with cad_docs first when unsure.',
|
|
1225
|
+
parameters: {
|
|
1226
|
+
doc: docRefParam,
|
|
1227
|
+
},
|
|
1228
|
+
output: {
|
|
1229
|
+
schema: {
|
|
1230
|
+
type: 'object',
|
|
1231
|
+
additionalProperties: false,
|
|
1232
|
+
properties: {
|
|
1233
|
+
docId: { type: 'string', required: true, description: 'Opened document id.' },
|
|
1234
|
+
name: { type: 'string', required: true, description: 'Document name.' },
|
|
1235
|
+
bodies: { type: 'number', required: true, description: 'Bodies in the document.' },
|
|
1236
|
+
triangles: { type: 'number', required: true, description: 'Document triangle count.' },
|
|
1237
|
+
version: { type: 'number', required: true, description: 'Document version.' },
|
|
1238
|
+
sceneUrl: { type: 'string', description: 'Versioned viewer URL (web compositions).' },
|
|
1239
|
+
},
|
|
1240
|
+
},
|
|
1241
|
+
render: (_args, value) => [{ type: 'text', text: `打开文档 ${String(value.name)} · ${Number(value.bodies)} 体 (version ${Number(value.version)})` }],
|
|
1242
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
1243
|
+
},
|
|
1244
|
+
isConcurrencySafe: () => false,
|
|
1245
|
+
async execute(args, exec) {
|
|
1246
|
+
const sessionId = sessionKeyOf(exec);
|
|
1247
|
+
const meta = await deps.registry.resolve(args.doc);
|
|
1248
|
+
if (meta === null) {
|
|
1249
|
+
const available = (await deps.registry.list()).map((doc) => doc.name).join(' / ');
|
|
1250
|
+
throw new Error(`no document matches "${String(args.doc)}" — available: ${available === '' ? '(none)' : available}`);
|
|
1251
|
+
}
|
|
1252
|
+
await deps.registry.bind(sessionId, meta.id);
|
|
1253
|
+
const doc = await deps.registry.open(meta.id);
|
|
1254
|
+
if (doc === null)
|
|
1255
|
+
throw new Error(`document file missing: ${meta.id}`);
|
|
1256
|
+
await activateDocument(doc);
|
|
1257
|
+
const meshes = [...meshCache.values()];
|
|
1258
|
+
const triangles = meshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0);
|
|
1259
|
+
const sceneUrlBase = deps.ensureSceneRoute();
|
|
1260
|
+
const value = {
|
|
1261
|
+
docId: meta.id,
|
|
1262
|
+
name: meta.name,
|
|
1263
|
+
bodies: meshes.length,
|
|
1264
|
+
triangles,
|
|
1265
|
+
version: document.doc.version,
|
|
1266
|
+
};
|
|
1267
|
+
if (meshes.length > 0)
|
|
1268
|
+
await deps.store.publish(meta.id, meshes);
|
|
1269
|
+
if (meshes.length > 0 && sceneUrlBase !== null) {
|
|
1270
|
+
value.sceneUrl = `${sceneUrlBase.replace('/scene', '/bin')}/${meta.id}?v=${document.doc.version}`;
|
|
1271
|
+
}
|
|
1272
|
+
return value;
|
|
1273
|
+
},
|
|
1274
|
+
presentCall: (args) => ({ card: 'generic', title: `打开文档 ${String(args.doc)}`, kind: 'other' }),
|
|
1275
|
+
presentResult: () => ({ card: 'generic', title: '打开文档' }),
|
|
1276
|
+
});
|
|
1277
|
+
const cadDocRename = defineTool({
|
|
1278
|
+
name: 'cad_doc_rename',
|
|
1279
|
+
description: 'Rename a modeling document in the file space.',
|
|
1280
|
+
parameters: {
|
|
1281
|
+
doc: docRefParam,
|
|
1282
|
+
name: { type: 'string', required: true, description: 'New document name.' },
|
|
1283
|
+
},
|
|
1284
|
+
output: {
|
|
1285
|
+
schema: {
|
|
1286
|
+
type: 'object',
|
|
1287
|
+
additionalProperties: false,
|
|
1288
|
+
properties: {
|
|
1289
|
+
docId: { type: 'string', required: true, description: 'Document id.' },
|
|
1290
|
+
name: { type: 'string', required: true, description: 'New name.' },
|
|
1291
|
+
},
|
|
1292
|
+
},
|
|
1293
|
+
render: (_args, value) => [{ type: 'text', text: `文档已重命名为 ${String(value.name)}` }],
|
|
1294
|
+
},
|
|
1295
|
+
isConcurrencySafe: () => false,
|
|
1296
|
+
async execute(args, exec) {
|
|
1297
|
+
void exec;
|
|
1298
|
+
const meta = await deps.registry.resolve(args.doc);
|
|
1299
|
+
if (meta === null)
|
|
1300
|
+
throw new Error(`no document matches "${String(args.doc)}"`);
|
|
1301
|
+
const renamed = await deps.registry.rename(meta.id, args.name);
|
|
1302
|
+
if (renamed === null)
|
|
1303
|
+
throw new Error(`rename failed: ${meta.id}`);
|
|
1304
|
+
return { docId: renamed.id, name: renamed.name };
|
|
1305
|
+
},
|
|
1306
|
+
presentCall: (args) => ({ card: 'generic', title: `重命名文档 ${String(args.doc)}`, kind: 'other' }),
|
|
1307
|
+
presentResult: () => ({ card: 'generic', title: '重命名文档' }),
|
|
1308
|
+
});
|
|
1309
|
+
const cadDocDelete = defineTool({
|
|
1310
|
+
name: 'cad_doc_delete',
|
|
1311
|
+
description: 'Permanently delete a modeling document from the file space (op log + bodies). Requires confirm=true — ask the user before calling. ' +
|
|
1312
|
+
'To delete a single body inside a document, use cad_delete instead.',
|
|
1313
|
+
parameters: {
|
|
1314
|
+
doc: docRefParam,
|
|
1315
|
+
confirm: { type: 'boolean', required: true, description: 'Must be explicitly true to delete.' },
|
|
1316
|
+
},
|
|
1317
|
+
output: {
|
|
1318
|
+
schema: {
|
|
1319
|
+
type: 'object',
|
|
1320
|
+
additionalProperties: false,
|
|
1321
|
+
properties: {
|
|
1322
|
+
deleted: { type: 'string', required: true, description: 'Deleted document id.' },
|
|
1323
|
+
name: { type: 'string', required: true, description: 'Deleted document name.' },
|
|
1324
|
+
},
|
|
1325
|
+
},
|
|
1326
|
+
render: (_args, value) => [{ type: 'text', text: `已删除文档 ${String(value.name)}` }],
|
|
1327
|
+
},
|
|
1328
|
+
isConcurrencySafe: () => false,
|
|
1329
|
+
async execute(args, exec) {
|
|
1330
|
+
void exec;
|
|
1331
|
+
if (args.confirm !== true) {
|
|
1332
|
+
throw new Error('pass confirm=true to delete — this permanently removes the document and its bodies');
|
|
1333
|
+
}
|
|
1334
|
+
const meta = await deps.registry.resolve(args.doc);
|
|
1335
|
+
if (meta === null)
|
|
1336
|
+
throw new Error(`no document matches "${String(args.doc)}"`);
|
|
1337
|
+
const removed = await deps.registry.remove(meta.id);
|
|
1338
|
+
if (!removed)
|
|
1339
|
+
throw new Error(`delete failed: ${meta.id}`);
|
|
1340
|
+
// Drop the active-document state if it was the deleted one; the next
|
|
1341
|
+
// modeling op lazily creates a fresh document for its session.
|
|
1342
|
+
if (document.doc.docId === meta.id) {
|
|
1343
|
+
document = new ModelDocument(deps.workspaceRoot);
|
|
1344
|
+
meshCache.clear();
|
|
1345
|
+
drawingSheets = new Map();
|
|
1346
|
+
lastDrawingId = null;
|
|
1347
|
+
syncedEpoch = -1;
|
|
1348
|
+
}
|
|
1349
|
+
return { deleted: meta.id, name: meta.name };
|
|
1350
|
+
},
|
|
1351
|
+
presentCall: (args) => ({ card: 'generic', title: `删除文档 ${String(args.doc)}`, kind: 'delete' }),
|
|
1352
|
+
presentResult: () => ({ card: 'generic', title: '删除文档' }),
|
|
1353
|
+
});
|
|
1354
|
+
// ── constraint tools (Ansatz solver) ──────────────────────────────────────
|
|
1355
|
+
const constraintTools = createConstraintTools({
|
|
1356
|
+
deps,
|
|
1357
|
+
// Read the ACTIVE document per call: multi-document sessions swap it.
|
|
1358
|
+
getDocument: () => document,
|
|
1359
|
+
meshCache,
|
|
1360
|
+
constraintState,
|
|
1361
|
+
resolveDoc,
|
|
1362
|
+
syncAssembly,
|
|
1363
|
+
assemblyMetaOf,
|
|
1364
|
+
});
|
|
788
1365
|
return [
|
|
789
1366
|
cadCreatePrim,
|
|
790
1367
|
cadExtrude,
|
|
1368
|
+
cadRevolve,
|
|
1369
|
+
cadChamfer,
|
|
1370
|
+
cadShell,
|
|
1371
|
+
cadDraft,
|
|
1372
|
+
cadPattern,
|
|
1373
|
+
cadLoft,
|
|
1374
|
+
cadSweep,
|
|
791
1375
|
cadBoolean,
|
|
792
1376
|
cadFillet,
|
|
793
1377
|
cadTransform,
|
|
@@ -798,11 +1382,24 @@ export function createModelTools(deps) {
|
|
|
798
1382
|
cadAssemblyInsert,
|
|
799
1383
|
cadAssemblyMove,
|
|
800
1384
|
cadAssemblyRemove,
|
|
1385
|
+
cadDocs,
|
|
1386
|
+
cadDocNew,
|
|
1387
|
+
cadDocOpen,
|
|
1388
|
+
cadDocRename,
|
|
1389
|
+
cadDocDelete,
|
|
1390
|
+
...constraintTools,
|
|
801
1391
|
];
|
|
802
1392
|
}
|
|
803
1393
|
export const MODEL_TOOL_NAMES = [
|
|
804
1394
|
'cad_create_prim',
|
|
805
1395
|
'cad_extrude_profile',
|
|
1396
|
+
'cad_revolve',
|
|
1397
|
+
'cad_chamfer',
|
|
1398
|
+
'cad_shell',
|
|
1399
|
+
'cad_draft',
|
|
1400
|
+
'cad_pattern',
|
|
1401
|
+
'cad_loft',
|
|
1402
|
+
'cad_sweep',
|
|
806
1403
|
'cad_boolean',
|
|
807
1404
|
'cad_fillet',
|
|
808
1405
|
'cad_transform',
|
|
@@ -813,4 +1410,12 @@ export const MODEL_TOOL_NAMES = [
|
|
|
813
1410
|
'cad_assembly_insert',
|
|
814
1411
|
'cad_assembly_move',
|
|
815
1412
|
'cad_assembly_remove',
|
|
1413
|
+
'cad_docs',
|
|
1414
|
+
'cad_doc_new',
|
|
1415
|
+
'cad_doc_open',
|
|
1416
|
+
'cad_doc_rename',
|
|
1417
|
+
'cad_doc_delete',
|
|
1418
|
+
'cad_constraint',
|
|
1419
|
+
'cad_solve',
|
|
1420
|
+
'cad_motion',
|
|
816
1421
|
];
|