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
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,228 @@ 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 cadPattern = defineTool({
|
|
601
|
+
name: 'cad_pattern',
|
|
602
|
+
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.',
|
|
603
|
+
parameters: {
|
|
604
|
+
target: bodyTarget,
|
|
605
|
+
mode: { type: 'string', required: true, enum: ['linear', 'circular'], description: 'Pattern mode.' },
|
|
606
|
+
count: { type: 'number', required: true, description: 'Total placements (≥2).' },
|
|
607
|
+
delta: { type: 'array', items: { type: 'number' }, description: 'linear: spacing [dx,dy,dz] per copy (mm).' },
|
|
608
|
+
axis: { type: 'array', items: { type: 'number' }, description: 'circular: principal axis [x,y,z] (default +Z).' },
|
|
609
|
+
at: { type: 'array', items: { type: 'number' }, description: 'circular: axis point [x,y,z] (default origin).' },
|
|
610
|
+
angle: { type: 'number', description: 'circular: total sweep in degrees (default 360).' },
|
|
611
|
+
},
|
|
612
|
+
output: {
|
|
613
|
+
schema: {
|
|
614
|
+
type: 'object',
|
|
615
|
+
additionalProperties: false,
|
|
616
|
+
properties: {
|
|
617
|
+
bodyId: { type: 'string', required: true },
|
|
618
|
+
created: { type: 'array', items: { type: 'string' }, required: true, description: 'Ids of the newly created copy bodies.' },
|
|
619
|
+
...requiredCounts,
|
|
620
|
+
...commonOptional,
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) + '\ncreated: ' + value.created.join(', ') }],
|
|
624
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
625
|
+
},
|
|
626
|
+
isConcurrencySafe: () => false,
|
|
627
|
+
async execute(args, exec) {
|
|
628
|
+
await resolveDoc(exec);
|
|
629
|
+
if (args.mode === 'linear' && !Array.isArray(args.delta))
|
|
630
|
+
throw new Error('linear patterns need delta: [dx,dy,dz]');
|
|
631
|
+
const op = {
|
|
632
|
+
kind: 'pattern', target: args.target, mode: args.mode, count: args.count,
|
|
633
|
+
...(Array.isArray(args.delta) ? { delta: args.delta } : {}),
|
|
634
|
+
...(Array.isArray(args.axis) ? { axis: args.axis } : {}),
|
|
635
|
+
...(Array.isArray(args.at) ? { at: args.at } : {}),
|
|
636
|
+
...(args.angle !== undefined ? { angle: args.angle } : {}),
|
|
637
|
+
};
|
|
638
|
+
const result = await runModelOp(op);
|
|
639
|
+
const created = (result.created ?? []).map((entry) => {
|
|
640
|
+
if (entry.mesh !== undefined)
|
|
641
|
+
meshCache.set(entry.bodyId, mirrorMesh(entry.mesh, entry.bodyId));
|
|
642
|
+
return entry.bodyId;
|
|
643
|
+
});
|
|
644
|
+
await document.record(op, null);
|
|
645
|
+
const meshes = [...meshCache.values()];
|
|
646
|
+
const sceneUrlBase = deps.ensureSceneRoute();
|
|
647
|
+
if (sceneUrlBase !== null && meshes.length > 0) {
|
|
648
|
+
await deps.store.publish(document.doc.docId, meshes);
|
|
649
|
+
}
|
|
650
|
+
const value = {
|
|
651
|
+
bodyId: args.target,
|
|
652
|
+
created,
|
|
653
|
+
triangles: meshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0),
|
|
654
|
+
bodies: meshes.length,
|
|
655
|
+
version: document.doc.version,
|
|
656
|
+
...(sceneUrlBase !== null ? { sceneUrl: `${sceneUrlBase.replace('/scene', '/bin')}/${document.doc.docId}?v=${document.doc.version}` } : {}),
|
|
657
|
+
};
|
|
658
|
+
return value;
|
|
659
|
+
},
|
|
660
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD ${String(args.mode ?? '')} pattern ×${String(args.count ?? '')}`, kind: 'other' }),
|
|
661
|
+
presentResult: () => ({ card: 'generic', title: 'CAD pattern' }),
|
|
662
|
+
});
|
|
378
663
|
const cadBoolean = defineTool({
|
|
379
664
|
name: 'cad_boolean',
|
|
380
665
|
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 +682,8 @@ export function createModelTools(deps) {
|
|
|
397
682
|
presentationMeta: (_args, value) => metaOf(value),
|
|
398
683
|
},
|
|
399
684
|
isConcurrencySafe: () => false,
|
|
400
|
-
async execute(args) {
|
|
401
|
-
await
|
|
685
|
+
async execute(args, exec) {
|
|
686
|
+
await resolveDoc(exec);
|
|
402
687
|
const op = { kind: 'boolean', op: args.op, target: args.target, tools: args.tools };
|
|
403
688
|
const result = await runModelOp(op);
|
|
404
689
|
return syncScene(op, result);
|
|
@@ -427,8 +712,8 @@ export function createModelTools(deps) {
|
|
|
427
712
|
presentationMeta: (_args, value) => metaOf(value),
|
|
428
713
|
},
|
|
429
714
|
isConcurrencySafe: () => false,
|
|
430
|
-
async execute(args) {
|
|
431
|
-
await
|
|
715
|
+
async execute(args, exec) {
|
|
716
|
+
await resolveDoc(exec);
|
|
432
717
|
const op = { kind: 'fillet', target: args.target, radius: args.radius };
|
|
433
718
|
const result = await runModelOp(op);
|
|
434
719
|
return syncScene(op, result);
|
|
@@ -459,8 +744,8 @@ export function createModelTools(deps) {
|
|
|
459
744
|
presentationMeta: (_args, value) => metaOf(value),
|
|
460
745
|
},
|
|
461
746
|
isConcurrencySafe: () => false,
|
|
462
|
-
async execute(args) {
|
|
463
|
-
await
|
|
747
|
+
async execute(args, exec) {
|
|
748
|
+
await resolveDoc(exec);
|
|
464
749
|
const op = {
|
|
465
750
|
kind: 'transform',
|
|
466
751
|
target: args.target,
|
|
@@ -514,8 +799,8 @@ export function createModelTools(deps) {
|
|
|
514
799
|
},
|
|
515
800
|
},
|
|
516
801
|
isConcurrencySafe: () => true,
|
|
517
|
-
async execute(args) {
|
|
518
|
-
await
|
|
802
|
+
async execute(args, exec) {
|
|
803
|
+
await resolveDoc(exec);
|
|
519
804
|
const resolved = resolveWorkspacePath(args.path, deps.workspaceRoot);
|
|
520
805
|
const lower = args.path.toLowerCase();
|
|
521
806
|
// Assembly export: compound of transformed instances via the worker.
|
|
@@ -611,8 +896,8 @@ export function createModelTools(deps) {
|
|
|
611
896
|
presentationMeta: (_args, value) => metaOf(value),
|
|
612
897
|
},
|
|
613
898
|
isConcurrencySafe: () => false,
|
|
614
|
-
async execute(args) {
|
|
615
|
-
await
|
|
899
|
+
async execute(args, exec) {
|
|
900
|
+
await resolveDoc(exec);
|
|
616
901
|
const op = { kind: 'delete', target: args.target };
|
|
617
902
|
const result = await runModelOp(op);
|
|
618
903
|
return syncScene(op, result);
|
|
@@ -640,8 +925,8 @@ export function createModelTools(deps) {
|
|
|
640
925
|
presentationMeta: (_args, value) => metaOf(value),
|
|
641
926
|
},
|
|
642
927
|
isConcurrencySafe: () => true,
|
|
643
|
-
async execute(args) {
|
|
644
|
-
await
|
|
928
|
+
async execute(args, exec) {
|
|
929
|
+
await resolveDoc(exec);
|
|
645
930
|
const op = { kind: 'volume', target: args.target };
|
|
646
931
|
const result = await runModelOp(op);
|
|
647
932
|
return syncScene(op, result);
|
|
@@ -668,8 +953,8 @@ export function createModelTools(deps) {
|
|
|
668
953
|
presentationMeta: (_args, value) => drawingMetaOf(value),
|
|
669
954
|
},
|
|
670
955
|
isConcurrencySafe: () => false,
|
|
671
|
-
async execute(args) {
|
|
672
|
-
await
|
|
956
|
+
async execute(args, exec) {
|
|
957
|
+
await resolveDoc(exec);
|
|
673
958
|
const target = args.target ?? lastBodyId();
|
|
674
959
|
if (target === undefined)
|
|
675
960
|
throw new Error('nothing to draw — create a body first');
|
|
@@ -717,8 +1002,8 @@ export function createModelTools(deps) {
|
|
|
717
1002
|
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
718
1003
|
},
|
|
719
1004
|
isConcurrencySafe: () => false,
|
|
720
|
-
async execute(args) {
|
|
721
|
-
await
|
|
1005
|
+
async execute(args, exec) {
|
|
1006
|
+
await resolveDoc(exec);
|
|
722
1007
|
if (!meshCache.has(args.bodyId))
|
|
723
1008
|
throw new Error(`unknown body: ${args.bodyId}`);
|
|
724
1009
|
const instanceId = nextInstanceId();
|
|
@@ -750,8 +1035,8 @@ export function createModelTools(deps) {
|
|
|
750
1035
|
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
751
1036
|
},
|
|
752
1037
|
isConcurrencySafe: () => false,
|
|
753
|
-
async execute(args) {
|
|
754
|
-
await
|
|
1038
|
+
async execute(args, exec) {
|
|
1039
|
+
await resolveDoc(exec);
|
|
755
1040
|
const op = {
|
|
756
1041
|
kind: 'assembly_transform',
|
|
757
1042
|
instanceId: args.instanceId,
|
|
@@ -776,8 +1061,8 @@ export function createModelTools(deps) {
|
|
|
776
1061
|
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
777
1062
|
},
|
|
778
1063
|
isConcurrencySafe: () => false,
|
|
779
|
-
async execute(args) {
|
|
780
|
-
await
|
|
1064
|
+
async execute(args, exec) {
|
|
1065
|
+
await resolveDoc(exec);
|
|
781
1066
|
const op = { kind: 'assembly_remove', instanceId: args.instanceId };
|
|
782
1067
|
const result = await runModelOp(op);
|
|
783
1068
|
return syncAssembly(op, result);
|
|
@@ -785,9 +1070,242 @@ export function createModelTools(deps) {
|
|
|
785
1070
|
presentCall: (args) => ({ card: 'generic', title: `移除 ${String(args.instanceId)}`, kind: 'delete' }),
|
|
786
1071
|
presentResult: () => ({ card: 'generic', title: '装配移除' }),
|
|
787
1072
|
});
|
|
1073
|
+
// ── document (file space) tools ───────────────────────────────────────────
|
|
1074
|
+
const docRefParam = { type: 'string', required: true, description: 'Document id or exact name (list with cad_docs).' };
|
|
1075
|
+
const renderDocs = (value) => {
|
|
1076
|
+
const docs = Array.isArray(value.docs) ? value.docs : [];
|
|
1077
|
+
const active = typeof value.activeDoc === 'string' ? value.activeDoc : '';
|
|
1078
|
+
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)}`);
|
|
1079
|
+
return lines.length > 0 ? ['工作区文档:', ...lines].join('\n') : '工作区还没有建模文档';
|
|
1080
|
+
};
|
|
1081
|
+
const cadDocs = defineTool({
|
|
1082
|
+
name: 'cad_docs',
|
|
1083
|
+
description: 'List the workspace modeling documents (the file space): id, name, body count and update time, marking the one active for this session. ' +
|
|
1084
|
+
'Call before cad_doc_open / cad_doc_delete to discover ids and names.',
|
|
1085
|
+
parameters: {},
|
|
1086
|
+
output: {
|
|
1087
|
+
schema: {
|
|
1088
|
+
type: 'object',
|
|
1089
|
+
additionalProperties: false,
|
|
1090
|
+
properties: {
|
|
1091
|
+
docs: {
|
|
1092
|
+
type: 'array',
|
|
1093
|
+
required: true,
|
|
1094
|
+
items: {
|
|
1095
|
+
type: 'object',
|
|
1096
|
+
additionalProperties: false,
|
|
1097
|
+
properties: {
|
|
1098
|
+
id: { type: 'string', required: true },
|
|
1099
|
+
name: { type: 'string', required: true },
|
|
1100
|
+
bodies: { type: 'number', required: true },
|
|
1101
|
+
ops: { type: 'number', required: true },
|
|
1102
|
+
updatedAt: { type: 'string', required: true },
|
|
1103
|
+
active: { type: 'boolean' },
|
|
1104
|
+
},
|
|
1105
|
+
},
|
|
1106
|
+
},
|
|
1107
|
+
activeDoc: { type: 'string', description: 'Active document id for this session (absent: none).' },
|
|
1108
|
+
},
|
|
1109
|
+
},
|
|
1110
|
+
render: (_args, value) => [{ type: 'text', text: renderDocs(value) }],
|
|
1111
|
+
},
|
|
1112
|
+
isConcurrencySafe: () => true,
|
|
1113
|
+
async execute(_args, exec) {
|
|
1114
|
+
const sessionId = sessionKeyOf(exec);
|
|
1115
|
+
const docs = await deps.registry.list();
|
|
1116
|
+
const activeDoc = await deps.registry.bindingOf(sessionId);
|
|
1117
|
+
return {
|
|
1118
|
+
docs: docs.map((doc) => ({ id: doc.id, name: doc.name, bodies: doc.bodyCount, ops: doc.opCount, updatedAt: doc.updatedAt, ...(doc.id === activeDoc ? { active: true } : {}) })),
|
|
1119
|
+
...(activeDoc === null ? {} : { activeDoc }),
|
|
1120
|
+
};
|
|
1121
|
+
},
|
|
1122
|
+
presentCall: () => ({ card: 'generic', title: '文档列表', kind: 'read' }),
|
|
1123
|
+
presentResult: () => ({ card: 'generic', title: '文档列表' }),
|
|
1124
|
+
});
|
|
1125
|
+
const cadDocNew = defineTool({
|
|
1126
|
+
name: 'cad_doc_new',
|
|
1127
|
+
description: 'Create a new modeling document and make it this session\'s active modeling target (previous documents stay in the file space). ' +
|
|
1128
|
+
'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.',
|
|
1129
|
+
parameters: {
|
|
1130
|
+
name: { type: 'string', description: 'Document name (default: 未命名 N).' },
|
|
1131
|
+
},
|
|
1132
|
+
output: {
|
|
1133
|
+
schema: {
|
|
1134
|
+
type: 'object',
|
|
1135
|
+
additionalProperties: false,
|
|
1136
|
+
properties: {
|
|
1137
|
+
docId: { type: 'string', required: true, description: 'New document id.' },
|
|
1138
|
+
name: { type: 'string', required: true, description: 'Document name.' },
|
|
1139
|
+
bodies: { type: 'number', required: true, description: 'Bodies (0 for a fresh document).' },
|
|
1140
|
+
version: { type: 'number', required: true, description: 'Document version.' },
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
render: (_args, value) => [{ type: 'text', text: `新建文档 ${String(value.name)}(后续建模操作都写入该文档)` }],
|
|
1144
|
+
},
|
|
1145
|
+
isConcurrencySafe: () => false,
|
|
1146
|
+
async execute(args, exec) {
|
|
1147
|
+
const sessionId = sessionKeyOf(exec);
|
|
1148
|
+
const doc = await deps.registry.create(args.name);
|
|
1149
|
+
await deps.registry.bind(sessionId, doc.doc.docId);
|
|
1150
|
+
await activateDocument(doc);
|
|
1151
|
+
const name = (await deps.registry.list()).find((meta) => meta.id === doc.doc.docId)?.name ?? doc.doc.docId;
|
|
1152
|
+
return { docId: doc.doc.docId, name, bodies: 0, version: doc.doc.version };
|
|
1153
|
+
},
|
|
1154
|
+
presentCall: (args) => ({ card: 'generic', title: `新建文档 ${String(args.name ?? '')}`.trim(), kind: 'other' }),
|
|
1155
|
+
presentResult: () => ({ card: 'generic', title: '新建文档' }),
|
|
1156
|
+
});
|
|
1157
|
+
const cadDocOpen = defineTool({
|
|
1158
|
+
name: 'cad_doc_open',
|
|
1159
|
+
description: 'Open an existing modeling document and make it this session\'s active modeling target (its bodies load back exactly). ' +
|
|
1160
|
+
'Use when the user asks to continue an earlier model (e.g. “打开发动机文档”) — resolve the id/name with cad_docs first when unsure.',
|
|
1161
|
+
parameters: {
|
|
1162
|
+
doc: docRefParam,
|
|
1163
|
+
},
|
|
1164
|
+
output: {
|
|
1165
|
+
schema: {
|
|
1166
|
+
type: 'object',
|
|
1167
|
+
additionalProperties: false,
|
|
1168
|
+
properties: {
|
|
1169
|
+
docId: { type: 'string', required: true, description: 'Opened document id.' },
|
|
1170
|
+
name: { type: 'string', required: true, description: 'Document name.' },
|
|
1171
|
+
bodies: { type: 'number', required: true, description: 'Bodies in the document.' },
|
|
1172
|
+
triangles: { type: 'number', required: true, description: 'Document triangle count.' },
|
|
1173
|
+
version: { type: 'number', required: true, description: 'Document version.' },
|
|
1174
|
+
sceneUrl: { type: 'string', description: 'Versioned viewer URL (web compositions).' },
|
|
1175
|
+
},
|
|
1176
|
+
},
|
|
1177
|
+
render: (_args, value) => [{ type: 'text', text: `打开文档 ${String(value.name)} · ${Number(value.bodies)} 体 (version ${Number(value.version)})` }],
|
|
1178
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
1179
|
+
},
|
|
1180
|
+
isConcurrencySafe: () => false,
|
|
1181
|
+
async execute(args, exec) {
|
|
1182
|
+
const sessionId = sessionKeyOf(exec);
|
|
1183
|
+
const meta = await deps.registry.resolve(args.doc);
|
|
1184
|
+
if (meta === null) {
|
|
1185
|
+
const available = (await deps.registry.list()).map((doc) => doc.name).join(' / ');
|
|
1186
|
+
throw new Error(`no document matches "${String(args.doc)}" — available: ${available === '' ? '(none)' : available}`);
|
|
1187
|
+
}
|
|
1188
|
+
await deps.registry.bind(sessionId, meta.id);
|
|
1189
|
+
const doc = await deps.registry.open(meta.id);
|
|
1190
|
+
if (doc === null)
|
|
1191
|
+
throw new Error(`document file missing: ${meta.id}`);
|
|
1192
|
+
await activateDocument(doc);
|
|
1193
|
+
const meshes = [...meshCache.values()];
|
|
1194
|
+
const triangles = meshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0);
|
|
1195
|
+
const sceneUrlBase = deps.ensureSceneRoute();
|
|
1196
|
+
const value = {
|
|
1197
|
+
docId: meta.id,
|
|
1198
|
+
name: meta.name,
|
|
1199
|
+
bodies: meshes.length,
|
|
1200
|
+
triangles,
|
|
1201
|
+
version: document.doc.version,
|
|
1202
|
+
};
|
|
1203
|
+
if (meshes.length > 0)
|
|
1204
|
+
await deps.store.publish(meta.id, meshes);
|
|
1205
|
+
if (meshes.length > 0 && sceneUrlBase !== null) {
|
|
1206
|
+
value.sceneUrl = `${sceneUrlBase.replace('/scene', '/bin')}/${meta.id}?v=${document.doc.version}`;
|
|
1207
|
+
}
|
|
1208
|
+
return value;
|
|
1209
|
+
},
|
|
1210
|
+
presentCall: (args) => ({ card: 'generic', title: `打开文档 ${String(args.doc)}`, kind: 'other' }),
|
|
1211
|
+
presentResult: () => ({ card: 'generic', title: '打开文档' }),
|
|
1212
|
+
});
|
|
1213
|
+
const cadDocRename = defineTool({
|
|
1214
|
+
name: 'cad_doc_rename',
|
|
1215
|
+
description: 'Rename a modeling document in the file space.',
|
|
1216
|
+
parameters: {
|
|
1217
|
+
doc: docRefParam,
|
|
1218
|
+
name: { type: 'string', required: true, description: 'New document name.' },
|
|
1219
|
+
},
|
|
1220
|
+
output: {
|
|
1221
|
+
schema: {
|
|
1222
|
+
type: 'object',
|
|
1223
|
+
additionalProperties: false,
|
|
1224
|
+
properties: {
|
|
1225
|
+
docId: { type: 'string', required: true, description: 'Document id.' },
|
|
1226
|
+
name: { type: 'string', required: true, description: 'New name.' },
|
|
1227
|
+
},
|
|
1228
|
+
},
|
|
1229
|
+
render: (_args, value) => [{ type: 'text', text: `文档已重命名为 ${String(value.name)}` }],
|
|
1230
|
+
},
|
|
1231
|
+
isConcurrencySafe: () => false,
|
|
1232
|
+
async execute(args, exec) {
|
|
1233
|
+
void exec;
|
|
1234
|
+
const meta = await deps.registry.resolve(args.doc);
|
|
1235
|
+
if (meta === null)
|
|
1236
|
+
throw new Error(`no document matches "${String(args.doc)}"`);
|
|
1237
|
+
const renamed = await deps.registry.rename(meta.id, args.name);
|
|
1238
|
+
if (renamed === null)
|
|
1239
|
+
throw new Error(`rename failed: ${meta.id}`);
|
|
1240
|
+
return { docId: renamed.id, name: renamed.name };
|
|
1241
|
+
},
|
|
1242
|
+
presentCall: (args) => ({ card: 'generic', title: `重命名文档 ${String(args.doc)}`, kind: 'other' }),
|
|
1243
|
+
presentResult: () => ({ card: 'generic', title: '重命名文档' }),
|
|
1244
|
+
});
|
|
1245
|
+
const cadDocDelete = defineTool({
|
|
1246
|
+
name: 'cad_doc_delete',
|
|
1247
|
+
description: 'Permanently delete a modeling document from the file space (op log + bodies). Requires confirm=true — ask the user before calling. ' +
|
|
1248
|
+
'To delete a single body inside a document, use cad_delete instead.',
|
|
1249
|
+
parameters: {
|
|
1250
|
+
doc: docRefParam,
|
|
1251
|
+
confirm: { type: 'boolean', required: true, description: 'Must be explicitly true to delete.' },
|
|
1252
|
+
},
|
|
1253
|
+
output: {
|
|
1254
|
+
schema: {
|
|
1255
|
+
type: 'object',
|
|
1256
|
+
additionalProperties: false,
|
|
1257
|
+
properties: {
|
|
1258
|
+
deleted: { type: 'string', required: true, description: 'Deleted document id.' },
|
|
1259
|
+
name: { type: 'string', required: true, description: 'Deleted document name.' },
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
render: (_args, value) => [{ type: 'text', text: `已删除文档 ${String(value.name)}` }],
|
|
1263
|
+
},
|
|
1264
|
+
isConcurrencySafe: () => false,
|
|
1265
|
+
async execute(args, exec) {
|
|
1266
|
+
void exec;
|
|
1267
|
+
if (args.confirm !== true) {
|
|
1268
|
+
throw new Error('pass confirm=true to delete — this permanently removes the document and its bodies');
|
|
1269
|
+
}
|
|
1270
|
+
const meta = await deps.registry.resolve(args.doc);
|
|
1271
|
+
if (meta === null)
|
|
1272
|
+
throw new Error(`no document matches "${String(args.doc)}"`);
|
|
1273
|
+
const removed = await deps.registry.remove(meta.id);
|
|
1274
|
+
if (!removed)
|
|
1275
|
+
throw new Error(`delete failed: ${meta.id}`);
|
|
1276
|
+
// Drop the active-document state if it was the deleted one; the next
|
|
1277
|
+
// modeling op lazily creates a fresh document for its session.
|
|
1278
|
+
if (document.doc.docId === meta.id) {
|
|
1279
|
+
document = new ModelDocument(deps.workspaceRoot);
|
|
1280
|
+
meshCache.clear();
|
|
1281
|
+
drawingSheets = new Map();
|
|
1282
|
+
lastDrawingId = null;
|
|
1283
|
+
syncedEpoch = -1;
|
|
1284
|
+
}
|
|
1285
|
+
return { deleted: meta.id, name: meta.name };
|
|
1286
|
+
},
|
|
1287
|
+
presentCall: (args) => ({ card: 'generic', title: `删除文档 ${String(args.doc)}`, kind: 'delete' }),
|
|
1288
|
+
presentResult: () => ({ card: 'generic', title: '删除文档' }),
|
|
1289
|
+
});
|
|
1290
|
+
// ── constraint tools (Ansatz solver) ──────────────────────────────────────
|
|
1291
|
+
const constraintTools = createConstraintTools({
|
|
1292
|
+
deps,
|
|
1293
|
+
// Read the ACTIVE document per call: multi-document sessions swap it.
|
|
1294
|
+
getDocument: () => document,
|
|
1295
|
+
meshCache,
|
|
1296
|
+
constraintState,
|
|
1297
|
+
resolveDoc,
|
|
1298
|
+
syncAssembly,
|
|
1299
|
+
assemblyMetaOf,
|
|
1300
|
+
});
|
|
788
1301
|
return [
|
|
789
1302
|
cadCreatePrim,
|
|
790
1303
|
cadExtrude,
|
|
1304
|
+
cadRevolve,
|
|
1305
|
+
cadChamfer,
|
|
1306
|
+
cadPattern,
|
|
1307
|
+
cadLoft,
|
|
1308
|
+
cadSweep,
|
|
791
1309
|
cadBoolean,
|
|
792
1310
|
cadFillet,
|
|
793
1311
|
cadTransform,
|
|
@@ -798,11 +1316,22 @@ export function createModelTools(deps) {
|
|
|
798
1316
|
cadAssemblyInsert,
|
|
799
1317
|
cadAssemblyMove,
|
|
800
1318
|
cadAssemblyRemove,
|
|
1319
|
+
cadDocs,
|
|
1320
|
+
cadDocNew,
|
|
1321
|
+
cadDocOpen,
|
|
1322
|
+
cadDocRename,
|
|
1323
|
+
cadDocDelete,
|
|
1324
|
+
...constraintTools,
|
|
801
1325
|
];
|
|
802
1326
|
}
|
|
803
1327
|
export const MODEL_TOOL_NAMES = [
|
|
804
1328
|
'cad_create_prim',
|
|
805
1329
|
'cad_extrude_profile',
|
|
1330
|
+
'cad_revolve',
|
|
1331
|
+
'cad_chamfer',
|
|
1332
|
+
'cad_pattern',
|
|
1333
|
+
'cad_loft',
|
|
1334
|
+
'cad_sweep',
|
|
806
1335
|
'cad_boolean',
|
|
807
1336
|
'cad_fillet',
|
|
808
1337
|
'cad_transform',
|
|
@@ -813,4 +1342,12 @@ export const MODEL_TOOL_NAMES = [
|
|
|
813
1342
|
'cad_assembly_insert',
|
|
814
1343
|
'cad_assembly_move',
|
|
815
1344
|
'cad_assembly_remove',
|
|
1345
|
+
'cad_docs',
|
|
1346
|
+
'cad_doc_new',
|
|
1347
|
+
'cad_doc_open',
|
|
1348
|
+
'cad_doc_rename',
|
|
1349
|
+
'cad_doc_delete',
|
|
1350
|
+
'cad_constraint',
|
|
1351
|
+
'cad_solve',
|
|
1352
|
+
'cad_motion',
|
|
816
1353
|
];
|