dsh-cad 0.16.2 → 0.17.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 +9 -0
- package/README.zh-CN.md +9 -0
- package/lib/client.js +31 -31
- package/lib/client.js.map +3 -3
- package/lib/modeling/assembly.js +318 -215
- package/lib/modeling/cadorange-adapter.cjs +75 -0
- package/lib/modeling/cadorange-bridge.cjs +10 -0
- package/lib/modeling/joints.js +442 -0
- package/lib/modeling/modeling-worker.cjs +88 -0
- package/lib/tools/cad-model.js +2282 -1797
- package/lib/types/modeling/assembly.d.ts +106 -78
- package/lib/types/modeling/client.d.ts +279 -227
- package/lib/types/modeling/joints.d.ts +129 -0
- package/lib/types/tools/cad-model.d.ts +16 -16
- package/package.json +2 -2
package/lib/tools/cad-model.js
CHANGED
|
@@ -1,1797 +1,2282 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
]
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
};
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const
|
|
661
|
-
if (
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
},
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
},
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
},
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
},
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
},
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
},
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
return
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
return value;
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
},
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
},
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
},
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
instanceId
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
await
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
},
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
const
|
|
1622
|
-
if (
|
|
1623
|
-
throw new Error(`
|
|
1624
|
-
|
|
1625
|
-
const
|
|
1626
|
-
const
|
|
1627
|
-
const
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
const
|
|
1670
|
-
if (
|
|
1671
|
-
throw new Error(`
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The native modeling tool family (phase 2). Every tool mutates the shared
|
|
3
|
+
* workspace modeling document, then refreshes the same viewer card through a
|
|
4
|
+
* stable viewId and a version-parameterized scene URL.
|
|
5
|
+
*
|
|
6
|
+
* Schema notes: every optional field syncScene may emit (`name`, `removed`,
|
|
7
|
+
* `volume`, `filePath`, `sceneUrl`) is declared on every tool
|
|
8
|
+
* (additionalProperties:false rejects undeclared keys), and execute returns
|
|
9
|
+
* only defined keys (an explicit undefined fails the registry's
|
|
10
|
+
* lossless-JSON validation).
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
16
|
+
import { runModelOp, workerResetEpoch } from '../modeling/client.js';
|
|
17
|
+
import { ModelDocument } from '../modeling/document.js';
|
|
18
|
+
import { composeAssemblyMeshes, foldAssemblyState } from '../modeling/assembly.js';
|
|
19
|
+
import { buildDrawingSheet, drawingToDxf, drawingToSvg } from '../modeling/drawing.js';
|
|
20
|
+
import { loadCadFile, resolveSessionPath } from './util.js';
|
|
21
|
+
import { applyConnectValue, checkPairing, explodeOffsets, jointValueText, mergeJointValue, normalizeJointSpec, solveConnection, solveConnections, } from '../modeling/joints.js';
|
|
22
|
+
import { toDcPrtDocument } from '../feature_script/dc_prt.js';
|
|
23
|
+
import { createConstraintTools } from './cad-constraint.js';
|
|
24
|
+
/** Mirror a worker mesh with its raw typed arrays (binary-transport ready). */
|
|
25
|
+
function mirrorMesh(mesh, bodyId) {
|
|
26
|
+
return {
|
|
27
|
+
name: mesh.name === '' ? bodyId : mesh.name,
|
|
28
|
+
positions: mesh.positions,
|
|
29
|
+
normals: mesh.normals,
|
|
30
|
+
indices: mesh.indices,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Build the whole tool family over the document registry + worker + stores. */
|
|
34
|
+
export function createModelTools(deps) {
|
|
35
|
+
// The active document and its derived caches are swapped as a unit whenever
|
|
36
|
+
// a session's binding points elsewhere (see resolveDoc). meshCache /
|
|
37
|
+
// drawingSheets are cleared and rebuilt by replay.
|
|
38
|
+
let document = new ModelDocument(deps.workspaceRoot);
|
|
39
|
+
/** bodyId → raw worker mesh mirror (binary scene source, zero encoding). */
|
|
40
|
+
const meshCache = new Map();
|
|
41
|
+
/** sketchName → display payload { wire, points, fill } (repopulated by replay). */
|
|
42
|
+
const sketchWires = new Map();
|
|
43
|
+
/** Reset epoch after the last sync — out-of-band replays (cad_view on a
|
|
44
|
+
* .dcprt) bump the epoch and force a re-replay before the next op. */
|
|
45
|
+
let syncedEpoch = -1;
|
|
46
|
+
/** drawingId → rebuilt sheet (backing cad_export .svg/.dxf). */
|
|
47
|
+
let drawingSheets = new Map();
|
|
48
|
+
let lastDrawingId = null;
|
|
49
|
+
/** Live constraint-model state (rebuilt from the op log on replay). */
|
|
50
|
+
const constraintState = { model: null, lastSuggestions: [] };
|
|
51
|
+
/**
|
|
52
|
+
* Standard drawing views (GB first-angle): 主视图 front, 俯视图 top,
|
|
53
|
+
* 左视图 left, 轴测 iso. Same constants as the worker's fallback.
|
|
54
|
+
*/
|
|
55
|
+
const DRAWING_VIEWS = [
|
|
56
|
+
{ name: 'front', dir: [0, -1, 0], xDir: [1, 0, 0] },
|
|
57
|
+
{ name: 'top', dir: [0, 0, 1], xDir: [1, 0, 0] },
|
|
58
|
+
{ name: 'left', dir: [-1, 0, 0], xDir: [0, 1, 0] },
|
|
59
|
+
{ name: 'iso', dir: [1, -1, 1], xDir: [1, 1, 0] },
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* A second stable UUID derived from the document id (hex reversed + salted
|
|
63
|
+
* version nibble): SceneStore's viewId regex demands UUID shape, while the
|
|
64
|
+
* drawing scene must keep one stable viewId across re-draws/replays.
|
|
65
|
+
*/
|
|
66
|
+
const siblingUuid = (salt) => {
|
|
67
|
+
const hex = document.doc.docId.replace(/-/g, '').split('').reverse().join('');
|
|
68
|
+
const salted = (hex.slice(0, 12) + salt + hex.slice(13)).slice(0, 32);
|
|
69
|
+
return `${salted.slice(0, 8)}-${salted.slice(8, 12)}-${salted.slice(12, 16)}-${salted.slice(16, 20)}-${salted.slice(20, 32)}`;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Replay a document's ops into the worker and rebuild the derived caches
|
|
73
|
+
* (mesh mirror, drawing sheets, constraint model). The worker is always
|
|
74
|
+
* reset first — also for empty documents, so a previous document's shapes
|
|
75
|
+
* never leak in. Absorbs the old restoreOnce body.
|
|
76
|
+
*/
|
|
77
|
+
async function replayActiveDoc() {
|
|
78
|
+
await runModelOp({ kind: 'reset' });
|
|
79
|
+
// Derived state belongs to the outgoing document: clear before replay so
|
|
80
|
+
// a document without constraints/drawings never inherits the previous one's.
|
|
81
|
+
constraintState.model = null;
|
|
82
|
+
sketchWires.clear();
|
|
83
|
+
/** Latest instances list seen while replaying (assembly re-publish below). */
|
|
84
|
+
let replayedInstances = null;
|
|
85
|
+
for (const op of document.doc.ops) {
|
|
86
|
+
try {
|
|
87
|
+
const result = await runModelOp(op);
|
|
88
|
+
if (result.instances !== undefined)
|
|
89
|
+
replayedInstances = result.instances;
|
|
90
|
+
// A replayed sketch re-discretizes its display payload (curves,
|
|
91
|
+
// vertex dots, region fill) — cad_sketch_edit's rewrite path relies
|
|
92
|
+
// on this to refresh the viewport.
|
|
93
|
+
if (op.kind === 'sketch_set' && result.wire !== undefined) {
|
|
94
|
+
sketchWires.set(op.name, { wire: result.wire, points: result.points, fill: result.fill });
|
|
95
|
+
}
|
|
96
|
+
// A replayed constraint model returns to live state.
|
|
97
|
+
if (op.kind === 'constraints') {
|
|
98
|
+
constraintState.model = op.model;
|
|
99
|
+
}
|
|
100
|
+
// A replayed drawing re-generates its sheet and re-publishes the
|
|
101
|
+
// scene so the stable viewId keeps serving after restarts.
|
|
102
|
+
if (op.kind === 'drawing' && op.sceneViewId !== undefined && result.views !== undefined) {
|
|
103
|
+
const sheet = buildDrawingSheet({ partName: op.name ?? op.target, views: result.views, paper: op.paper });
|
|
104
|
+
drawingSheets.set(op.sceneViewId, { sheet, partName: op.name ?? op.target });
|
|
105
|
+
lastDrawingId = op.sceneViewId;
|
|
106
|
+
await deps.sceneStore.putAt(op.sceneViewId, {
|
|
107
|
+
kind: '2d',
|
|
108
|
+
format: 'drawing',
|
|
109
|
+
entities: sheet.entities,
|
|
110
|
+
bounds: sheet.bounds,
|
|
111
|
+
layers: sheet.layers,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// A single stale op must not block recovery; later ops may be independent.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (document.doc.ops.length > 0) {
|
|
120
|
+
const all = await runModelOp({ kind: 'tessellate_all' });
|
|
121
|
+
meshCache.clear();
|
|
122
|
+
for (const mesh of all.meshes ?? []) {
|
|
123
|
+
meshCache.set(mesh.bodyId, mirrorMesh(mesh, mesh.bodyId));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Re-publish the assembly scene so a document opened after a restart shows
|
|
127
|
+
// the composed (per-instance colored) assembly, not the stale disk mirror.
|
|
128
|
+
if (replayedInstances !== null && deps.ensureSceneRoute() !== null) {
|
|
129
|
+
const { explode } = foldAssemblyState(document.doc.ops, document.doc.bodyNames);
|
|
130
|
+
const offsets = explodeOffsets(meshCache, replayedInstances, explode);
|
|
131
|
+
await deps.store.publish(`asm-${document.doc.docId}`, composeAssemblyMeshes(meshCache, replayedInstances, offsets.size > 0 ? offsets : undefined));
|
|
132
|
+
}
|
|
133
|
+
syncedEpoch = workerResetEpoch();
|
|
134
|
+
}
|
|
135
|
+
/** Swap the active document: restore from disk, reset the worker, replay. */
|
|
136
|
+
async function activateDocument(next) {
|
|
137
|
+
await next.restore();
|
|
138
|
+
document = next;
|
|
139
|
+
meshCache.clear();
|
|
140
|
+
drawingSheets = new Map();
|
|
141
|
+
lastDrawingId = null;
|
|
142
|
+
await replayActiveDoc();
|
|
143
|
+
}
|
|
144
|
+
/** Session key from the tool run context (`_default` outside a session). */
|
|
145
|
+
const sessionKeyOf = (exec) => {
|
|
146
|
+
const agent = exec?.agent;
|
|
147
|
+
return typeof agent?.id === 'string' && agent.id !== '' ? agent.id : '_default';
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Per-op entry point (replaces restoreOnce): resolve the calling session's
|
|
151
|
+
* active document, switching documents (reset + replay) when its binding
|
|
152
|
+
* points elsewhere, then re-sync when the worker was reset out of band.
|
|
153
|
+
*/
|
|
154
|
+
async function resolveDoc(exec) {
|
|
155
|
+
const sessionId = sessionKeyOf(exec);
|
|
156
|
+
const bound = await deps.registry.bindingOf(sessionId);
|
|
157
|
+
if (bound === null) {
|
|
158
|
+
// Upgrade continuity: the first unbound session inherits the migrated
|
|
159
|
+
// legacy document (if any) instead of starting from scratch.
|
|
160
|
+
const legacy = await deps.registry.claimLegacyFor(sessionId);
|
|
161
|
+
if (legacy !== null) {
|
|
162
|
+
const doc = await deps.registry.open(legacy);
|
|
163
|
+
if (doc !== null) {
|
|
164
|
+
await activateDocument(doc);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Unbound session (new conversation): start on a fresh empty document.
|
|
169
|
+
const fresh = await deps.registry.create();
|
|
170
|
+
await deps.registry.bind(sessionId, fresh.doc.docId);
|
|
171
|
+
await activateDocument(fresh);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (bound !== document.doc.docId) {
|
|
175
|
+
const next = await deps.registry.open(bound);
|
|
176
|
+
if (next === null) {
|
|
177
|
+
// Manifest entry without a document file — recover with a fresh doc.
|
|
178
|
+
const fresh = await deps.registry.create();
|
|
179
|
+
await deps.registry.bind(sessionId, fresh.doc.docId);
|
|
180
|
+
await activateDocument(fresh);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
await activateDocument(next);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (syncedEpoch !== workerResetEpoch()) {
|
|
187
|
+
await document.restore();
|
|
188
|
+
await replayActiveDoc();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Sketch display meshes (Onshape-style viewport curves): a translucent
|
|
193
|
+
* region fill (lighter than the lines), the blue profile curves as LINES
|
|
194
|
+
* on z=0, TRUE vertex dots at the corners/ends, and a faint gray plane
|
|
195
|
+
* frame around the bounding box. Names carry the `sketch:` prefix so the
|
|
196
|
+
* feature tree's eye toggles can address them (`sketch:<name>[:plane|:fill|:pts]`).
|
|
197
|
+
*/
|
|
198
|
+
const SKETCH_LINE_COLOR = 0x2f80d6;
|
|
199
|
+
const SKETCH_FILL_COLOR = 0x74a9e0;
|
|
200
|
+
const SKETCH_POINT_COLOR = 0x1c5fa8;
|
|
201
|
+
const SKETCH_PLANE_COLOR = 0xb9c2cc;
|
|
202
|
+
const polylineWireMesh = (name, color, flat) => {
|
|
203
|
+
const positions = new Float32Array(flat);
|
|
204
|
+
const segments = flat.length / 3 - 1;
|
|
205
|
+
const indices = new Uint32Array(segments * 2);
|
|
206
|
+
for (let i = 0, s = 0; i < segments; i++) {
|
|
207
|
+
indices[s++] = i;
|
|
208
|
+
indices[s++] = i + 1;
|
|
209
|
+
}
|
|
210
|
+
return { name, color, mode: 'wire', positions, indices };
|
|
211
|
+
};
|
|
212
|
+
const sketchWireMeshes = () => {
|
|
213
|
+
const out = [];
|
|
214
|
+
for (const [name, payload] of sketchWires) {
|
|
215
|
+
// Translucent enclosed region (kernel-triangulated face).
|
|
216
|
+
if (payload.fill !== undefined) {
|
|
217
|
+
out.push({
|
|
218
|
+
name: `sketch:${name}:fill`,
|
|
219
|
+
color: SKETCH_FILL_COLOR,
|
|
220
|
+
mode: 'fill',
|
|
221
|
+
positions: new Float32Array(payload.fill.positions),
|
|
222
|
+
indices: new Uint32Array(payload.fill.indices),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (payload.wire !== undefined) {
|
|
226
|
+
out.push(polylineWireMesh(`sketch:${name}`, SKETCH_LINE_COLOR, payload.wire));
|
|
227
|
+
}
|
|
228
|
+
// TRUE vertex dots (screen-constant size client-side).
|
|
229
|
+
if (payload.points !== undefined && payload.points.length >= 3) {
|
|
230
|
+
out.push({
|
|
231
|
+
name: `sketch:${name}:pts`,
|
|
232
|
+
color: SKETCH_POINT_COLOR,
|
|
233
|
+
mode: 'points',
|
|
234
|
+
positions: new Float32Array(payload.points),
|
|
235
|
+
indices: new Uint32Array(0),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// Plane frame: the profile bbox padded 20%, drawn as a closed rectangle.
|
|
239
|
+
if (payload.wire !== undefined) {
|
|
240
|
+
const flat = payload.wire;
|
|
241
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
242
|
+
for (let i = 0; i + 2 < flat.length; i += 3) {
|
|
243
|
+
if (flat[i] < minX)
|
|
244
|
+
minX = flat[i];
|
|
245
|
+
if (flat[i] > maxX)
|
|
246
|
+
maxX = flat[i];
|
|
247
|
+
if (flat[i + 1] < minY)
|
|
248
|
+
minY = flat[i + 1];
|
|
249
|
+
if (flat[i + 1] > maxY)
|
|
250
|
+
maxY = flat[i + 1];
|
|
251
|
+
}
|
|
252
|
+
if (Number.isFinite(minX)) {
|
|
253
|
+
const pad = Math.max(2, 0.2 * Math.max(maxX - minX, maxY - minY));
|
|
254
|
+
const frame = [
|
|
255
|
+
minX - pad, minY - pad, 0, maxX + pad, minY - pad, 0,
|
|
256
|
+
maxX + pad, maxY + pad, 0, minX - pad, maxY + pad, 0,
|
|
257
|
+
minX - pad, minY - pad, 0,
|
|
258
|
+
];
|
|
259
|
+
out.push(polylineWireMesh(`sketch:${name}:plane`, SKETCH_PLANE_COLOR, frame));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return out;
|
|
264
|
+
};
|
|
265
|
+
async function syncScene(op, result, filePath) {
|
|
266
|
+
if (result.mesh !== undefined && result.bodyId !== undefined) {
|
|
267
|
+
meshCache.set(result.bodyId, mirrorMesh(result.mesh, result.bodyId));
|
|
268
|
+
}
|
|
269
|
+
for (const mesh of result.meshes ?? []) {
|
|
270
|
+
meshCache.set(mesh.bodyId, mirrorMesh(mesh, mesh.bodyId));
|
|
271
|
+
}
|
|
272
|
+
const removed = [...(result.removed ?? []), ...(result.deleted !== undefined ? [result.deleted] : [])];
|
|
273
|
+
for (const id of removed)
|
|
274
|
+
meshCache.delete(id);
|
|
275
|
+
const nameEntry = result.bodyId !== undefined && result.name !== undefined ? { bodyId: result.bodyId, name: result.name } : null;
|
|
276
|
+
await document.record(op, nameEntry);
|
|
277
|
+
await deps.registry.touch(document.doc.docId, { opCount: document.doc.version, bodyCount: meshCache.size });
|
|
278
|
+
const meshes = [...meshCache.values()];
|
|
279
|
+
const triangles = meshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0);
|
|
280
|
+
const sceneUrlBase = deps.ensureSceneRoute();
|
|
281
|
+
// Bodies + sketch display wires ride the SAME binary scene, so a version
|
|
282
|
+
// bump (any op, cad_sketch_edit's replay included) refreshes both.
|
|
283
|
+
const publishable = [...meshes, ...sketchWireMeshes()];
|
|
284
|
+
if (publishable.length > 0) {
|
|
285
|
+
// Direct worker→three.js transport: packed binary, in-memory, no file
|
|
286
|
+
// write per step (a debounced disk mirror keeps restart replay).
|
|
287
|
+
await deps.store.publish(document.doc.docId, publishable);
|
|
288
|
+
}
|
|
289
|
+
const value = {
|
|
290
|
+
triangles,
|
|
291
|
+
bodies: meshes.length,
|
|
292
|
+
version: document.doc.version,
|
|
293
|
+
};
|
|
294
|
+
if (result.bodyId !== undefined)
|
|
295
|
+
value.bodyId = result.bodyId;
|
|
296
|
+
if (result.name !== undefined)
|
|
297
|
+
value.name = result.name;
|
|
298
|
+
if (removed.length > 0)
|
|
299
|
+
value.removed = removed;
|
|
300
|
+
if (result.volume !== undefined)
|
|
301
|
+
value.volume = result.volume;
|
|
302
|
+
if (filePath !== undefined)
|
|
303
|
+
value.filePath = filePath;
|
|
304
|
+
if (sceneUrlBase !== null) {
|
|
305
|
+
value.sceneUrl = `${sceneUrlBase.replace('/scene', '/bin')}/${document.doc.docId}?v=${document.doc.version}`;
|
|
306
|
+
}
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
const nextBodyId = () => `b${document.doc.version + 1}`;
|
|
310
|
+
const nextInstanceId = () => `a${document.doc.version + 1}`;
|
|
311
|
+
/**
|
|
312
|
+
* Assembly sync: compose instance meshes from the body cache (pure
|
|
313
|
+
* transform math), publish the packed assembly scene under the stable
|
|
314
|
+
* `asm-<docId>` viewId, and record the op. Runs instead of syncScene —
|
|
315
|
+
* body geometry is untouched by assembly ops. The composed scene honors
|
|
316
|
+
* the document's exploded-view factor (folded from the op log after the
|
|
317
|
+
* record, so the just-recorded explode op takes effect immediately).
|
|
318
|
+
*/
|
|
319
|
+
async function syncAssembly(op, result, filePath) {
|
|
320
|
+
const instances = result.instances ?? [];
|
|
321
|
+
await document.record(op, null);
|
|
322
|
+
await deps.registry.touch(document.doc.docId, { opCount: document.doc.version, bodyCount: meshCache.size });
|
|
323
|
+
const { explode } = foldAssemblyState(document.doc.ops, document.doc.bodyNames);
|
|
324
|
+
const offsets = explodeOffsets(meshCache, instances, explode);
|
|
325
|
+
const meshes = composeAssemblyMeshes(meshCache, instances, offsets.size > 0 ? offsets : undefined);
|
|
326
|
+
const sceneUrlBase = deps.ensureSceneRoute();
|
|
327
|
+
if (sceneUrlBase !== null) {
|
|
328
|
+
// Publish even when empty — removing the last instance must refresh the tab.
|
|
329
|
+
await deps.store.publish(`asm-${document.doc.docId}`, meshes);
|
|
330
|
+
}
|
|
331
|
+
const triangles = meshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0);
|
|
332
|
+
const value = {
|
|
333
|
+
instances: instances.length,
|
|
334
|
+
triangles,
|
|
335
|
+
bodies: meshCache.size,
|
|
336
|
+
version: document.doc.version,
|
|
337
|
+
viewId: `asm-${document.doc.docId}`,
|
|
338
|
+
};
|
|
339
|
+
if (result.instanceId !== undefined)
|
|
340
|
+
value.instanceId = result.instanceId;
|
|
341
|
+
if (filePath !== undefined)
|
|
342
|
+
value.filePath = filePath;
|
|
343
|
+
if (sceneUrlBase !== null) {
|
|
344
|
+
value.sceneUrl = `${sceneUrlBase.replace('/scene', '/bin')}/asm-${document.doc.docId}?v=${document.doc.version}`;
|
|
345
|
+
}
|
|
346
|
+
return value;
|
|
347
|
+
}
|
|
348
|
+
/** Build + publish the drawing sheet for a replayed/executed drawing op. */
|
|
349
|
+
async function publishDrawing(sceneViewId, partName, views, paper) {
|
|
350
|
+
const sheet = buildDrawingSheet({
|
|
351
|
+
partName,
|
|
352
|
+
views: views ?? [],
|
|
353
|
+
paper,
|
|
354
|
+
drawingNo: `DSH-${String(document.doc.version).padStart(3, '0')}`,
|
|
355
|
+
date: new Date().toISOString().slice(0, 10),
|
|
356
|
+
});
|
|
357
|
+
drawingSheets.set(sceneViewId, { sheet, partName });
|
|
358
|
+
lastDrawingId = sceneViewId;
|
|
359
|
+
await deps.sceneStore.putAt(sceneViewId, {
|
|
360
|
+
kind: '2d',
|
|
361
|
+
format: 'drawing',
|
|
362
|
+
entities: sheet.entities,
|
|
363
|
+
bounds: sheet.bounds,
|
|
364
|
+
layers: sheet.layers,
|
|
365
|
+
});
|
|
366
|
+
return sheet;
|
|
367
|
+
}
|
|
368
|
+
/** Most recently created surviving body (default drawing/export target). */
|
|
369
|
+
const lastBodyId = () => {
|
|
370
|
+
const keys = [...meshCache.keys()];
|
|
371
|
+
return keys[keys.length - 1];
|
|
372
|
+
};
|
|
373
|
+
// The model-facing render text: bodyId visibility is the load-bearing part.
|
|
374
|
+
const renderModel = (value) => {
|
|
375
|
+
const lines = [
|
|
376
|
+
value.bodyId !== undefined
|
|
377
|
+
? `${String(value.name ?? value.bodyId)} → ${String(value.bodyId)} (version ${String(value.version)}, document: ${String(value.bodies)} bodies, ${String(value.triangles)} triangles)`
|
|
378
|
+
: `document: ${String(value.bodies)} bodies, ${String(value.triangles)} triangles (version ${String(value.version)})`,
|
|
379
|
+
];
|
|
380
|
+
if (Array.isArray(value.removed) && value.removed.length > 0)
|
|
381
|
+
lines.push(`consumed bodies: ${value.removed.join(', ')}`);
|
|
382
|
+
// cad_sketch_list: the sketches are the payload — render them, or the
|
|
383
|
+
// result reads as just a body count (issue #6).
|
|
384
|
+
if (Array.isArray(value.sketches)) {
|
|
385
|
+
const sketches = value.sketches;
|
|
386
|
+
if (sketches.length === 0) {
|
|
387
|
+
lines.push('sketches: (none — create one with cad_sketch_new)');
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
for (const sketch of sketches) {
|
|
391
|
+
const size = sketch.type === 'circle'
|
|
392
|
+
? `R${String(sketch.radius)}`
|
|
393
|
+
: sketch.type === 'segments'
|
|
394
|
+
? `${String(sketch.segments)} segments`
|
|
395
|
+
: `${String(sketch.points)} points`;
|
|
396
|
+
lines.push(`sketch ${String(sketch.name)}: ${String(sketch.type)} (${size})`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (value.volume !== undefined)
|
|
401
|
+
lines.push(`volume: ${Number(value.volume).toFixed(2)} mm³`);
|
|
402
|
+
if (value.filePath !== undefined)
|
|
403
|
+
lines.push(`written: ${String(value.filePath)}`);
|
|
404
|
+
return lines.join('\n');
|
|
405
|
+
};
|
|
406
|
+
const metaOf = (value) => ({
|
|
407
|
+
viewId: document.doc.docId,
|
|
408
|
+
kind: '3d',
|
|
409
|
+
format: 'model',
|
|
410
|
+
file: 'modeling document',
|
|
411
|
+
doc: 'part',
|
|
412
|
+
...(value.sceneUrl === undefined ? {} : { sceneUrl: String(value.sceneUrl) }),
|
|
413
|
+
title: `CAD model · ${String(value.bodies)} ${value.bodies === 1 ? 'body' : 'bodies'}`,
|
|
414
|
+
stats: {
|
|
415
|
+
meshes: Number(value.bodies),
|
|
416
|
+
triangles: Number(value.triangles),
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
/** Presentation meta for assembly ops (kind 3d, doc assembly). */
|
|
420
|
+
const assemblyMetaOf = (value) => ({
|
|
421
|
+
viewId: String(value.viewId),
|
|
422
|
+
// viewId 是装配场景 id(asm-<docId>);docId 供客户端查文档级数据(特征树等)。
|
|
423
|
+
docId: document.doc.docId,
|
|
424
|
+
kind: '3d',
|
|
425
|
+
format: 'assembly',
|
|
426
|
+
file: 'assembly',
|
|
427
|
+
doc: 'assembly',
|
|
428
|
+
...(value.sceneUrl === undefined ? {} : { sceneUrl: String(value.sceneUrl) }),
|
|
429
|
+
title: `装配体 · ${String(value.instances)} 实例`,
|
|
430
|
+
stats: {
|
|
431
|
+
meshes: Number(value.instances),
|
|
432
|
+
triangles: Number(value.triangles),
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
/** Presentation meta for drawing ops (kind 2d, doc drawing). */
|
|
436
|
+
const drawingMetaOf = (value) => ({
|
|
437
|
+
viewId: String(value.drawingId),
|
|
438
|
+
// viewId 是图纸场景 id;docId 供客户端查文档级数据(特征树等)。
|
|
439
|
+
docId: document.doc.docId,
|
|
440
|
+
kind: '2d',
|
|
441
|
+
format: 'drawing',
|
|
442
|
+
file: 'drawing',
|
|
443
|
+
doc: 'drawing',
|
|
444
|
+
...(value.sceneUrl === undefined ? {} : { sceneUrl: String(value.sceneUrl) }),
|
|
445
|
+
title: `工程图 · ${String(value.name ?? value.target)}`,
|
|
446
|
+
stats: {
|
|
447
|
+
entities: Number(value.entities),
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
const renderAssembly = (value) => {
|
|
451
|
+
const lines = [
|
|
452
|
+
value.instanceId !== undefined
|
|
453
|
+
? `instance ${String(value.instanceId)} placed (assembly: ${String(value.instances)} instances, ${String(value.triangles)} triangles)`
|
|
454
|
+
: `assembly: ${String(value.instances)} instances, ${String(value.triangles)} triangles`,
|
|
455
|
+
];
|
|
456
|
+
if (value.filePath !== undefined)
|
|
457
|
+
lines.push(`written: ${String(value.filePath)}`);
|
|
458
|
+
return lines.join('\n');
|
|
459
|
+
};
|
|
460
|
+
const renderDrawing = (value) => {
|
|
461
|
+
const lines = [
|
|
462
|
+
`工程图 ${String(value.target)}: 主视图/俯视图/左视图/轴测 · OCCT 真实消隐 · 比例 ${String(value.scaleText ?? '')} · ${String(value.entities)} 实体 (version ${String(value.version)})`,
|
|
463
|
+
];
|
|
464
|
+
if (value.filePath !== undefined)
|
|
465
|
+
lines.push(`written: ${String(value.filePath)}`);
|
|
466
|
+
return lines.join('\n');
|
|
467
|
+
};
|
|
468
|
+
const numberParam = (description) => ({ type: 'number', description });
|
|
469
|
+
const pointParam = (description) => ({ type: 'array', items: { type: 'number' }, description });
|
|
470
|
+
const bodyTarget = { type: 'string', required: true, description: 'BodyId to operate on.' };
|
|
471
|
+
/** Optional fields every tool's schema declares (syncScene emits any subset). */
|
|
472
|
+
const commonOptional = {
|
|
473
|
+
name: { type: 'string', description: 'Body display name.' },
|
|
474
|
+
removed: { type: 'array', items: { type: 'string' }, description: 'Bodies consumed by a boolean.' },
|
|
475
|
+
volume: { type: 'number', description: 'Body volume in mm³.' },
|
|
476
|
+
filePath: { type: 'string', description: 'Written file path (cad_export).' },
|
|
477
|
+
sceneUrl: { type: 'string', description: 'Versioned viewer URL (web compositions).' },
|
|
478
|
+
};
|
|
479
|
+
const requiredCounts = {
|
|
480
|
+
triangles: { type: 'number', required: true, description: 'Document triangle count.' },
|
|
481
|
+
bodies: { type: 'number', required: true, description: 'Bodies in the document.' },
|
|
482
|
+
version: { type: 'number', required: true, description: 'Document version (increments per op).' },
|
|
483
|
+
};
|
|
484
|
+
/** Schema shared by the assembly tool family. */
|
|
485
|
+
const assemblySchema = {
|
|
486
|
+
instances: { type: 'number', required: true, description: 'Instances in the assembly.' },
|
|
487
|
+
instanceId: { type: 'string', description: 'Affected instance id.' },
|
|
488
|
+
viewId: { type: 'string', description: 'Stable assembly scene viewId.' },
|
|
489
|
+
triangles: { type: 'number', required: true, description: 'Assembly triangle count.' },
|
|
490
|
+
bodies: { type: 'number', required: true, description: 'Bodies in the document.' },
|
|
491
|
+
version: { type: 'number', required: true, description: 'Document version.' },
|
|
492
|
+
filePath: { type: 'string', description: 'Written file path (cad_export).' },
|
|
493
|
+
sceneUrl: { type: 'string', description: 'Versioned assembly scene URL (web compositions).' },
|
|
494
|
+
};
|
|
495
|
+
/** Schema for the drawing tool output. */
|
|
496
|
+
const drawingSchema = {
|
|
497
|
+
target: { type: 'string', required: true, description: 'Drawn bodyId.' },
|
|
498
|
+
name: { type: 'string', description: 'Drawing/part display name.' },
|
|
499
|
+
drawingId: { type: 'string', description: 'Stable drawing scene viewId.' },
|
|
500
|
+
entities: { type: 'number', required: true, description: 'Sheet entity count.' },
|
|
501
|
+
scale: { type: 'number', required: true, description: 'Standard-view scale (1 = full size).' },
|
|
502
|
+
scaleText: { type: 'string', description: 'Scale as titled text (1:2 / 2:1).' },
|
|
503
|
+
bodies: { type: 'number', required: true, description: 'Bodies in the document.' },
|
|
504
|
+
triangles: { type: 'number', required: true, description: 'Document triangle count.' },
|
|
505
|
+
version: { type: 'number', required: true, description: 'Document version.' },
|
|
506
|
+
filePath: { type: 'string', description: 'Written file path (cad_export).' },
|
|
507
|
+
sceneUrl: { type: 'string', description: 'Versioned drawing scene URL (web compositions).' },
|
|
508
|
+
};
|
|
509
|
+
const cadCreatePrim = defineTool({
|
|
510
|
+
name: 'cad_create_prim',
|
|
511
|
+
description: 'Create a parametric primitive in the shared modeling document (mm, Z-up). Kinds: box (dx,dy,dz), cylinder (radius,height), sphere (radius), cone (radius1,radius2,height), torus (majorRadius,minorRadius). ' +
|
|
512
|
+
'`at` is the anchor point with per-kind semantics: box → min corner (the solid spans at .. at+[dx,dy,dz] along +X/+Y/+Z); cylinder/cone → base-circle center (the solid extends `height` from it along `axis`); sphere/torus → geometric center. ' +
|
|
513
|
+
'`axis` orients cylinder/cone/torus (default +Z). Returns the bodyId other CAD tools reference. The viewer card updates after every call.',
|
|
514
|
+
parameters: {
|
|
515
|
+
kind: { type: 'string', required: true, enum: ['box', 'cylinder', 'sphere', 'cone', 'torus'], description: 'Primitive kind.' },
|
|
516
|
+
dx: numberParam('box: size X (mm).'),
|
|
517
|
+
dy: numberParam('box: size Y (mm).'),
|
|
518
|
+
dz: numberParam('box: size Z (mm).'),
|
|
519
|
+
radius: numberParam('cylinder/sphere: radius (mm).'),
|
|
520
|
+
radius1: numberParam('cone: base radius (mm).'),
|
|
521
|
+
radius2: numberParam('cone: top radius (mm, 0 = pointed).'),
|
|
522
|
+
height: numberParam('cylinder/cone: height (mm).'),
|
|
523
|
+
majorRadius: numberParam('torus: center radius (mm).'),
|
|
524
|
+
minorRadius: numberParam('torus: tube radius (mm).'),
|
|
525
|
+
at: pointParam('anchor [x,y,z] (mm): box = min corner, cylinder/cone = base-circle center, sphere/torus = geometric center.'),
|
|
526
|
+
axis: pointParam('axis direction [x,y,z] for cylinder/cone/torus.'),
|
|
527
|
+
name: { type: 'string', description: 'Optional display name.' },
|
|
528
|
+
},
|
|
529
|
+
output: {
|
|
530
|
+
schema: {
|
|
531
|
+
type: 'object',
|
|
532
|
+
additionalProperties: false,
|
|
533
|
+
properties: {
|
|
534
|
+
bodyId: { type: 'string', required: true, description: 'Stable body reference.' },
|
|
535
|
+
...requiredCounts,
|
|
536
|
+
...commonOptional,
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
540
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
541
|
+
},
|
|
542
|
+
isConcurrencySafe: () => false,
|
|
543
|
+
async execute(args, exec) {
|
|
544
|
+
await resolveDoc(exec);
|
|
545
|
+
const bodyId = nextBodyId();
|
|
546
|
+
const params = args;
|
|
547
|
+
const op = { kind: 'create_prim', bodyId, prim: args.kind, params, name: args.name };
|
|
548
|
+
const result = await runModelOp(op);
|
|
549
|
+
return syncScene(op, result);
|
|
550
|
+
},
|
|
551
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD create ${String(args.kind)}`, kind: 'other' }),
|
|
552
|
+
presentResult: () => ({ card: 'generic', title: 'CAD create' }),
|
|
553
|
+
});
|
|
554
|
+
// ── named sketches (Sketch1…) — standalone, editable profiles ─────────────
|
|
555
|
+
/** Feature ops referencing a named sketch (for referrer scans / edit reports). */
|
|
556
|
+
const sketchReferrers = (name) => {
|
|
557
|
+
const out = [];
|
|
558
|
+
for (const op of document.doc.ops) {
|
|
559
|
+
if ('sketch' in op && op.sketch === name)
|
|
560
|
+
out.push(op.bodyId);
|
|
561
|
+
}
|
|
562
|
+
return out;
|
|
563
|
+
};
|
|
564
|
+
/** Sketch definitions in log order (last set wins, deletes remove). */
|
|
565
|
+
const sketchesInLog = () => {
|
|
566
|
+
const byName = new Map();
|
|
567
|
+
for (const op of document.doc.ops) {
|
|
568
|
+
if (op.kind === 'sketch_set')
|
|
569
|
+
byName.set(op.name, op.profile);
|
|
570
|
+
else if (op.kind === 'sketch_delete')
|
|
571
|
+
byName.delete(op.name);
|
|
572
|
+
}
|
|
573
|
+
return [...byName].map(([name, profile]) => ({ name, profile }));
|
|
574
|
+
};
|
|
575
|
+
/** Compact profile summary for cad_sketch_list output. */
|
|
576
|
+
const sketchProfileStats = (profile) => {
|
|
577
|
+
if (Array.isArray(profile))
|
|
578
|
+
return { type: 'polygon', points: profile.length / 2 };
|
|
579
|
+
if (profile !== null && typeof profile === 'object') {
|
|
580
|
+
const record = profile;
|
|
581
|
+
if (record.circle !== undefined) {
|
|
582
|
+
return { type: 'circle', radius: record.circle.radius };
|
|
583
|
+
}
|
|
584
|
+
const segments = Array.isArray(record.segments) ? record.segments.length : 0;
|
|
585
|
+
return { type: 'segments', segments };
|
|
586
|
+
}
|
|
587
|
+
return { type: 'unknown' };
|
|
588
|
+
};
|
|
589
|
+
const cadSketchNew = defineTool({
|
|
590
|
+
name: 'cad_sketch_new',
|
|
591
|
+
description: 'Create a NAMED sketch (e.g. Sketch1) — a standalone, editable profile kept in the modeling document AND RENDERED IN THE VIEWPORT as blue curves + a plane frame (Onshape-style; refreshes on every edit, listed in the left feature tree with an eye toggle). Same profile forms as cad_extrude_profile: {start: [x,y], segments: […]} curve segments, {circle: {center, radius}}, or a legacy flat points loop. ' +
|
|
592
|
+
'Features then reference it BY NAME (cad_extrude_profile / cad_revolve / cad_sweep `sketch` param), and cad_sketch_edit redefines it with every dependent feature rebuilt — the parametric loop.',
|
|
593
|
+
parameters: {
|
|
594
|
+
profile: { type: 'json', required: true, description: 'Profile: {start:[x,y], segments:[…]} | {circle:{center:[x,y], radius}} | flat [x0,y0,…] loop.' },
|
|
595
|
+
name: { type: 'string', description: 'Sketch name (default Sketch<N>, auto-numbered).' },
|
|
596
|
+
},
|
|
597
|
+
output: {
|
|
598
|
+
schema: {
|
|
599
|
+
type: 'object',
|
|
600
|
+
additionalProperties: false,
|
|
601
|
+
properties: {
|
|
602
|
+
sketch: { type: 'string', required: true, description: 'The sketch name (as referenced by features).' },
|
|
603
|
+
type: { type: 'string', required: true, description: 'Profile form: polygon | segments | circle.' },
|
|
604
|
+
points: { type: 'number', description: 'Polygon point count.' },
|
|
605
|
+
segments: { type: 'number', description: 'Curve-segment count.' },
|
|
606
|
+
radius: { type: 'number', description: 'Circle radius (mm).' },
|
|
607
|
+
...requiredCounts,
|
|
608
|
+
...commonOptional,
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
612
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
613
|
+
},
|
|
614
|
+
isConcurrencySafe: () => false,
|
|
615
|
+
async execute(args, exec) {
|
|
616
|
+
await resolveDoc(exec);
|
|
617
|
+
const existing = new Set(sketchesInLog().map((entry) => entry.name));
|
|
618
|
+
let n = existing.size + 1;
|
|
619
|
+
while (existing.has(`Sketch${n}`))
|
|
620
|
+
n += 1;
|
|
621
|
+
const name = typeof args.name === 'string' && args.name !== '' ? args.name : `Sketch${n}`;
|
|
622
|
+
if (existing.has(name))
|
|
623
|
+
throw new Error(`sketch already exists: ${name} — use cad_sketch_edit to redefine it`);
|
|
624
|
+
const op = { kind: 'sketch_set', name, profile: args.profile };
|
|
625
|
+
const result = await runModelOp(op);
|
|
626
|
+
if (result.wire !== undefined) {
|
|
627
|
+
sketchWires.set(name, { wire: result.wire, points: result.points, fill: result.fill });
|
|
628
|
+
}
|
|
629
|
+
const value = await syncScene(op, result);
|
|
630
|
+
return { sketch: name, ...sketchProfileStats(args.profile), ...value };
|
|
631
|
+
},
|
|
632
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD sketch ${String(args.name ?? 'new')}`, kind: 'other' }),
|
|
633
|
+
presentResult: () => ({ card: 'generic', title: 'CAD sketch created' }),
|
|
634
|
+
});
|
|
635
|
+
const cadSketchEdit = defineTool({
|
|
636
|
+
name: 'cad_sketch_edit',
|
|
637
|
+
description: 'Redefine a named sketch (回改): the document op is rewritten in place and the whole log replayed, so EVERY feature referencing the sketch (extrude/revolve/sweep via their `sketch` param) rebuilds with the new profile — and the viewport\'s blue sketch curves + plane frame update to match. Use for parametric iteration: change Sketch1, the extrusion follows.',
|
|
638
|
+
parameters: {
|
|
639
|
+
name: { type: 'string', required: true, description: 'The sketch to redefine (must exist).' },
|
|
640
|
+
profile: { type: 'json', required: true, description: 'The new profile (same forms as cad_sketch_new).' },
|
|
641
|
+
},
|
|
642
|
+
output: {
|
|
643
|
+
schema: {
|
|
644
|
+
type: 'object',
|
|
645
|
+
additionalProperties: false,
|
|
646
|
+
properties: {
|
|
647
|
+
sketch: { type: 'string', required: true, description: 'The redefined sketch name.' },
|
|
648
|
+
dependents: { type: 'array', items: { type: 'string' }, description: 'BodyIds rebuilt because they reference the sketch.' },
|
|
649
|
+
...requiredCounts,
|
|
650
|
+
...commonOptional,
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
654
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
655
|
+
},
|
|
656
|
+
isConcurrencySafe: () => false,
|
|
657
|
+
async execute(args, exec) {
|
|
658
|
+
await resolveDoc(exec);
|
|
659
|
+
const name = String(args.name ?? '');
|
|
660
|
+
const dependents = sketchReferrers(name);
|
|
661
|
+
if (!(await document.rewriteSketch(name, args.profile))) {
|
|
662
|
+
throw new Error(`unknown sketch: ${name} — create it with cad_sketch_new`);
|
|
663
|
+
}
|
|
664
|
+
// The rewritten definition still precedes its consumers in the log, so
|
|
665
|
+
// the full replay recomputes every dependent feature with the new profile.
|
|
666
|
+
await replayActiveDoc();
|
|
667
|
+
const meshes = [...meshCache.values(), ...sketchWireMeshes()];
|
|
668
|
+
if (meshes.length > 0)
|
|
669
|
+
await deps.store.publish(document.doc.docId, meshes);
|
|
670
|
+
await deps.registry.touch(document.doc.docId, { opCount: document.doc.version, bodyCount: meshCache.size });
|
|
671
|
+
const triangles = meshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0);
|
|
672
|
+
const value = {
|
|
673
|
+
sketch: name,
|
|
674
|
+
dependents,
|
|
675
|
+
bodies: meshes.length,
|
|
676
|
+
triangles,
|
|
677
|
+
version: document.doc.version,
|
|
678
|
+
};
|
|
679
|
+
const sceneUrlBase = deps.ensureSceneRoute();
|
|
680
|
+
if (sceneUrlBase !== null) {
|
|
681
|
+
value.sceneUrl = `${sceneUrlBase.replace('/scene', '/bin')}/${document.doc.docId}?v=${document.doc.version}`;
|
|
682
|
+
}
|
|
683
|
+
return value;
|
|
684
|
+
},
|
|
685
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD sketch edit ${String(args.name ?? '')}`, kind: 'other' }),
|
|
686
|
+
presentResult: () => ({ card: 'generic', title: 'CAD sketch edited' }),
|
|
687
|
+
});
|
|
688
|
+
const cadSketchList = defineTool({
|
|
689
|
+
name: 'cad_sketch_list',
|
|
690
|
+
description: 'List the document\'s named sketches: name, profile form and size. Read-only.',
|
|
691
|
+
parameters: {},
|
|
692
|
+
output: {
|
|
693
|
+
schema: {
|
|
694
|
+
type: 'object',
|
|
695
|
+
additionalProperties: false,
|
|
696
|
+
properties: {
|
|
697
|
+
sketches: {
|
|
698
|
+
type: 'array',
|
|
699
|
+
required: true,
|
|
700
|
+
description: 'One entry per sketch.',
|
|
701
|
+
items: {
|
|
702
|
+
type: 'object',
|
|
703
|
+
additionalProperties: false,
|
|
704
|
+
properties: {
|
|
705
|
+
name: { type: 'string', required: true, description: 'Sketch name.' },
|
|
706
|
+
type: { type: 'string', required: true, description: 'polygon | segments | circle.' },
|
|
707
|
+
points: { type: 'number', description: 'Polygon point count.' },
|
|
708
|
+
segments: { type: 'number', description: 'Curve-segment count.' },
|
|
709
|
+
radius: { type: 'number', description: 'Circle radius (mm).' },
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
...requiredCounts,
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
717
|
+
},
|
|
718
|
+
isConcurrencySafe: () => true,
|
|
719
|
+
async execute(_args, exec) {
|
|
720
|
+
await resolveDoc(exec);
|
|
721
|
+
const meshes = [...meshCache.values()];
|
|
722
|
+
const sketches = sketchesInLog().map(({ name, profile }) => ({ name, ...sketchProfileStats(profile) }));
|
|
723
|
+
return {
|
|
724
|
+
sketches,
|
|
725
|
+
bodies: meshes.length,
|
|
726
|
+
triangles: meshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0),
|
|
727
|
+
version: document.doc.version,
|
|
728
|
+
};
|
|
729
|
+
},
|
|
730
|
+
presentCall: () => ({ card: 'generic', title: 'CAD sketch list', kind: 'other' }),
|
|
731
|
+
presentResult: () => ({ card: 'generic', title: 'CAD sketches' }),
|
|
732
|
+
});
|
|
733
|
+
const cadSketchDelete = defineTool({
|
|
734
|
+
name: 'cad_sketch_delete',
|
|
735
|
+
description: 'Permanently delete a named sketch. Refused while features still reference it (delete those bodies first, or redefine the sketch with cad_sketch_edit).',
|
|
736
|
+
parameters: {
|
|
737
|
+
name: { type: 'string', required: true, description: 'The sketch to delete.' },
|
|
738
|
+
},
|
|
739
|
+
output: {
|
|
740
|
+
schema: {
|
|
741
|
+
type: 'object',
|
|
742
|
+
additionalProperties: false,
|
|
743
|
+
properties: {
|
|
744
|
+
sketch: { type: 'string', required: true, description: 'The deleted sketch name.' },
|
|
745
|
+
...requiredCounts,
|
|
746
|
+
...commonOptional,
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
750
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
751
|
+
},
|
|
752
|
+
isConcurrencySafe: () => false,
|
|
753
|
+
async execute(args, exec) {
|
|
754
|
+
await resolveDoc(exec);
|
|
755
|
+
const name = String(args.name ?? '');
|
|
756
|
+
const referrers = sketchReferrers(name);
|
|
757
|
+
if (referrers.length > 0) {
|
|
758
|
+
throw new Error(`sketch ${name} is referenced by ${referrers.join(', ')} — delete those bodies first or redefine the sketch with cad_sketch_edit`);
|
|
759
|
+
}
|
|
760
|
+
const op = { kind: 'sketch_delete', name };
|
|
761
|
+
const result = await runModelOp(op); // unknown sketch → error, nothing recorded
|
|
762
|
+
sketchWires.delete(name);
|
|
763
|
+
const value = await syncScene(op, result);
|
|
764
|
+
return { sketch: name, ...value };
|
|
765
|
+
},
|
|
766
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD sketch delete ${String(args.name ?? '')}`, kind: 'other' }),
|
|
767
|
+
presentResult: () => ({ card: 'generic', title: 'CAD sketch deleted' }),
|
|
768
|
+
});
|
|
769
|
+
const cadExtrude = defineTool({
|
|
770
|
+
name: 'cad_extrude_profile',
|
|
771
|
+
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. ' +
|
|
772
|
+
'The legacy flat `points` polygon loop (≥3 points, auto-closed) still works. Alternatively pass `sketch` (a named sketch from cad_sketch_new) instead of an inline profile — editing the sketch then rebuilds this body (cad_sketch_edit). Use cad_boolean for holes.',
|
|
773
|
+
parameters: {
|
|
774
|
+
points: { type: 'array', items: { type: 'number' }, description: 'Legacy flat [x0,y0,x1,y1,…] polygon loop (mm).' },
|
|
775
|
+
profile: { type: 'json', description: 'Curve-segment profile: {start:[x,y], segments:[…]} or {circle:{center:[x,y], radius}}.' },
|
|
776
|
+
sketch: { type: 'string', description: 'Named sketch (cad_sketch_new) to extrude — replaces points/profile; cad_sketch_edit rebuilds this body.' },
|
|
777
|
+
height: { type: 'number', description: 'Extrusion height (mm, default 10).' },
|
|
778
|
+
base: { type: 'number', description: 'Z of the profile plane (mm, default 0).' },
|
|
779
|
+
name: { type: 'string', description: 'Optional display name.' },
|
|
780
|
+
},
|
|
781
|
+
output: {
|
|
782
|
+
schema: {
|
|
783
|
+
type: 'object',
|
|
784
|
+
additionalProperties: false,
|
|
785
|
+
properties: {
|
|
786
|
+
bodyId: { type: 'string', required: true },
|
|
787
|
+
...requiredCounts,
|
|
788
|
+
...commonOptional,
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
792
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
793
|
+
},
|
|
794
|
+
isConcurrencySafe: () => false,
|
|
795
|
+
async execute(args, exec) {
|
|
796
|
+
await resolveDoc(exec);
|
|
797
|
+
if (args.sketch === undefined && args.profile === undefined && (args.points === undefined || args.points.length < 6 || args.points.length % 2 !== 0)) {
|
|
798
|
+
throw new Error('provide a curve-segment `profile` object, a flat points array of ≥3 [x,y] pairs, or a named `sketch`');
|
|
799
|
+
}
|
|
800
|
+
const bodyId = nextBodyId();
|
|
801
|
+
const op = {
|
|
802
|
+
kind: 'extrude_profile', bodyId,
|
|
803
|
+
...(args.sketch !== undefined ? { sketch: String(args.sketch) } : args.profile !== undefined ? { profile: args.profile } : { points: args.points }),
|
|
804
|
+
height: args.height, base: args.base, name: args.name,
|
|
805
|
+
};
|
|
806
|
+
const result = await runModelOp(op);
|
|
807
|
+
return syncScene(op, result);
|
|
808
|
+
},
|
|
809
|
+
presentCall: () => ({ card: 'generic', title: 'CAD extrude profile', kind: 'other' }),
|
|
810
|
+
presentResult: () => ({ card: 'generic', title: 'CAD extrude' }),
|
|
811
|
+
});
|
|
812
|
+
const cadLoft = defineTool({
|
|
813
|
+
name: 'cad_loft',
|
|
814
|
+
description: 'Loft (放样): skin a solid through successive closed sections (≥2), ordered along the loft. Each section is either a flat [x0,y0,z0, x1,y1,z1, …] point loop, or an object: {points:[…], smooth?:true}, {through:[…]} (closed B-spline interpolating the points), or {profile, at?, normal?, xAxis?} — a 2D curve profile (flat [x,y], {start,segments:[line|arc|bspline]}, or {circle:{center,radius}}) laid in its own plane. ' +
|
|
815
|
+
'For a CREASE-FREE surface pass `smooth: true` (or a curve section): a polygon-section loft is planar strips meeting at G0 creases, while a periodic B-spline / NURBS section skims ONE continuous B-spline lateral face. ' +
|
|
816
|
+
'Sections may differ in shape and point count (e.g. a square lofted to a hexagon); `ruled` keeps the sides straight instead of smoothing. ' +
|
|
817
|
+
'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.',
|
|
818
|
+
parameters: {
|
|
819
|
+
sections: {
|
|
820
|
+
type: 'array',
|
|
821
|
+
required: true,
|
|
822
|
+
description: 'Ordered sections (≥2): each a flat [x,y,z,…] loop (≥9 numbers) or a section object {points|through|profile, at?, normal?, xAxis?, smooth?}.',
|
|
823
|
+
items: { type: 'json', description: 'One closed section loop or section object.' },
|
|
824
|
+
},
|
|
825
|
+
smooth: { type: 'boolean', description: 'Fit a periodic B-spline through each plain point loop — the crease-free "NURBS section" loft (default false).' },
|
|
826
|
+
ruled: { type: 'boolean', description: 'Straight (ruled) sides instead of a smoothed surface (default false).' },
|
|
827
|
+
solid: { type: 'boolean', description: 'Cap the ends into a solid (default true).' },
|
|
828
|
+
name: { type: 'string', description: 'Optional display name.' },
|
|
829
|
+
},
|
|
830
|
+
output: {
|
|
831
|
+
schema: {
|
|
832
|
+
type: 'object',
|
|
833
|
+
additionalProperties: false,
|
|
834
|
+
properties: {
|
|
835
|
+
bodyId: { type: 'string', required: true },
|
|
836
|
+
...requiredCounts,
|
|
837
|
+
...commonOptional,
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
841
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
842
|
+
},
|
|
843
|
+
isConcurrencySafe: () => false,
|
|
844
|
+
async execute(args, exec) {
|
|
845
|
+
await resolveDoc(exec);
|
|
846
|
+
if (!Array.isArray(args.sections) || args.sections.length < 2)
|
|
847
|
+
throw new Error('sections must list at least 2 closed loops');
|
|
848
|
+
for (const section of args.sections) {
|
|
849
|
+
if (Array.isArray(section)) {
|
|
850
|
+
if (section.length < 9 || section.length % 3 !== 0)
|
|
851
|
+
throw new Error('a point loop section needs ≥3 [x,y,z] triplets (≥9 numbers)');
|
|
852
|
+
}
|
|
853
|
+
else if (section === null || typeof section !== 'object') {
|
|
854
|
+
throw new Error('each section must be a flat [x,y,z,…] loop or a {points|through|profile, …} object');
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
const bodyId = nextBodyId();
|
|
858
|
+
const op = { kind: 'loft', bodyId, sections: args.sections, solid: args.solid, ruled: args.ruled, smooth: args.smooth, name: args.name };
|
|
859
|
+
const result = await runModelOp(op);
|
|
860
|
+
return syncScene(op, result);
|
|
861
|
+
},
|
|
862
|
+
presentCall: () => ({ card: 'generic', title: 'CAD loft', kind: 'other' }),
|
|
863
|
+
presentResult: () => ({ card: 'generic', title: 'CAD loft' }),
|
|
864
|
+
});
|
|
865
|
+
const cadSweep = defineTool({
|
|
866
|
+
name: 'cad_sweep',
|
|
867
|
+
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. ' +
|
|
868
|
+
'`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.',
|
|
869
|
+
parameters: {
|
|
870
|
+
profile: { type: 'json', required: true, description: 'Closed outline on the start plane: flat [x,y,…] array, {start, segments:[…]} curve chain, or {circle:{center,radius}}.' },
|
|
871
|
+
sketch: { type: 'string', description: 'Named sketch (cad_sketch_new) to sweep — replaces the inline profile; cad_sketch_edit rebuilds this body.' },
|
|
872
|
+
path: { type: 'array', required: true, items: { type: 'number' }, description: 'Sweep path as [x,y,z,…] triplets (≥6 numbers).' },
|
|
873
|
+
name: { type: 'string', description: 'Optional display name.' },
|
|
874
|
+
},
|
|
875
|
+
output: {
|
|
876
|
+
schema: {
|
|
877
|
+
type: 'object',
|
|
878
|
+
additionalProperties: false,
|
|
879
|
+
properties: {
|
|
880
|
+
bodyId: { type: 'string', required: true },
|
|
881
|
+
...requiredCounts,
|
|
882
|
+
...commonOptional,
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
886
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
887
|
+
},
|
|
888
|
+
isConcurrencySafe: () => false,
|
|
889
|
+
async execute(args, exec) {
|
|
890
|
+
await resolveDoc(exec);
|
|
891
|
+
const bodyId = nextBodyId();
|
|
892
|
+
const op = {
|
|
893
|
+
kind: 'sweep', bodyId,
|
|
894
|
+
...(args.sketch !== undefined ? { sketch: String(args.sketch) } : { profile: args.profile }),
|
|
895
|
+
path: args.path, name: args.name,
|
|
896
|
+
};
|
|
897
|
+
const result = await runModelOp(op);
|
|
898
|
+
return syncScene(op, result);
|
|
899
|
+
},
|
|
900
|
+
presentCall: () => ({ card: 'generic', title: 'CAD sweep', kind: 'other' }),
|
|
901
|
+
presentResult: () => ({ card: 'generic', title: 'CAD sweep' }),
|
|
902
|
+
});
|
|
903
|
+
const cadRevolve = defineTool({
|
|
904
|
+
name: 'cad_revolve',
|
|
905
|
+
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. ' +
|
|
906
|
+
'Accepts the same curve-segment profiles as cad_extrude_profile (lines/arcs/bspline/circle), so rounded rims are exact. `angle` in degrees (default 360).',
|
|
907
|
+
parameters: {
|
|
908
|
+
profile: { type: 'json', required: true, description: 'Profile in (radius, height): flat [r0,h0, r1,h1,…] loop, {start, segments:[…]}, or {circle:{center,radius}}.' },
|
|
909
|
+
sketch: { type: 'string', description: 'Named sketch (cad_sketch_new) to revolve — replaces the inline profile; cad_sketch_edit rebuilds this body.' },
|
|
910
|
+
angle: { type: 'number', description: 'Sweep angle in degrees (default 360).' },
|
|
911
|
+
axis: { type: 'array', items: { type: 'number' }, description: 'Revolve axis direction [x,y,z] (default +Z).' },
|
|
912
|
+
at: { type: 'array', items: { type: 'number' }, description: 'A point the axis passes through [x,y,z] (default origin).' },
|
|
913
|
+
name: { type: 'string', description: 'Optional display name.' },
|
|
914
|
+
},
|
|
915
|
+
output: {
|
|
916
|
+
schema: {
|
|
917
|
+
type: 'object',
|
|
918
|
+
additionalProperties: false,
|
|
919
|
+
properties: {
|
|
920
|
+
bodyId: { type: 'string', required: true },
|
|
921
|
+
...requiredCounts,
|
|
922
|
+
...commonOptional,
|
|
923
|
+
},
|
|
924
|
+
},
|
|
925
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
926
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
927
|
+
},
|
|
928
|
+
isConcurrencySafe: () => false,
|
|
929
|
+
async execute(args, exec) {
|
|
930
|
+
await resolveDoc(exec);
|
|
931
|
+
const bodyId = nextBodyId();
|
|
932
|
+
const op = {
|
|
933
|
+
kind: 'revolve', bodyId,
|
|
934
|
+
...(args.sketch !== undefined ? { sketch: String(args.sketch) } : { profile: args.profile }),
|
|
935
|
+
...(args.angle !== undefined ? { angle: (args.angle * Math.PI) / 180 } : {}),
|
|
936
|
+
...(Array.isArray(args.axis) ? { axis: args.axis } : {}),
|
|
937
|
+
...(Array.isArray(args.at) ? { at: args.at } : {}),
|
|
938
|
+
...(args.name !== undefined ? { name: args.name } : {}),
|
|
939
|
+
};
|
|
940
|
+
const result = await runModelOp(op);
|
|
941
|
+
return syncScene(op, result);
|
|
942
|
+
},
|
|
943
|
+
presentCall: () => ({ card: 'generic', title: 'CAD revolve', kind: 'other' }),
|
|
944
|
+
presentResult: () => ({ card: 'generic', title: 'CAD revolve' }),
|
|
945
|
+
});
|
|
946
|
+
const cadChamfer = defineTool({
|
|
947
|
+
name: 'cad_chamfer',
|
|
948
|
+
description: 'Chamfer (倒角): bevel every sharp edge of a body with one equal distance (mm). Fails when the distance exceeds the adjacent faces.',
|
|
949
|
+
parameters: {
|
|
950
|
+
target: bodyTarget,
|
|
951
|
+
distance: { type: 'number', required: true, description: 'Chamfer distance (mm).' },
|
|
952
|
+
},
|
|
953
|
+
output: {
|
|
954
|
+
schema: {
|
|
955
|
+
type: 'object',
|
|
956
|
+
additionalProperties: false,
|
|
957
|
+
properties: {
|
|
958
|
+
bodyId: { type: 'string', required: true },
|
|
959
|
+
...requiredCounts,
|
|
960
|
+
...commonOptional,
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
964
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
965
|
+
},
|
|
966
|
+
isConcurrencySafe: () => false,
|
|
967
|
+
async execute(args, exec) {
|
|
968
|
+
await resolveDoc(exec);
|
|
969
|
+
const op = { kind: 'chamfer', target: args.target, distance: args.distance };
|
|
970
|
+
const result = await runModelOp(op);
|
|
971
|
+
return syncScene(op, result);
|
|
972
|
+
},
|
|
973
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD chamfer ${String(args.target)}`, kind: 'other' }),
|
|
974
|
+
presentResult: () => ({ card: 'generic', title: 'CAD chamfer' }),
|
|
975
|
+
});
|
|
976
|
+
const cadShell = defineTool({
|
|
977
|
+
name: 'cad_shell',
|
|
978
|
+
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). ' +
|
|
979
|
+
'Keeps the model editable after shelling: fillet/chamfer/loft/sweep/revolve/boolean all keep working on the shelled solid.',
|
|
980
|
+
parameters: {
|
|
981
|
+
target: bodyTarget,
|
|
982
|
+
thickness: { type: 'number', required: true, description: 'Wall thickness (mm, positive).' },
|
|
983
|
+
open: { type: 'array', items: { type: 'array', items: { type: 'number' } }, description: 'Outward normals of the faces to open, e.g. [[0,0,1]].' },
|
|
984
|
+
},
|
|
985
|
+
output: {
|
|
986
|
+
schema: {
|
|
987
|
+
type: 'object',
|
|
988
|
+
additionalProperties: false,
|
|
989
|
+
properties: {
|
|
990
|
+
bodyId: { type: 'string', required: true },
|
|
991
|
+
...requiredCounts,
|
|
992
|
+
...commonOptional,
|
|
993
|
+
},
|
|
994
|
+
},
|
|
995
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
996
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
997
|
+
},
|
|
998
|
+
isConcurrencySafe: () => false,
|
|
999
|
+
async execute(args, exec) {
|
|
1000
|
+
await resolveDoc(exec);
|
|
1001
|
+
const op = { kind: 'shell', target: args.target, thickness: args.thickness, ...(Array.isArray(args.open) ? { openNormals: args.open } : {}) };
|
|
1002
|
+
const result = await runModelOp(op);
|
|
1003
|
+
return syncScene(op, result);
|
|
1004
|
+
},
|
|
1005
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD shell ${String(args.target)}`, kind: 'other' }),
|
|
1006
|
+
presentResult: () => ({ card: 'generic', title: 'CAD shell' }),
|
|
1007
|
+
});
|
|
1008
|
+
const cadDraft = defineTool({
|
|
1009
|
+
name: 'cad_draft',
|
|
1010
|
+
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. ' +
|
|
1011
|
+
'Runs on the occt.ts kernel (>= 0.3.0): the result becomes a HOSTED body (same caveats as cad_shell).',
|
|
1012
|
+
parameters: {
|
|
1013
|
+
target: bodyTarget,
|
|
1014
|
+
angle: { type: 'number', required: true, description: 'Draft angle in degrees (positive opens the walls toward the pull direction).' },
|
|
1015
|
+
direction: { type: 'array', items: { type: 'number' }, description: 'Pull direction [x,y,z] (default +Z).' },
|
|
1016
|
+
},
|
|
1017
|
+
output: {
|
|
1018
|
+
schema: {
|
|
1019
|
+
type: 'object',
|
|
1020
|
+
additionalProperties: false,
|
|
1021
|
+
properties: {
|
|
1022
|
+
bodyId: { type: 'string', required: true },
|
|
1023
|
+
...requiredCounts,
|
|
1024
|
+
...commonOptional,
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
1028
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
1029
|
+
},
|
|
1030
|
+
isConcurrencySafe: () => false,
|
|
1031
|
+
async execute(args, exec) {
|
|
1032
|
+
await resolveDoc(exec);
|
|
1033
|
+
const op = { kind: 'draft', target: args.target, angle: args.angle, ...(Array.isArray(args.direction) ? { direction: args.direction } : {}) };
|
|
1034
|
+
const result = await runModelOp(op);
|
|
1035
|
+
return syncScene(op, result);
|
|
1036
|
+
},
|
|
1037
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD draft ${String(args.target)} ${String(args.angle ?? '')}°`, kind: 'other' }),
|
|
1038
|
+
presentResult: () => ({ card: 'generic', title: 'CAD draft' }),
|
|
1039
|
+
});
|
|
1040
|
+
const cadPattern = defineTool({
|
|
1041
|
+
name: 'cad_pattern',
|
|
1042
|
+
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.',
|
|
1043
|
+
parameters: {
|
|
1044
|
+
target: bodyTarget,
|
|
1045
|
+
mode: { type: 'string', required: true, enum: ['linear', 'circular'], description: 'Pattern mode.' },
|
|
1046
|
+
count: { type: 'number', required: true, description: 'Total placements (≥2).' },
|
|
1047
|
+
delta: { type: 'array', items: { type: 'number' }, description: 'linear: spacing [dx,dy,dz] per copy (mm).' },
|
|
1048
|
+
axis: { type: 'array', items: { type: 'number' }, description: 'circular: principal axis [x,y,z] (default +Z).' },
|
|
1049
|
+
at: { type: 'array', items: { type: 'number' }, description: 'circular: axis point [x,y,z] (default origin).' },
|
|
1050
|
+
angle: { type: 'number', description: 'circular: total sweep in degrees (default 360).' },
|
|
1051
|
+
},
|
|
1052
|
+
output: {
|
|
1053
|
+
schema: {
|
|
1054
|
+
type: 'object',
|
|
1055
|
+
additionalProperties: false,
|
|
1056
|
+
properties: {
|
|
1057
|
+
bodyId: { type: 'string', required: true },
|
|
1058
|
+
created: { type: 'array', items: { type: 'string' }, required: true, description: 'Ids of the newly created copy bodies.' },
|
|
1059
|
+
...requiredCounts,
|
|
1060
|
+
...commonOptional,
|
|
1061
|
+
},
|
|
1062
|
+
},
|
|
1063
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) + '\ncreated: ' + value.created.join(', ') }],
|
|
1064
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
1065
|
+
},
|
|
1066
|
+
isConcurrencySafe: () => false,
|
|
1067
|
+
async execute(args, exec) {
|
|
1068
|
+
await resolveDoc(exec);
|
|
1069
|
+
if (args.mode === 'linear' && !Array.isArray(args.delta))
|
|
1070
|
+
throw new Error('linear patterns need delta: [dx,dy,dz]');
|
|
1071
|
+
const op = {
|
|
1072
|
+
kind: 'pattern', target: args.target, mode: args.mode, count: args.count,
|
|
1073
|
+
...(Array.isArray(args.delta) ? { delta: args.delta } : {}),
|
|
1074
|
+
...(Array.isArray(args.axis) ? { axis: args.axis } : {}),
|
|
1075
|
+
...(Array.isArray(args.at) ? { at: args.at } : {}),
|
|
1076
|
+
...(args.angle !== undefined ? { angle: args.angle } : {}),
|
|
1077
|
+
};
|
|
1078
|
+
const result = await runModelOp(op);
|
|
1079
|
+
const createdEntries = result.created ?? [];
|
|
1080
|
+
const created = createdEntries.map((entry) => {
|
|
1081
|
+
if (entry.mesh !== undefined)
|
|
1082
|
+
meshCache.set(entry.bodyId, mirrorMesh(entry.mesh, entry.bodyId));
|
|
1083
|
+
return entry.bodyId;
|
|
1084
|
+
});
|
|
1085
|
+
// Record every copy's name: bodyNames is the manifest the assembly
|
|
1086
|
+
// tree reads, and skipping copies marked pattern-derived instances as
|
|
1087
|
+
// missing (issue #6).
|
|
1088
|
+
await document.record(op, createdEntries.map((entry) => ({ bodyId: entry.bodyId, name: entry.name ?? entry.bodyId })));
|
|
1089
|
+
const meshes = [...meshCache.values()];
|
|
1090
|
+
const sceneUrlBase = deps.ensureSceneRoute();
|
|
1091
|
+
if (sceneUrlBase !== null && meshes.length > 0) {
|
|
1092
|
+
await deps.store.publish(document.doc.docId, meshes);
|
|
1093
|
+
}
|
|
1094
|
+
const value = {
|
|
1095
|
+
bodyId: args.target,
|
|
1096
|
+
created,
|
|
1097
|
+
triangles: meshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0),
|
|
1098
|
+
bodies: meshes.length,
|
|
1099
|
+
version: document.doc.version,
|
|
1100
|
+
...(sceneUrlBase !== null ? { sceneUrl: `${sceneUrlBase.replace('/scene', '/bin')}/${document.doc.docId}?v=${document.doc.version}` } : {}),
|
|
1101
|
+
};
|
|
1102
|
+
return value;
|
|
1103
|
+
},
|
|
1104
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD ${String(args.mode ?? '')} pattern ×${String(args.count ?? '')}`, kind: 'other' }),
|
|
1105
|
+
presentResult: () => ({ card: 'generic', title: 'CAD pattern' }),
|
|
1106
|
+
});
|
|
1107
|
+
const cadBoolean = defineTool({
|
|
1108
|
+
name: 'cad_boolean',
|
|
1109
|
+
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.',
|
|
1110
|
+
parameters: {
|
|
1111
|
+
op: { type: 'string', required: true, enum: ['fuse', 'cut', 'common'], description: 'Boolean operation.' },
|
|
1112
|
+
target: { type: 'string', required: true, description: 'BodyId kept as the result.' },
|
|
1113
|
+
tools: { type: 'array', required: true, items: { type: 'string' }, description: 'BodyIds combined into the target.' },
|
|
1114
|
+
},
|
|
1115
|
+
output: {
|
|
1116
|
+
schema: {
|
|
1117
|
+
type: 'object',
|
|
1118
|
+
additionalProperties: false,
|
|
1119
|
+
properties: {
|
|
1120
|
+
bodyId: { type: 'string', required: true },
|
|
1121
|
+
...requiredCounts,
|
|
1122
|
+
...commonOptional,
|
|
1123
|
+
},
|
|
1124
|
+
},
|
|
1125
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
1126
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
1127
|
+
},
|
|
1128
|
+
isConcurrencySafe: () => false,
|
|
1129
|
+
async execute(args, exec) {
|
|
1130
|
+
await resolveDoc(exec);
|
|
1131
|
+
const op = { kind: 'boolean', op: args.op, target: args.target, tools: args.tools };
|
|
1132
|
+
const result = await runModelOp(op);
|
|
1133
|
+
return syncScene(op, result);
|
|
1134
|
+
},
|
|
1135
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD ${String(args.op)} ${String(args.target)}`, kind: 'other' }),
|
|
1136
|
+
presentResult: () => ({ card: 'generic', title: 'CAD boolean' }),
|
|
1137
|
+
});
|
|
1138
|
+
const cadFillet = defineTool({
|
|
1139
|
+
name: 'cad_fillet',
|
|
1140
|
+
description: 'Round every sharp edge of a body with one radius (mm). Fails when the radius exceeds the adjacent faces.',
|
|
1141
|
+
parameters: {
|
|
1142
|
+
target: bodyTarget,
|
|
1143
|
+
radius: { type: 'number', required: true, description: 'Fillet radius (mm).' },
|
|
1144
|
+
},
|
|
1145
|
+
output: {
|
|
1146
|
+
schema: {
|
|
1147
|
+
type: 'object',
|
|
1148
|
+
additionalProperties: false,
|
|
1149
|
+
properties: {
|
|
1150
|
+
bodyId: { type: 'string', required: true },
|
|
1151
|
+
...requiredCounts,
|
|
1152
|
+
...commonOptional,
|
|
1153
|
+
},
|
|
1154
|
+
},
|
|
1155
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
1156
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
1157
|
+
},
|
|
1158
|
+
isConcurrencySafe: () => false,
|
|
1159
|
+
async execute(args, exec) {
|
|
1160
|
+
await resolveDoc(exec);
|
|
1161
|
+
const op = { kind: 'fillet', target: args.target, radius: args.radius };
|
|
1162
|
+
const result = await runModelOp(op);
|
|
1163
|
+
return syncScene(op, result);
|
|
1164
|
+
},
|
|
1165
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD fillet ${String(args.target)}`, kind: 'other' }),
|
|
1166
|
+
presentResult: () => ({ card: 'generic', title: 'CAD fillet' }),
|
|
1167
|
+
});
|
|
1168
|
+
const cadTransform = defineTool({
|
|
1169
|
+
name: 'cad_transform',
|
|
1170
|
+
description: 'Move/rotate/mirror a body (mm, degrees, Z-up). Applies in order: translate → rotate (XYZ Euler degrees) → mirror (plane through the origin by normal).',
|
|
1171
|
+
parameters: {
|
|
1172
|
+
target: bodyTarget,
|
|
1173
|
+
translate: pointParam('translation [x,y,z] (mm).'),
|
|
1174
|
+
rotate: pointParam('rotation [rx,ry,rz] (degrees, XYZ Euler).'),
|
|
1175
|
+
mirror: pointParam('mirror plane normal [x,y,z] through the origin.'),
|
|
1176
|
+
},
|
|
1177
|
+
output: {
|
|
1178
|
+
schema: {
|
|
1179
|
+
type: 'object',
|
|
1180
|
+
additionalProperties: false,
|
|
1181
|
+
properties: {
|
|
1182
|
+
bodyId: { type: 'string', required: true },
|
|
1183
|
+
...requiredCounts,
|
|
1184
|
+
...commonOptional,
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1187
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
1188
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
1189
|
+
},
|
|
1190
|
+
isConcurrencySafe: () => false,
|
|
1191
|
+
async execute(args, exec) {
|
|
1192
|
+
await resolveDoc(exec);
|
|
1193
|
+
const op = {
|
|
1194
|
+
kind: 'transform',
|
|
1195
|
+
target: args.target,
|
|
1196
|
+
translate: args.translate,
|
|
1197
|
+
rotate: args.rotate,
|
|
1198
|
+
mirror: args.mirror,
|
|
1199
|
+
};
|
|
1200
|
+
const result = await runModelOp(op);
|
|
1201
|
+
return syncScene(op, result);
|
|
1202
|
+
},
|
|
1203
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD transform ${String(args.target)}`, kind: 'other' }),
|
|
1204
|
+
presentResult: () => ({ card: 'generic', title: 'CAD transform' }),
|
|
1205
|
+
});
|
|
1206
|
+
const cadExport = defineTool({
|
|
1207
|
+
name: 'cad_export',
|
|
1208
|
+
description: 'Export to a workspace path (extension selects format): .step / .stl of a body, or .dcprt (the native replayable part document). ' +
|
|
1209
|
+
'target "assembly" exports the whole assembly (STEP assembly of transformed instances). ' +
|
|
1210
|
+
'target "drawing" exports the latest drawing sheet (.svg vector / .dxf exchange).',
|
|
1211
|
+
parameters: {
|
|
1212
|
+
target: { type: 'string', description: 'BodyId; "assembly" for the whole assembly; "drawing" for the latest sheet (required for .step/.stl; unused for .dcprt).' },
|
|
1213
|
+
path: { type: 'string', required: true, description: 'Destination file path (extension selects format).' },
|
|
1214
|
+
},
|
|
1215
|
+
output: {
|
|
1216
|
+
schema: {
|
|
1217
|
+
type: 'object',
|
|
1218
|
+
additionalProperties: false,
|
|
1219
|
+
properties: {
|
|
1220
|
+
...requiredCounts,
|
|
1221
|
+
...commonOptional,
|
|
1222
|
+
instances: { type: 'number', description: 'Assembly instance count (assembly export).' },
|
|
1223
|
+
viewId: { type: 'string', description: 'Assembly scene viewId (assembly export).' },
|
|
1224
|
+
target: { type: 'string', description: 'Exported target: bodyId, "assembly", or "drawing".' },
|
|
1225
|
+
drawingId: { type: 'string', description: 'Drawing scene viewId (drawing export).' },
|
|
1226
|
+
entities: { type: 'number', description: 'Sheet entity count (drawing export).' },
|
|
1227
|
+
scale: { type: 'number', description: 'Standard-view scale (drawing export).' },
|
|
1228
|
+
scaleText: { type: 'string', description: 'Scale as titled text (drawing export).' },
|
|
1229
|
+
filePath: { type: 'string', required: true, description: 'Written file path.' },
|
|
1230
|
+
},
|
|
1231
|
+
},
|
|
1232
|
+
render: (_args, value) => [{
|
|
1233
|
+
type: 'text',
|
|
1234
|
+
text: value.viewId !== undefined && String(value.viewId).startsWith('asm-')
|
|
1235
|
+
? renderAssembly(value)
|
|
1236
|
+
: renderModel(value),
|
|
1237
|
+
}],
|
|
1238
|
+
presentationMeta: (_args, value) => {
|
|
1239
|
+
const record = value;
|
|
1240
|
+
if (typeof record.viewId === 'string' && record.viewId.startsWith('asm-'))
|
|
1241
|
+
return assemblyMetaOf(record);
|
|
1242
|
+
return metaOf(record);
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
isConcurrencySafe: () => true,
|
|
1246
|
+
async execute(args, exec) {
|
|
1247
|
+
await resolveDoc(exec);
|
|
1248
|
+
const resolved = resolveSessionPath(args.path, exec, deps.workspaceRoot);
|
|
1249
|
+
// Auto-create the parent directory — the session workspace is the base,
|
|
1250
|
+
// and nested export paths should just work (issue #6).
|
|
1251
|
+
await mkdir(path.dirname(resolved), { recursive: true });
|
|
1252
|
+
const lower = args.path.toLowerCase();
|
|
1253
|
+
// Assembly export: compound of transformed instances via the worker.
|
|
1254
|
+
if (args.target === 'assembly') {
|
|
1255
|
+
const format = lower.endsWith('.stl') ? 'stl' : 'step';
|
|
1256
|
+
const op = { kind: 'export_assembly', format };
|
|
1257
|
+
const result = await runModelOp(op);
|
|
1258
|
+
if (result.bytes === undefined)
|
|
1259
|
+
throw new Error('assembly export produced no data');
|
|
1260
|
+
await writeFile(resolved, Buffer.from(result.bytes));
|
|
1261
|
+
return syncAssembly(op, result, resolved);
|
|
1262
|
+
}
|
|
1263
|
+
// Drawing export: serialize the latest rebuilt sheet — no op recorded
|
|
1264
|
+
// (pure export, same convention as .dcprt).
|
|
1265
|
+
if (args.target === 'drawing') {
|
|
1266
|
+
if (lastDrawingId === null)
|
|
1267
|
+
throw new Error('no drawing yet — call cad_drawing first');
|
|
1268
|
+
const entry = drawingSheets.get(lastDrawingId);
|
|
1269
|
+
if (entry === undefined)
|
|
1270
|
+
throw new Error('drawing sheet is unavailable');
|
|
1271
|
+
if (lower.endsWith('.dxf'))
|
|
1272
|
+
await writeFile(resolved, drawingToDxf(entry.sheet), 'utf8');
|
|
1273
|
+
else if (lower.endsWith('.svg'))
|
|
1274
|
+
await writeFile(resolved, drawingToSvg(entry.sheet), 'utf8');
|
|
1275
|
+
else
|
|
1276
|
+
throw new Error('drawing export supports .svg and .dxf paths');
|
|
1277
|
+
const meshes = [...meshCache.values()];
|
|
1278
|
+
const sceneUrlBase = deps.ensureSceneRoute();
|
|
1279
|
+
const value = {
|
|
1280
|
+
target: 'drawing',
|
|
1281
|
+
name: entry.partName,
|
|
1282
|
+
drawingId: lastDrawingId,
|
|
1283
|
+
entities: entry.sheet.entityCount,
|
|
1284
|
+
scale: entry.sheet.scale,
|
|
1285
|
+
scaleText: entry.sheet.scale >= 1 ? `${entry.sheet.scale}:1` : `1:${Math.round(1 / entry.sheet.scale * 100) / 100}`,
|
|
1286
|
+
bodies: meshes.length,
|
|
1287
|
+
triangles: meshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0),
|
|
1288
|
+
version: document.doc.version,
|
|
1289
|
+
filePath: resolved,
|
|
1290
|
+
};
|
|
1291
|
+
if (sceneUrlBase !== null)
|
|
1292
|
+
value.sceneUrl = `/dsh-cad/scene/${lastDrawingId}?v=${document.doc.version}`;
|
|
1293
|
+
return value;
|
|
1294
|
+
}
|
|
1295
|
+
// .dcprt serializes the whole document on the main thread — no worker
|
|
1296
|
+
// op (that path exports one body's geometry), no entry in the log.
|
|
1297
|
+
if (lower.endsWith('.dcprt')) {
|
|
1298
|
+
if (document.doc.ops.length === 0)
|
|
1299
|
+
throw new Error('nothing to export — the modeling document is empty');
|
|
1300
|
+
await writeFile(resolved, JSON.stringify(toDcPrtDocument(document.doc)));
|
|
1301
|
+
const meshes = [...meshCache.values()];
|
|
1302
|
+
const value = {
|
|
1303
|
+
triangles: meshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0),
|
|
1304
|
+
bodies: meshes.length,
|
|
1305
|
+
version: document.doc.version,
|
|
1306
|
+
filePath: resolved,
|
|
1307
|
+
};
|
|
1308
|
+
const sceneUrlBase = deps.ensureSceneRoute();
|
|
1309
|
+
if (sceneUrlBase !== null) {
|
|
1310
|
+
value.sceneUrl = `${sceneUrlBase.replace('/scene', '/bin')}/${document.doc.docId}?v=${document.doc.version}`;
|
|
1311
|
+
}
|
|
1312
|
+
return value;
|
|
1313
|
+
}
|
|
1314
|
+
if (args.target === undefined)
|
|
1315
|
+
throw new Error('target is required for .step/.stl exports');
|
|
1316
|
+
const format = lower.endsWith('.stl') ? 'stl' : 'step';
|
|
1317
|
+
const op = { kind: 'export', target: args.target, format };
|
|
1318
|
+
const result = await runModelOp(op);
|
|
1319
|
+
if (result.bytes === undefined)
|
|
1320
|
+
throw new Error('export produced no data');
|
|
1321
|
+
await writeFile(resolved, Buffer.from(result.bytes));
|
|
1322
|
+
return syncScene(op, result, resolved);
|
|
1323
|
+
},
|
|
1324
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD export ${String(args.path)}`, kind: 'other' }),
|
|
1325
|
+
presentResult: () => ({ card: 'generic', title: 'CAD export' }),
|
|
1326
|
+
});
|
|
1327
|
+
const cadDelete = defineTool({
|
|
1328
|
+
name: 'cad_delete',
|
|
1329
|
+
description: 'Delete a body from the modeling document.',
|
|
1330
|
+
parameters: {
|
|
1331
|
+
target: bodyTarget,
|
|
1332
|
+
},
|
|
1333
|
+
output: {
|
|
1334
|
+
schema: {
|
|
1335
|
+
type: 'object',
|
|
1336
|
+
additionalProperties: false,
|
|
1337
|
+
properties: {
|
|
1338
|
+
...requiredCounts,
|
|
1339
|
+
...commonOptional,
|
|
1340
|
+
},
|
|
1341
|
+
},
|
|
1342
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
1343
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
1344
|
+
},
|
|
1345
|
+
isConcurrencySafe: () => false,
|
|
1346
|
+
async execute(args, exec) {
|
|
1347
|
+
await resolveDoc(exec);
|
|
1348
|
+
const op = { kind: 'delete', target: args.target };
|
|
1349
|
+
const result = await runModelOp(op);
|
|
1350
|
+
return syncScene(op, result);
|
|
1351
|
+
},
|
|
1352
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD delete ${String(args.target)}`, kind: 'delete' }),
|
|
1353
|
+
presentResult: () => ({ card: 'generic', title: 'CAD delete' }),
|
|
1354
|
+
});
|
|
1355
|
+
const cadVolume = defineTool({
|
|
1356
|
+
name: 'cad_volume',
|
|
1357
|
+
description: "Report a body's exact BRep volume in mm³.",
|
|
1358
|
+
parameters: {
|
|
1359
|
+
target: bodyTarget,
|
|
1360
|
+
},
|
|
1361
|
+
output: {
|
|
1362
|
+
schema: {
|
|
1363
|
+
type: 'object',
|
|
1364
|
+
additionalProperties: false,
|
|
1365
|
+
properties: {
|
|
1366
|
+
...requiredCounts,
|
|
1367
|
+
...commonOptional,
|
|
1368
|
+
volume: { type: 'number', required: true, description: 'Volume (mm³).' },
|
|
1369
|
+
centroid: { type: 'array', items: { type: 'number' }, description: 'Center of mass [x,y,z] (occt.ts backend).' },
|
|
1370
|
+
},
|
|
1371
|
+
},
|
|
1372
|
+
render: (_args, value) => [{ type: 'text', text: renderModel(value) }],
|
|
1373
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
1374
|
+
},
|
|
1375
|
+
isConcurrencySafe: () => true,
|
|
1376
|
+
async execute(args, exec) {
|
|
1377
|
+
await resolveDoc(exec);
|
|
1378
|
+
const op = { kind: 'volume', target: args.target };
|
|
1379
|
+
const result = await runModelOp(op);
|
|
1380
|
+
const value = await syncScene(op, result);
|
|
1381
|
+
if (Array.isArray(result.centroid))
|
|
1382
|
+
value.centroid = result.centroid;
|
|
1383
|
+
return value;
|
|
1384
|
+
},
|
|
1385
|
+
presentCall: (args) => ({ card: 'generic', title: `CAD volume ${String(args.target)}`, kind: 'read' }),
|
|
1386
|
+
presentResult: () => ({ card: 'generic', title: 'CAD volume' }),
|
|
1387
|
+
});
|
|
1388
|
+
const cadDrawing = defineTool({
|
|
1389
|
+
name: 'cad_drawing',
|
|
1390
|
+
description: 'Generate an engineering drawing (工程图) of a body: GB first-angle 主视图/俯视图/左视图 plus an isometric view on an A4/A3 sheet — ' +
|
|
1391
|
+
'hidden lines dashed, frame, title block, overall dimensions, standard scale. Renders in the Drawing tab and exports via cad_export target "drawing" (.svg/.dxf).',
|
|
1392
|
+
parameters: {
|
|
1393
|
+
target: { type: 'string', description: 'BodyId to draw (default: the most recently created surviving body).' },
|
|
1394
|
+
paper: { type: 'string', enum: ['A4', 'A3'], description: 'Sheet size (default A4 landscape).' },
|
|
1395
|
+
name: { type: 'string', description: 'Part display name for the title block.' },
|
|
1396
|
+
},
|
|
1397
|
+
output: {
|
|
1398
|
+
schema: {
|
|
1399
|
+
type: 'object',
|
|
1400
|
+
additionalProperties: false,
|
|
1401
|
+
properties: drawingSchema,
|
|
1402
|
+
},
|
|
1403
|
+
render: (_args, value) => [{ type: 'text', text: renderDrawing(value) }],
|
|
1404
|
+
presentationMeta: (_args, value) => drawingMetaOf(value),
|
|
1405
|
+
},
|
|
1406
|
+
isConcurrencySafe: () => false,
|
|
1407
|
+
async execute(args, exec) {
|
|
1408
|
+
await resolveDoc(exec);
|
|
1409
|
+
const target = args.target ?? lastBodyId();
|
|
1410
|
+
if (target === undefined)
|
|
1411
|
+
throw new Error('nothing to draw — create a body first');
|
|
1412
|
+
if (!meshCache.has(target))
|
|
1413
|
+
throw new Error(`unknown body: ${target}`);
|
|
1414
|
+
const partName = args.name ?? document.doc.bodyNames[target] ?? target;
|
|
1415
|
+
const paper = args.paper === 'A3' ? 'A3' : 'A4';
|
|
1416
|
+
const sceneViewId = siblingUuid('d');
|
|
1417
|
+
const op = { kind: 'drawing', target, views: DRAWING_VIEWS, sceneViewId, name: partName, paper };
|
|
1418
|
+
const result = await runModelOp(op);
|
|
1419
|
+
const sheet = await publishDrawing(sceneViewId, partName, result.views, paper);
|
|
1420
|
+
const meshes = [...meshCache.values()];
|
|
1421
|
+
const sceneUrlBase = deps.ensureSceneRoute();
|
|
1422
|
+
const value = {
|
|
1423
|
+
target,
|
|
1424
|
+
name: partName,
|
|
1425
|
+
drawingId: sceneViewId,
|
|
1426
|
+
entities: sheet.entityCount,
|
|
1427
|
+
scale: sheet.scale,
|
|
1428
|
+
scaleText: sheet.scale >= 1 ? `${sheet.scale}:1` : `1:${Math.round(1 / sheet.scale * 100) / 100}`,
|
|
1429
|
+
bodies: meshes.length,
|
|
1430
|
+
triangles: meshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0),
|
|
1431
|
+
version: document.doc.version,
|
|
1432
|
+
};
|
|
1433
|
+
if (sceneUrlBase !== null)
|
|
1434
|
+
value.sceneUrl = `/dsh-cad/scene/${sceneViewId}?v=${document.doc.version}`;
|
|
1435
|
+
return value;
|
|
1436
|
+
},
|
|
1437
|
+
presentCall: (args) => ({ card: 'generic', title: `工程图 ${String(args.target ?? '')}`.trim(), kind: 'other' }),
|
|
1438
|
+
presentResult: () => ({ card: 'generic', title: '工程图' }),
|
|
1439
|
+
});
|
|
1440
|
+
const cadAssemblyInsert = defineTool({
|
|
1441
|
+
name: 'cad_assembly_insert',
|
|
1442
|
+
description: 'Insert a body into the assembly as a placed instance (mm, degrees, Z-up). `at` positions the instance origin; `rotate` orients it (XYZ Euler). ' +
|
|
1443
|
+
'Bodies stay untouched in the Part Studio; the Assembly tab renders every instance. Instances persist in the document op log.',
|
|
1444
|
+
parameters: {
|
|
1445
|
+
bodyId: { type: 'string', required: true, description: 'BodyId to insert.' },
|
|
1446
|
+
at: pointParam('instance position [x,y,z] (mm).'),
|
|
1447
|
+
rotate: pointParam('instance rotation [rx,ry,rz] (degrees, XYZ Euler).'),
|
|
1448
|
+
name: { type: 'string', description: 'Instance display name (defaults to the body name).' },
|
|
1449
|
+
},
|
|
1450
|
+
output: {
|
|
1451
|
+
schema: { type: 'object', additionalProperties: false, properties: assemblySchema },
|
|
1452
|
+
render: (_args, value) => [{ type: 'text', text: renderAssembly(value) }],
|
|
1453
|
+
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
1454
|
+
},
|
|
1455
|
+
isConcurrencySafe: () => false,
|
|
1456
|
+
async execute(args, exec) {
|
|
1457
|
+
await resolveDoc(exec);
|
|
1458
|
+
if (!meshCache.has(args.bodyId))
|
|
1459
|
+
throw new Error(`unknown body: ${args.bodyId}`);
|
|
1460
|
+
const instanceId = nextInstanceId();
|
|
1461
|
+
const op = {
|
|
1462
|
+
kind: 'assembly_insert',
|
|
1463
|
+
instanceId,
|
|
1464
|
+
bodyId: args.bodyId,
|
|
1465
|
+
name: args.name ?? document.doc.bodyNames[args.bodyId] ?? args.bodyId,
|
|
1466
|
+
translate: args.at,
|
|
1467
|
+
rotate: args.rotate,
|
|
1468
|
+
};
|
|
1469
|
+
const result = await runModelOp(op);
|
|
1470
|
+
return syncAssembly(op, result);
|
|
1471
|
+
},
|
|
1472
|
+
presentCall: (args) => ({ card: 'generic', title: `装配 ${String(args.bodyId)}`, kind: 'other' }),
|
|
1473
|
+
presentResult: () => ({ card: 'generic', title: '装配插入' }),
|
|
1474
|
+
});
|
|
1475
|
+
const cadAssemblyMove = defineTool({
|
|
1476
|
+
name: 'cad_assembly_move',
|
|
1477
|
+
description: 'Set an assembly instance placement (absolute, not incremental): `at` sets the position, `rotate` the orientation (mm, degrees).',
|
|
1478
|
+
parameters: {
|
|
1479
|
+
instanceId: { type: 'string', required: true, description: 'InstanceId to move.' },
|
|
1480
|
+
at: pointParam('new position [x,y,z] (mm).'),
|
|
1481
|
+
rotate: pointParam('new rotation [rx,ry,rz] (degrees, XYZ Euler).'),
|
|
1482
|
+
},
|
|
1483
|
+
output: {
|
|
1484
|
+
schema: { type: 'object', additionalProperties: false, properties: assemblySchema },
|
|
1485
|
+
render: (_args, value) => [{ type: 'text', text: renderAssembly(value) }],
|
|
1486
|
+
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
1487
|
+
},
|
|
1488
|
+
isConcurrencySafe: () => false,
|
|
1489
|
+
async execute(args, exec) {
|
|
1490
|
+
await resolveDoc(exec);
|
|
1491
|
+
const op = {
|
|
1492
|
+
kind: 'assembly_transform',
|
|
1493
|
+
instanceId: args.instanceId,
|
|
1494
|
+
translate: args.at,
|
|
1495
|
+
rotate: args.rotate,
|
|
1496
|
+
};
|
|
1497
|
+
const result = await runModelOp(op);
|
|
1498
|
+
return syncAssembly(op, result);
|
|
1499
|
+
},
|
|
1500
|
+
presentCall: (args) => ({ card: 'generic', title: `移动 ${String(args.instanceId)}`, kind: 'other' }),
|
|
1501
|
+
presentResult: () => ({ card: 'generic', title: '装配移动' }),
|
|
1502
|
+
});
|
|
1503
|
+
const cadAssemblyRemove = defineTool({
|
|
1504
|
+
name: 'cad_assembly_remove',
|
|
1505
|
+
description: 'Remove an instance from the assembly (the body itself stays in the Part Studio).',
|
|
1506
|
+
parameters: {
|
|
1507
|
+
instanceId: { type: 'string', required: true, description: 'InstanceId to remove.' },
|
|
1508
|
+
},
|
|
1509
|
+
output: {
|
|
1510
|
+
schema: { type: 'object', additionalProperties: false, properties: assemblySchema },
|
|
1511
|
+
render: (_args, value) => [{ type: 'text', text: renderAssembly(value) }],
|
|
1512
|
+
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
1513
|
+
},
|
|
1514
|
+
isConcurrencySafe: () => false,
|
|
1515
|
+
async execute(args, exec) {
|
|
1516
|
+
await resolveDoc(exec);
|
|
1517
|
+
const op = { kind: 'assembly_remove', instanceId: args.instanceId };
|
|
1518
|
+
const result = await runModelOp(op);
|
|
1519
|
+
return syncAssembly(op, result);
|
|
1520
|
+
},
|
|
1521
|
+
presentCall: (args) => ({ card: 'generic', title: `移除 ${String(args.instanceId)}`, kind: 'delete' }),
|
|
1522
|
+
presentResult: () => ({ card: 'generic', title: '装配移除' }),
|
|
1523
|
+
});
|
|
1524
|
+
// ── joint-driven assembly (cadorange 0.10.0 joint algebra, main-thread) ────
|
|
1525
|
+
/** Fold the ACTIVE document's assembly state (placements + joints). */
|
|
1526
|
+
const foldActive = () => foldAssemblyState(document.doc.ops, document.doc.bodyNames);
|
|
1527
|
+
const placementEqual = (a, b) => a.translate.every((v, i) => Math.abs(v - (b.translate[i] ?? 0)) < 1e-6) &&
|
|
1528
|
+
a.rotate.every((v, i) => Math.abs(v - (b.rotate[i] ?? 0)) < 1e-6);
|
|
1529
|
+
/** Flat joint rows (id/name/label/type/value) for tool outputs. */
|
|
1530
|
+
const jointRows = (state) => {
|
|
1531
|
+
const rows = [];
|
|
1532
|
+
for (const instance of state.instances) {
|
|
1533
|
+
for (const [label, spec] of Object.entries(state.joints.get(instance.instanceId) ?? {})) {
|
|
1534
|
+
const row = {
|
|
1535
|
+
instance: instance.instanceId,
|
|
1536
|
+
name: instance.name,
|
|
1537
|
+
label,
|
|
1538
|
+
type: spec.type,
|
|
1539
|
+
};
|
|
1540
|
+
const value = jointValueText(spec);
|
|
1541
|
+
if (value !== '')
|
|
1542
|
+
row.value = value;
|
|
1543
|
+
rows.push(row);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return rows;
|
|
1547
|
+
};
|
|
1548
|
+
/**
|
|
1549
|
+
* Re-solve every recorded connection at the joints' CURRENT values and
|
|
1550
|
+
* write changed child placements back through recorded assembly_transform
|
|
1551
|
+
* ops — the drive/solve motion path. Returns the moved placements.
|
|
1552
|
+
*/
|
|
1553
|
+
async function recomputeConnections() {
|
|
1554
|
+
const state = foldActive();
|
|
1555
|
+
const solved = solveConnections(state.instances, state.joints, state.connections);
|
|
1556
|
+
const moved = [];
|
|
1557
|
+
for (const instance of state.instances) {
|
|
1558
|
+
const placement = solved.get(instance.instanceId);
|
|
1559
|
+
if (placement === undefined || placementEqual(placement, instance))
|
|
1560
|
+
continue;
|
|
1561
|
+
const op = { kind: 'assembly_transform', instanceId: instance.instanceId, translate: placement.translate, rotate: placement.rotate };
|
|
1562
|
+
const result = await runModelOp(op);
|
|
1563
|
+
await syncAssembly(op, result);
|
|
1564
|
+
moved.push({ instanceId: instance.instanceId, translate: placement.translate, rotate: placement.rotate });
|
|
1565
|
+
}
|
|
1566
|
+
return moved;
|
|
1567
|
+
}
|
|
1568
|
+
/** Shared output schema fields for tools that emit `moved` placements. */
|
|
1569
|
+
const movedSchema = {
|
|
1570
|
+
moved: {
|
|
1571
|
+
type: 'array',
|
|
1572
|
+
description: 'Instances re-placed by the solve, with their new placements.',
|
|
1573
|
+
items: {
|
|
1574
|
+
type: 'json',
|
|
1575
|
+
description: '{ instanceId, translate: [x,y,z], rotate: [rx,ry,rz] }.',
|
|
1576
|
+
},
|
|
1577
|
+
},
|
|
1578
|
+
};
|
|
1579
|
+
/**
|
|
1580
|
+
* Assembly stats for read-style outputs (instance count + composed
|
|
1581
|
+
* triangle count at the current explode factor).
|
|
1582
|
+
*/
|
|
1583
|
+
const assemblyStats = () => {
|
|
1584
|
+
const state = foldActive();
|
|
1585
|
+
const offsets = explodeOffsets(meshCache, state.instances, state.explode);
|
|
1586
|
+
const triangles = composeAssemblyMeshes(meshCache, state.instances, offsets.size > 0 ? offsets : undefined)
|
|
1587
|
+
.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0);
|
|
1588
|
+
return { instances: state.instances.length, triangles };
|
|
1589
|
+
};
|
|
1590
|
+
const cadAssemblyJoint = defineTool({
|
|
1591
|
+
name: 'cad_assembly_joint',
|
|
1592
|
+
description: 'Define a joint on an assembly instance (the build123d joint family): rigid 刚性 / revolute 旋转铰 / linear 滑动 / cylindrical 圆柱 / ball 球面 / planar 平面 / polar 极坐标. ' +
|
|
1593
|
+
'The joint frame (`at` + `axis` + optional `xDir`) lives in the INSTANCE\'s local coordinates (mm, degrees). Joints are placement data — pair them with cad_assembly_connect to solve placements. ' +
|
|
1594
|
+
'Redefining a label replaces the joint.',
|
|
1595
|
+
parameters: {
|
|
1596
|
+
instanceId: { type: 'string', required: true, description: 'InstanceId that owns the joint.' },
|
|
1597
|
+
label: { type: 'string', required: true, description: 'Joint label — connect references instances by label (e.g. "axis", "pin").' },
|
|
1598
|
+
joint: {
|
|
1599
|
+
type: 'json',
|
|
1600
|
+
required: true,
|
|
1601
|
+
description: '{ type: "rigid"|"revolute"|"linear"|"cylindrical"|"ball"|"planar"|"polar", at?: [x,y,z] frame origin (local), axis?: [x,y,z] joint axis (default +Z), xDir?: [x,y,z] zero-position reference, ' +
|
|
1602
|
+
'value?: number | { angle?, linear?, radial?, position?: [u,v], angles?: [ax,ay,az] }, angularRange?: [min,max]°, linearRange?: [min,max]mm }.',
|
|
1603
|
+
},
|
|
1604
|
+
},
|
|
1605
|
+
output: {
|
|
1606
|
+
schema: {
|
|
1607
|
+
type: 'object',
|
|
1608
|
+
additionalProperties: false,
|
|
1609
|
+
properties: {
|
|
1610
|
+
...assemblySchema,
|
|
1611
|
+
joint: { type: 'json', description: 'The stored joint (type + value text).' },
|
|
1612
|
+
},
|
|
1613
|
+
},
|
|
1614
|
+
render: (_args, value) => [{ type: 'text', text: renderAssembly(value) }],
|
|
1615
|
+
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
1616
|
+
},
|
|
1617
|
+
isConcurrencySafe: () => false,
|
|
1618
|
+
async execute(args, exec) {
|
|
1619
|
+
await resolveDoc(exec);
|
|
1620
|
+
const spec = normalizeJointSpec(args.joint, 'joint');
|
|
1621
|
+
const state = foldActive();
|
|
1622
|
+
if (!state.instances.some((instance) => instance.instanceId === args.instanceId)) {
|
|
1623
|
+
throw new Error(`unknown instance: ${String(args.instanceId)} — insert it with cad_assembly_insert first`);
|
|
1624
|
+
}
|
|
1625
|
+
const op = { kind: 'assembly_joint', instanceId: args.instanceId, label: args.label, joint: spec };
|
|
1626
|
+
const result = await runModelOp(op);
|
|
1627
|
+
const value = await syncAssembly(op, result);
|
|
1628
|
+
const text = jointValueText(spec);
|
|
1629
|
+
return { ...value, joint: { instanceId: args.instanceId, label: args.label, type: spec.type, ...(text !== '' ? { value: text } : {}) } };
|
|
1630
|
+
},
|
|
1631
|
+
presentCall: (args) => ({ card: 'generic', title: `关节 ${String(args.instanceId)}.${String(args.label)}`, kind: 'other' }),
|
|
1632
|
+
presentResult: () => ({ card: 'generic', title: '定义关节' }),
|
|
1633
|
+
});
|
|
1634
|
+
const cadAssemblyConnect = defineTool({
|
|
1635
|
+
name: 'cad_assembly_connect',
|
|
1636
|
+
description: 'Place a child instance by pairing joints: the parent stays fixed, the child is repositioned so the two joint frames coincide (with motion values applied) — 装配配合的关节版. ' +
|
|
1637
|
+
'Pairing rules: a rigid child connects to any parent kind; non-rigid parents take a rigid child (linear also takes revolute). Optional `value` sets the parent joint\'s motion at connect time. ' +
|
|
1638
|
+
'The connection is recorded — cad_assembly_drive re-solves it at new joint values.',
|
|
1639
|
+
parameters: {
|
|
1640
|
+
instanceId: { type: 'string', required: true, description: 'Child instance to place.' },
|
|
1641
|
+
parent: { type: 'string', required: true, description: 'Parent (fixed) instance.' },
|
|
1642
|
+
parentLabel: { type: 'string', required: true, description: 'Parent joint label.' },
|
|
1643
|
+
childLabel: { type: 'string', required: true, description: 'Child joint label.' },
|
|
1644
|
+
value: { type: 'json', description: 'Parent joint motion at connect time (number or { angle?, linear?, … }).' },
|
|
1645
|
+
},
|
|
1646
|
+
output: {
|
|
1647
|
+
schema: {
|
|
1648
|
+
type: 'object',
|
|
1649
|
+
additionalProperties: false,
|
|
1650
|
+
properties: {
|
|
1651
|
+
...assemblySchema,
|
|
1652
|
+
placement: { type: 'json', description: 'The solved child placement { translate, rotate }.' },
|
|
1653
|
+
},
|
|
1654
|
+
},
|
|
1655
|
+
render: (_args, value) => [{ type: 'text', text: renderAssembly(value) }],
|
|
1656
|
+
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
1657
|
+
},
|
|
1658
|
+
isConcurrencySafe: () => false,
|
|
1659
|
+
async execute(args, exec) {
|
|
1660
|
+
await resolveDoc(exec);
|
|
1661
|
+
const state = foldActive();
|
|
1662
|
+
const child = state.instances.find((instance) => instance.instanceId === args.instanceId);
|
|
1663
|
+
const parent = state.instances.find((instance) => instance.instanceId === args.parent);
|
|
1664
|
+
if (child === undefined)
|
|
1665
|
+
throw new Error(`unknown instance: ${String(args.instanceId)}`);
|
|
1666
|
+
if (parent === undefined)
|
|
1667
|
+
throw new Error(`unknown instance: ${String(args.parent)}`);
|
|
1668
|
+
const parentSpec = state.joints.get(parent.instanceId)?.[args.parentLabel];
|
|
1669
|
+
const childSpec = state.joints.get(child.instanceId)?.[args.childLabel];
|
|
1670
|
+
if (parentSpec === undefined) {
|
|
1671
|
+
throw new Error(`no joint "${String(args.parentLabel)}" on ${parent.instanceId} — define it with cad_assembly_joint`);
|
|
1672
|
+
}
|
|
1673
|
+
if (childSpec === undefined) {
|
|
1674
|
+
throw new Error(`no joint "${String(args.childLabel)}" on ${child.instanceId} — define it with cad_assembly_joint`);
|
|
1675
|
+
}
|
|
1676
|
+
checkPairing(parentSpec.type, childSpec.type, `connect ${parent.instanceId}.${String(args.parentLabel)} ↔ ${child.instanceId}.${String(args.childLabel)}`);
|
|
1677
|
+
const applied = args.value !== undefined
|
|
1678
|
+
? applyConnectValue(parentSpec, childSpec, args.value, `connect ${parent.instanceId}.${String(args.parentLabel)}`)
|
|
1679
|
+
: { parent: parentSpec, child: childSpec };
|
|
1680
|
+
const placement = solveConnection(parent, applied.parent, applied.child);
|
|
1681
|
+
const connectOp = {
|
|
1682
|
+
kind: 'assembly_connect',
|
|
1683
|
+
instanceId: child.instanceId,
|
|
1684
|
+
parent: parent.instanceId,
|
|
1685
|
+
parentLabel: args.parentLabel,
|
|
1686
|
+
childLabel: args.childLabel,
|
|
1687
|
+
...(args.value !== undefined ? { value: args.value } : {}),
|
|
1688
|
+
};
|
|
1689
|
+
const connectResult = await runModelOp(connectOp);
|
|
1690
|
+
await syncAssembly(connectOp, connectResult);
|
|
1691
|
+
const transformOp = {
|
|
1692
|
+
kind: 'assembly_transform',
|
|
1693
|
+
instanceId: child.instanceId,
|
|
1694
|
+
translate: placement.translate,
|
|
1695
|
+
rotate: placement.rotate,
|
|
1696
|
+
};
|
|
1697
|
+
const transformResult = await runModelOp(transformOp);
|
|
1698
|
+
const value = await syncAssembly(transformOp, transformResult);
|
|
1699
|
+
return { ...value, placement: { translate: placement.translate, rotate: placement.rotate } };
|
|
1700
|
+
},
|
|
1701
|
+
presentCall: (args) => ({ card: 'generic', title: `配合 ${String(args.parent)} ↔ ${String(args.instanceId)}`, kind: 'other' }),
|
|
1702
|
+
presentResult: () => ({ card: 'generic', title: '关节配合' }),
|
|
1703
|
+
});
|
|
1704
|
+
const cadAssemblyDrive = defineTool({
|
|
1705
|
+
name: 'cad_assembly_drive',
|
|
1706
|
+
description: 'Drive a joint\'s motion value, then re-solve every recorded connection (运动仿真的一步): set angle/linear/etc., the solver replays all pairings at the new value and writes changed placements back. ' +
|
|
1707
|
+
'Value forms per kind — revolute: number (°); linear: number (mm); cylindrical: { angle, linear }; ball: { angles: [ax,ay,az] }; planar: { angle, position: [u,v] }; polar: { angle, radial }. ' +
|
|
1708
|
+
'Ranges declared on the joint are enforced.',
|
|
1709
|
+
parameters: {
|
|
1710
|
+
instanceId: { type: 'string', required: true, description: 'Instance owning the joint.' },
|
|
1711
|
+
label: { type: 'string', required: true, description: 'Joint label.' },
|
|
1712
|
+
value: { type: 'json', required: true, description: 'New motion value (number or kind-appropriate object).' },
|
|
1713
|
+
},
|
|
1714
|
+
output: {
|
|
1715
|
+
schema: {
|
|
1716
|
+
type: 'object',
|
|
1717
|
+
additionalProperties: false,
|
|
1718
|
+
properties: {
|
|
1719
|
+
...assemblySchema,
|
|
1720
|
+
joint: { type: 'json', description: 'The driven joint (label/type/value text).' },
|
|
1721
|
+
...movedSchema,
|
|
1722
|
+
},
|
|
1723
|
+
},
|
|
1724
|
+
render: (_args, value) => [{ type: 'text', text: renderAssembly(value) }],
|
|
1725
|
+
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
1726
|
+
},
|
|
1727
|
+
isConcurrencySafe: () => false,
|
|
1728
|
+
async execute(args, exec) {
|
|
1729
|
+
await resolveDoc(exec);
|
|
1730
|
+
const state = foldActive();
|
|
1731
|
+
const spec = state.joints.get(args.instanceId)?.[args.label];
|
|
1732
|
+
if (spec === undefined) {
|
|
1733
|
+
throw new Error(`no joint "${String(args.label)}" on ${String(args.instanceId)} — define it with cad_assembly_joint`);
|
|
1734
|
+
}
|
|
1735
|
+
const merged = mergeJointValue(spec, args.value, `drive ${String(args.instanceId)}.${String(args.label)}`);
|
|
1736
|
+
const op = { kind: 'assembly_joint_value', instanceId: args.instanceId, label: args.label, value: merged.value };
|
|
1737
|
+
const result = await runModelOp(op);
|
|
1738
|
+
await syncAssembly(op, result);
|
|
1739
|
+
const moved = await recomputeConnections();
|
|
1740
|
+
const stats = assemblyStats();
|
|
1741
|
+
const text = jointValueText(merged);
|
|
1742
|
+
return {
|
|
1743
|
+
instanceId: args.instanceId,
|
|
1744
|
+
joint: { instanceId: args.instanceId, label: args.label, type: merged.type, ...(text !== '' ? { value: text } : {}) },
|
|
1745
|
+
moved,
|
|
1746
|
+
...stats,
|
|
1747
|
+
viewId: `asm-${document.doc.docId}`,
|
|
1748
|
+
bodies: meshCache.size,
|
|
1749
|
+
version: document.doc.version,
|
|
1750
|
+
...(deps.ensureSceneRoute() !== null
|
|
1751
|
+
? { sceneUrl: `${deps.ensureSceneRoute().replace('/scene', '/bin')}/asm-${document.doc.docId}?v=${document.doc.version}` }
|
|
1752
|
+
: {}),
|
|
1753
|
+
};
|
|
1754
|
+
},
|
|
1755
|
+
presentCall: (args) => ({ card: 'generic', title: `驱动 ${String(args.instanceId)}.${String(args.label)}`, kind: 'other' }),
|
|
1756
|
+
presentResult: () => ({ card: 'generic', title: '驱动关节' }),
|
|
1757
|
+
});
|
|
1758
|
+
const cadAssemblySolve = defineTool({
|
|
1759
|
+
name: 'cad_assembly_solve',
|
|
1760
|
+
description: 'Re-solve every recorded joint pairing at current values (e.g. after manually moving a parent with cad_assembly_move). Children are re-placed from their parents\' current placements. ' +
|
|
1761
|
+
'No-op when the assembly has no connections.',
|
|
1762
|
+
parameters: {},
|
|
1763
|
+
output: {
|
|
1764
|
+
schema: {
|
|
1765
|
+
type: 'object',
|
|
1766
|
+
additionalProperties: false,
|
|
1767
|
+
properties: { ...assemblySchema, ...movedSchema },
|
|
1768
|
+
},
|
|
1769
|
+
render: (_args, value) => [{ type: 'text', text: renderAssembly(value) }],
|
|
1770
|
+
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
1771
|
+
},
|
|
1772
|
+
isConcurrencySafe: () => false,
|
|
1773
|
+
async execute(_args, exec) {
|
|
1774
|
+
await resolveDoc(exec);
|
|
1775
|
+
const moved = await recomputeConnections();
|
|
1776
|
+
return {
|
|
1777
|
+
moved,
|
|
1778
|
+
...assemblyStats(),
|
|
1779
|
+
viewId: `asm-${document.doc.docId}`,
|
|
1780
|
+
bodies: meshCache.size,
|
|
1781
|
+
version: document.doc.version,
|
|
1782
|
+
...(deps.ensureSceneRoute() !== null
|
|
1783
|
+
? { sceneUrl: `${deps.ensureSceneRoute().replace('/scene', '/bin')}/asm-${document.doc.docId}?v=${document.doc.version}` }
|
|
1784
|
+
: {}),
|
|
1785
|
+
};
|
|
1786
|
+
},
|
|
1787
|
+
presentCall: () => ({ card: 'generic', title: '求解装配', kind: 'other' }),
|
|
1788
|
+
presentResult: () => ({ card: 'generic', title: '求解装配' }),
|
|
1789
|
+
});
|
|
1790
|
+
const cadAssemblyExplode = defineTool({
|
|
1791
|
+
name: 'cad_assembly_explode',
|
|
1792
|
+
description: 'Set the exploded-view factor (爆炸图): every instance is pushed radially away from the assembly center — distance × factor (factor 0 = assembled, negative implodes). ' +
|
|
1793
|
+
'View-state only: body geometry, exports and drawings stay assembled. Persists in the document op log.',
|
|
1794
|
+
parameters: {
|
|
1795
|
+
factor: { type: 'number', required: true, description: 'Explode factor (0 = assembled).' },
|
|
1796
|
+
},
|
|
1797
|
+
output: {
|
|
1798
|
+
schema: {
|
|
1799
|
+
type: 'object',
|
|
1800
|
+
additionalProperties: false,
|
|
1801
|
+
properties: { ...assemblySchema, explode: { type: 'number', required: true, description: 'The stored explode factor.' } },
|
|
1802
|
+
},
|
|
1803
|
+
render: (_args, value) => [{ type: 'text', text: renderAssembly(value) }],
|
|
1804
|
+
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
1805
|
+
},
|
|
1806
|
+
isConcurrencySafe: () => false,
|
|
1807
|
+
async execute(args, exec) {
|
|
1808
|
+
await resolveDoc(exec);
|
|
1809
|
+
if (typeof args.factor !== 'number' || !Number.isFinite(args.factor))
|
|
1810
|
+
throw new Error('factor must be a finite number');
|
|
1811
|
+
const op = { kind: 'assembly_explode', factor: args.factor };
|
|
1812
|
+
const result = await runModelOp(op);
|
|
1813
|
+
const value = await syncAssembly(op, result);
|
|
1814
|
+
return { ...value, explode: args.factor };
|
|
1815
|
+
},
|
|
1816
|
+
presentCall: (args) => ({ card: 'generic', title: `爆炸 ×${String(args.factor)}`, kind: 'other' }),
|
|
1817
|
+
presentResult: () => ({ card: 'generic', title: '爆炸视图' }),
|
|
1818
|
+
});
|
|
1819
|
+
const cadAssemblyInfo = defineTool({
|
|
1820
|
+
name: 'cad_assembly_info',
|
|
1821
|
+
description: 'Read the assembly state: every instance (placement), joint (label/kind/value) and recorded pairing, plus the explode factor. Call this before connecting or driving to see current labels.',
|
|
1822
|
+
parameters: {},
|
|
1823
|
+
output: {
|
|
1824
|
+
schema: {
|
|
1825
|
+
type: 'object',
|
|
1826
|
+
additionalProperties: false,
|
|
1827
|
+
properties: {
|
|
1828
|
+
instances: {
|
|
1829
|
+
type: 'array',
|
|
1830
|
+
required: true,
|
|
1831
|
+
description: 'Placed instances.',
|
|
1832
|
+
items: { type: 'json', description: '{ instanceId, bodyId, name, translate, rotate }.' },
|
|
1833
|
+
},
|
|
1834
|
+
joints: { type: 'array', required: true, description: 'Defined joints.', items: { type: 'json', description: '{ instance, name, label, type, value? }.' } },
|
|
1835
|
+
connections: { type: 'array', required: true, description: 'Recorded pairings.', items: { type: 'json', description: '{ child, parent, parentLabel, childLabel }.' } },
|
|
1836
|
+
explode: { type: 'number', description: 'Exploded-view factor (absent when assembled).' },
|
|
1837
|
+
bodies: { type: 'number', required: true, description: 'Bodies in the document.' },
|
|
1838
|
+
version: { type: 'number', required: true, description: 'Document version.' },
|
|
1839
|
+
},
|
|
1840
|
+
},
|
|
1841
|
+
render: (_args, value) => [{ type: 'text', text: renderAssemblyInfo(value) }],
|
|
1842
|
+
},
|
|
1843
|
+
isConcurrencySafe: () => true,
|
|
1844
|
+
async execute(_args, exec) {
|
|
1845
|
+
await resolveDoc(exec);
|
|
1846
|
+
const state = foldActive();
|
|
1847
|
+
return {
|
|
1848
|
+
instances: state.instances.map((instance) => ({
|
|
1849
|
+
instanceId: instance.instanceId,
|
|
1850
|
+
bodyId: instance.bodyId,
|
|
1851
|
+
name: instance.name,
|
|
1852
|
+
translate: instance.translate,
|
|
1853
|
+
rotate: instance.rotate,
|
|
1854
|
+
})),
|
|
1855
|
+
joints: jointRows(state),
|
|
1856
|
+
connections: state.connections,
|
|
1857
|
+
...(Math.abs(state.explode) > 1e-12 ? { explode: state.explode } : {}),
|
|
1858
|
+
bodies: meshCache.size,
|
|
1859
|
+
version: document.doc.version,
|
|
1860
|
+
};
|
|
1861
|
+
},
|
|
1862
|
+
presentCall: () => ({ card: 'generic', title: '装配信息', kind: 'read' }),
|
|
1863
|
+
presentResult: () => ({ card: 'generic', title: '装配信息' }),
|
|
1864
|
+
});
|
|
1865
|
+
/** Text render for cad_assembly_info (labels are the load-bearing part). */
|
|
1866
|
+
const renderAssemblyInfo = (value) => {
|
|
1867
|
+
const instances = Array.isArray(value.instances) ? value.instances : [];
|
|
1868
|
+
const joints = Array.isArray(value.joints) ? value.joints : [];
|
|
1869
|
+
const connections = Array.isArray(value.connections) ? value.connections : [];
|
|
1870
|
+
const lines = [];
|
|
1871
|
+
if (instances.length === 0) {
|
|
1872
|
+
lines.push('装配体为空 — 用 cad_assembly_insert 插入实例');
|
|
1873
|
+
}
|
|
1874
|
+
else {
|
|
1875
|
+
for (const instance of instances) {
|
|
1876
|
+
lines.push(`${String(instance.instanceId)} ${String(instance.name)} (${String(instance.bodyId)}) at [${instance.translate.map((v) => Number(v.toFixed(2))).join(', ')}]`);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
if (joints.length > 0) {
|
|
1880
|
+
lines.push(`joints: ${joints.map((joint) => `${String(joint.instance)}.${String(joint.label)} ${String(joint.type)}${joint.value === undefined ? '' : ` ${String(joint.value)}`}`).join(' · ')}`);
|
|
1881
|
+
}
|
|
1882
|
+
if (connections.length > 0) {
|
|
1883
|
+
lines.push(`connections: ${connections.map((c) => `${String(c.child)}.${String(c.childLabel)} ↔ ${String(c.parent)}.${String(c.parentLabel)}`).join(' · ')}`);
|
|
1884
|
+
}
|
|
1885
|
+
if (value.explode !== undefined)
|
|
1886
|
+
lines.push(`explode: ×${String(value.explode)}`);
|
|
1887
|
+
return lines.join('\n');
|
|
1888
|
+
};
|
|
1889
|
+
const cadStepImport = defineTool({
|
|
1890
|
+
name: 'cad_step_import',
|
|
1891
|
+
description: 'Import a STEP file as an ASSEMBLY (结构化导入): names, part structure and instance placements survive — identical parts dedupe into one body referenced by N instances (齿轮阵列就是一个体). ' +
|
|
1892
|
+
'Creates bodies + placed instances in the active document (replayable; the file is cached as a workspace asset). Requires the cadorange kernel.',
|
|
1893
|
+
parameters: {
|
|
1894
|
+
file: { type: 'string', required: true, description: 'STEP file path (absolute, or relative to the workspace root).' },
|
|
1895
|
+
},
|
|
1896
|
+
output: {
|
|
1897
|
+
schema: {
|
|
1898
|
+
type: 'object',
|
|
1899
|
+
additionalProperties: false,
|
|
1900
|
+
properties: {
|
|
1901
|
+
bodies: { type: 'number', required: true, description: 'Unique imported bodies.' },
|
|
1902
|
+
instances: { type: 'number', required: true, description: 'Placed instances.' },
|
|
1903
|
+
parts: { type: 'array', required: true, description: 'Imported bodies.', items: { type: 'json', description: '{ bodyId, name }.' } },
|
|
1904
|
+
triangles: { type: 'number', required: true, description: 'Triangle count.' },
|
|
1905
|
+
version: { type: 'number', required: true, description: 'Document version.' },
|
|
1906
|
+
viewId: { type: 'string', description: 'Assembly scene viewId.' },
|
|
1907
|
+
sceneUrl: { type: 'string', description: 'Versioned assembly scene URL (web compositions).' },
|
|
1908
|
+
filePath: { type: 'string', description: 'Cached asset path.' },
|
|
1909
|
+
},
|
|
1910
|
+
},
|
|
1911
|
+
render: (_args, value) => [{ type: 'text', text: renderAssembly(value) }],
|
|
1912
|
+
presentationMeta: (_args, value) => assemblyMetaOf(value),
|
|
1913
|
+
},
|
|
1914
|
+
isConcurrencySafe: () => false,
|
|
1915
|
+
async execute(args, exec) {
|
|
1916
|
+
await resolveDoc(exec);
|
|
1917
|
+
const resolved = resolveSessionPath(String(args.file), exec, deps.workspaceRoot);
|
|
1918
|
+
const bytes = await loadCadFile(resolved);
|
|
1919
|
+
if (!bytes.subarray(0, 512).toString('latin1').includes('ISO-10303')) {
|
|
1920
|
+
throw new Error(`not a STEP file: ${resolved}`);
|
|
1921
|
+
}
|
|
1922
|
+
// Content-addressed sidecar asset — the recorded op references it, so
|
|
1923
|
+
// replay re-reads the same bytes without bloating the op log.
|
|
1924
|
+
const hash = createHash('sha1').update(bytes).digest('hex');
|
|
1925
|
+
const assetsDir = path.join(deps.workspaceRoot, '.dsh-cad', 'docs', 'assets');
|
|
1926
|
+
const assetPath = path.join(assetsDir, `${hash}.step`);
|
|
1927
|
+
await mkdir(assetsDir, { recursive: true });
|
|
1928
|
+
await writeFile(assetPath, bytes);
|
|
1929
|
+
const probe = await runModelOp({ kind: 'import_step', path: assetPath });
|
|
1930
|
+
const structure = probe.stepStructure;
|
|
1931
|
+
if (structure === undefined || structure.parts.length === 0)
|
|
1932
|
+
throw new Error('the STEP file parses into no solid parts');
|
|
1933
|
+
const base = document.doc.version + 1;
|
|
1934
|
+
const bodyAssignments = structure.parts.map((part, index) => ({ bodyId: `b${base + index}`, name: part.name }));
|
|
1935
|
+
const instanceAssignments = structure.leaves.map((leaf, index) => ({
|
|
1936
|
+
instanceId: `a${base + index}`,
|
|
1937
|
+
bodyId: bodyAssignments[leaf.part]?.bodyId ?? `b${base + leaf.part}`,
|
|
1938
|
+
name: leaf.name,
|
|
1939
|
+
translate: leaf.translate,
|
|
1940
|
+
rotate: leaf.rotate,
|
|
1941
|
+
}));
|
|
1942
|
+
const op = { kind: 'import_step', path: assetPath, bodies: bodyAssignments, instances: instanceAssignments };
|
|
1943
|
+
const result = await runModelOp(op);
|
|
1944
|
+
for (const mesh of result.meshes ?? [])
|
|
1945
|
+
meshCache.set(mesh.bodyId, mirrorMesh(mesh, mesh.bodyId));
|
|
1946
|
+
await document.record(op, bodyAssignments);
|
|
1947
|
+
await deps.registry.touch(document.doc.docId, { opCount: document.doc.version, bodyCount: meshCache.size });
|
|
1948
|
+
const sceneUrlBase = deps.ensureSceneRoute();
|
|
1949
|
+
const partMeshes = [...meshCache.values()];
|
|
1950
|
+
if (partMeshes.length > 0 && sceneUrlBase !== null) {
|
|
1951
|
+
await deps.store.publish(document.doc.docId, partMeshes);
|
|
1952
|
+
}
|
|
1953
|
+
const instances = result.instances ?? [];
|
|
1954
|
+
const { explode } = foldActive();
|
|
1955
|
+
const offsets = explodeOffsets(meshCache, instances, explode);
|
|
1956
|
+
const assemblyMeshes = composeAssemblyMeshes(meshCache, instances, offsets.size > 0 ? offsets : undefined);
|
|
1957
|
+
if (sceneUrlBase !== null)
|
|
1958
|
+
await deps.store.publish(`asm-${document.doc.docId}`, assemblyMeshes);
|
|
1959
|
+
const triangles = assemblyMeshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0);
|
|
1960
|
+
return {
|
|
1961
|
+
bodies: bodyAssignments.length,
|
|
1962
|
+
instances: instanceAssignments.length,
|
|
1963
|
+
parts: bodyAssignments,
|
|
1964
|
+
triangles,
|
|
1965
|
+
version: document.doc.version,
|
|
1966
|
+
...(sceneUrlBase !== null ? { viewId: `asm-${document.doc.docId}` } : {}),
|
|
1967
|
+
...(sceneUrlBase !== null
|
|
1968
|
+
? { sceneUrl: `${sceneUrlBase.replace('/scene', '/bin')}/asm-${document.doc.docId}?v=${document.doc.version}` }
|
|
1969
|
+
: {}),
|
|
1970
|
+
filePath: assetPath,
|
|
1971
|
+
};
|
|
1972
|
+
},
|
|
1973
|
+
presentCall: (args) => ({ card: 'generic', title: `导入 STEP ${String(args.file)}`, kind: 'other' }),
|
|
1974
|
+
presentResult: () => ({ card: 'generic', title: 'STEP 导入' }),
|
|
1975
|
+
});
|
|
1976
|
+
// ── document (file space) tools ───────────────────────────────────────────
|
|
1977
|
+
const docRefParam = { type: 'string', required: true, description: 'Document id or exact name (list with cad_docs).' };
|
|
1978
|
+
const renderDocs = (value) => {
|
|
1979
|
+
const docs = Array.isArray(value.docs) ? value.docs : [];
|
|
1980
|
+
const active = typeof value.activeDoc === 'string' ? value.activeDoc : '';
|
|
1981
|
+
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)}`);
|
|
1982
|
+
return lines.length > 0 ? ['工作区文档:', ...lines].join('\n') : '工作区还没有建模文档';
|
|
1983
|
+
};
|
|
1984
|
+
const cadDocs = defineTool({
|
|
1985
|
+
name: 'cad_docs',
|
|
1986
|
+
description: 'List the workspace modeling documents (the file space): id, name, body count and update time, marking the one active for this session. ' +
|
|
1987
|
+
'Call before cad_doc_open / cad_doc_delete to discover ids and names.',
|
|
1988
|
+
parameters: {},
|
|
1989
|
+
output: {
|
|
1990
|
+
schema: {
|
|
1991
|
+
type: 'object',
|
|
1992
|
+
additionalProperties: false,
|
|
1993
|
+
properties: {
|
|
1994
|
+
docs: {
|
|
1995
|
+
type: 'array',
|
|
1996
|
+
required: true,
|
|
1997
|
+
items: {
|
|
1998
|
+
type: 'object',
|
|
1999
|
+
additionalProperties: false,
|
|
2000
|
+
properties: {
|
|
2001
|
+
id: { type: 'string', required: true },
|
|
2002
|
+
name: { type: 'string', required: true },
|
|
2003
|
+
bodies: { type: 'number', required: true },
|
|
2004
|
+
ops: { type: 'number', required: true },
|
|
2005
|
+
updatedAt: { type: 'string', required: true },
|
|
2006
|
+
active: { type: 'boolean' },
|
|
2007
|
+
},
|
|
2008
|
+
},
|
|
2009
|
+
},
|
|
2010
|
+
activeDoc: { type: 'string', description: 'Active document id for this session (absent: none).' },
|
|
2011
|
+
},
|
|
2012
|
+
},
|
|
2013
|
+
render: (_args, value) => [{ type: 'text', text: renderDocs(value) }],
|
|
2014
|
+
},
|
|
2015
|
+
isConcurrencySafe: () => true,
|
|
2016
|
+
async execute(_args, exec) {
|
|
2017
|
+
const sessionId = sessionKeyOf(exec);
|
|
2018
|
+
const docs = await deps.registry.list();
|
|
2019
|
+
const activeDoc = await deps.registry.bindingOf(sessionId);
|
|
2020
|
+
return {
|
|
2021
|
+
docs: docs.map((doc) => ({ id: doc.id, name: doc.name, bodies: doc.bodyCount, ops: doc.opCount, updatedAt: doc.updatedAt, ...(doc.id === activeDoc ? { active: true } : {}) })),
|
|
2022
|
+
...(activeDoc === null ? {} : { activeDoc }),
|
|
2023
|
+
};
|
|
2024
|
+
},
|
|
2025
|
+
presentCall: () => ({ card: 'generic', title: '文档列表', kind: 'read' }),
|
|
2026
|
+
presentResult: () => ({ card: 'generic', title: '文档列表' }),
|
|
2027
|
+
});
|
|
2028
|
+
const cadDocNew = defineTool({
|
|
2029
|
+
name: 'cad_doc_new',
|
|
2030
|
+
description: 'Create a new modeling document and make it this session\'s active modeling target (previous documents stay in the file space). ' +
|
|
2031
|
+
'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.',
|
|
2032
|
+
parameters: {
|
|
2033
|
+
name: { type: 'string', description: 'Document name (default: 未命名 N).' },
|
|
2034
|
+
},
|
|
2035
|
+
output: {
|
|
2036
|
+
schema: {
|
|
2037
|
+
type: 'object',
|
|
2038
|
+
additionalProperties: false,
|
|
2039
|
+
properties: {
|
|
2040
|
+
docId: { type: 'string', required: true, description: 'New document id.' },
|
|
2041
|
+
name: { type: 'string', required: true, description: 'Document name.' },
|
|
2042
|
+
bodies: { type: 'number', required: true, description: 'Bodies (0 for a fresh document).' },
|
|
2043
|
+
version: { type: 'number', required: true, description: 'Document version.' },
|
|
2044
|
+
},
|
|
2045
|
+
},
|
|
2046
|
+
render: (_args, value) => [{ type: 'text', text: `新建文档 ${String(value.name)}(后续建模操作都写入该文档)` }],
|
|
2047
|
+
},
|
|
2048
|
+
isConcurrencySafe: () => false,
|
|
2049
|
+
async execute(args, exec) {
|
|
2050
|
+
const sessionId = sessionKeyOf(exec);
|
|
2051
|
+
const doc = await deps.registry.create(args.name);
|
|
2052
|
+
await deps.registry.bind(sessionId, doc.doc.docId);
|
|
2053
|
+
await activateDocument(doc);
|
|
2054
|
+
const name = (await deps.registry.list()).find((meta) => meta.id === doc.doc.docId)?.name ?? doc.doc.docId;
|
|
2055
|
+
return { docId: doc.doc.docId, name, bodies: 0, version: doc.doc.version };
|
|
2056
|
+
},
|
|
2057
|
+
presentCall: (args) => ({ card: 'generic', title: `新建文档 ${String(args.name ?? '')}`.trim(), kind: 'other' }),
|
|
2058
|
+
presentResult: () => ({ card: 'generic', title: '新建文档' }),
|
|
2059
|
+
});
|
|
2060
|
+
const cadDocOpen = defineTool({
|
|
2061
|
+
name: 'cad_doc_open',
|
|
2062
|
+
description: 'Open an existing modeling document and make it this session\'s active modeling target (its bodies load back exactly). ' +
|
|
2063
|
+
'Use when the user asks to continue an earlier model (e.g. “打开发动机文档”) — resolve the id/name with cad_docs first when unsure.',
|
|
2064
|
+
parameters: {
|
|
2065
|
+
doc: docRefParam,
|
|
2066
|
+
},
|
|
2067
|
+
output: {
|
|
2068
|
+
schema: {
|
|
2069
|
+
type: 'object',
|
|
2070
|
+
additionalProperties: false,
|
|
2071
|
+
properties: {
|
|
2072
|
+
docId: { type: 'string', required: true, description: 'Opened document id.' },
|
|
2073
|
+
name: { type: 'string', required: true, description: 'Document name.' },
|
|
2074
|
+
bodies: { type: 'number', required: true, description: 'Bodies in the document.' },
|
|
2075
|
+
triangles: { type: 'number', required: true, description: 'Document triangle count.' },
|
|
2076
|
+
version: { type: 'number', required: true, description: 'Document version.' },
|
|
2077
|
+
sceneUrl: { type: 'string', description: 'Versioned viewer URL (web compositions).' },
|
|
2078
|
+
},
|
|
2079
|
+
},
|
|
2080
|
+
render: (_args, value) => [{ type: 'text', text: `打开文档 ${String(value.name)} · ${Number(value.bodies)} 体 (version ${Number(value.version)})` }],
|
|
2081
|
+
presentationMeta: (_args, value) => metaOf(value),
|
|
2082
|
+
},
|
|
2083
|
+
isConcurrencySafe: () => false,
|
|
2084
|
+
async execute(args, exec) {
|
|
2085
|
+
const sessionId = sessionKeyOf(exec);
|
|
2086
|
+
const meta = await deps.registry.resolve(args.doc);
|
|
2087
|
+
if (meta === null) {
|
|
2088
|
+
const available = (await deps.registry.list()).map((doc) => doc.name).join(' / ');
|
|
2089
|
+
throw new Error(`no document matches "${String(args.doc)}" — available: ${available === '' ? '(none)' : available}`);
|
|
2090
|
+
}
|
|
2091
|
+
await deps.registry.bind(sessionId, meta.id);
|
|
2092
|
+
const doc = await deps.registry.open(meta.id);
|
|
2093
|
+
if (doc === null)
|
|
2094
|
+
throw new Error(`document file missing: ${meta.id}`);
|
|
2095
|
+
await activateDocument(doc);
|
|
2096
|
+
const meshes = [...meshCache.values()];
|
|
2097
|
+
const triangles = meshes.reduce((sum, mesh) => sum + mesh.indices.length / 3, 0);
|
|
2098
|
+
const sceneUrlBase = deps.ensureSceneRoute();
|
|
2099
|
+
const value = {
|
|
2100
|
+
docId: meta.id,
|
|
2101
|
+
name: meta.name,
|
|
2102
|
+
bodies: meshes.length,
|
|
2103
|
+
triangles,
|
|
2104
|
+
version: document.doc.version,
|
|
2105
|
+
};
|
|
2106
|
+
if (meshes.length > 0)
|
|
2107
|
+
await deps.store.publish(meta.id, meshes);
|
|
2108
|
+
if (meshes.length > 0 && sceneUrlBase !== null) {
|
|
2109
|
+
value.sceneUrl = `${sceneUrlBase.replace('/scene', '/bin')}/${meta.id}?v=${document.doc.version}`;
|
|
2110
|
+
}
|
|
2111
|
+
return value;
|
|
2112
|
+
},
|
|
2113
|
+
presentCall: (args) => ({ card: 'generic', title: `打开文档 ${String(args.doc)}`, kind: 'other' }),
|
|
2114
|
+
presentResult: () => ({ card: 'generic', title: '打开文档' }),
|
|
2115
|
+
});
|
|
2116
|
+
const cadDocRename = defineTool({
|
|
2117
|
+
name: 'cad_doc_rename',
|
|
2118
|
+
description: 'Rename a modeling document in the file space.',
|
|
2119
|
+
parameters: {
|
|
2120
|
+
doc: docRefParam,
|
|
2121
|
+
name: { type: 'string', required: true, description: 'New document name.' },
|
|
2122
|
+
},
|
|
2123
|
+
output: {
|
|
2124
|
+
schema: {
|
|
2125
|
+
type: 'object',
|
|
2126
|
+
additionalProperties: false,
|
|
2127
|
+
properties: {
|
|
2128
|
+
docId: { type: 'string', required: true, description: 'Document id.' },
|
|
2129
|
+
name: { type: 'string', required: true, description: 'New name.' },
|
|
2130
|
+
},
|
|
2131
|
+
},
|
|
2132
|
+
render: (_args, value) => [{ type: 'text', text: `文档已重命名为 ${String(value.name)}` }],
|
|
2133
|
+
},
|
|
2134
|
+
isConcurrencySafe: () => false,
|
|
2135
|
+
async execute(args, exec) {
|
|
2136
|
+
void exec;
|
|
2137
|
+
const meta = await deps.registry.resolve(args.doc);
|
|
2138
|
+
if (meta === null)
|
|
2139
|
+
throw new Error(`no document matches "${String(args.doc)}"`);
|
|
2140
|
+
const renamed = await deps.registry.rename(meta.id, args.name);
|
|
2141
|
+
if (renamed === null)
|
|
2142
|
+
throw new Error(`rename failed: ${meta.id}`);
|
|
2143
|
+
return { docId: renamed.id, name: renamed.name };
|
|
2144
|
+
},
|
|
2145
|
+
presentCall: (args) => ({ card: 'generic', title: `重命名文档 ${String(args.doc)}`, kind: 'other' }),
|
|
2146
|
+
presentResult: () => ({ card: 'generic', title: '重命名文档' }),
|
|
2147
|
+
});
|
|
2148
|
+
const cadDocDelete = defineTool({
|
|
2149
|
+
name: 'cad_doc_delete',
|
|
2150
|
+
description: 'Permanently delete a modeling document from the file space (op log + bodies). Requires confirm=true — ask the user before calling. ' +
|
|
2151
|
+
'To delete a single body inside a document, use cad_delete instead.',
|
|
2152
|
+
parameters: {
|
|
2153
|
+
doc: docRefParam,
|
|
2154
|
+
confirm: { type: 'boolean', required: true, description: 'Must be explicitly true to delete.' },
|
|
2155
|
+
},
|
|
2156
|
+
output: {
|
|
2157
|
+
schema: {
|
|
2158
|
+
type: 'object',
|
|
2159
|
+
additionalProperties: false,
|
|
2160
|
+
properties: {
|
|
2161
|
+
deleted: { type: 'string', required: true, description: 'Deleted document id.' },
|
|
2162
|
+
name: { type: 'string', required: true, description: 'Deleted document name.' },
|
|
2163
|
+
},
|
|
2164
|
+
},
|
|
2165
|
+
render: (_args, value) => [{ type: 'text', text: `已删除文档 ${String(value.name)}` }],
|
|
2166
|
+
},
|
|
2167
|
+
isConcurrencySafe: () => false,
|
|
2168
|
+
async execute(args, exec) {
|
|
2169
|
+
void exec;
|
|
2170
|
+
if (args.confirm !== true) {
|
|
2171
|
+
throw new Error('pass confirm=true to delete — this permanently removes the document and its bodies');
|
|
2172
|
+
}
|
|
2173
|
+
const meta = await deps.registry.resolve(args.doc);
|
|
2174
|
+
if (meta === null)
|
|
2175
|
+
throw new Error(`no document matches "${String(args.doc)}"`);
|
|
2176
|
+
const removed = await deps.registry.remove(meta.id);
|
|
2177
|
+
if (!removed)
|
|
2178
|
+
throw new Error(`delete failed: ${meta.id}`);
|
|
2179
|
+
// Drop the active-document state if it was the deleted one; the next
|
|
2180
|
+
// modeling op lazily creates a fresh document for its session.
|
|
2181
|
+
if (document.doc.docId === meta.id) {
|
|
2182
|
+
document = new ModelDocument(deps.workspaceRoot);
|
|
2183
|
+
meshCache.clear();
|
|
2184
|
+
drawingSheets = new Map();
|
|
2185
|
+
lastDrawingId = null;
|
|
2186
|
+
syncedEpoch = -1;
|
|
2187
|
+
}
|
|
2188
|
+
return { deleted: meta.id, name: meta.name };
|
|
2189
|
+
},
|
|
2190
|
+
presentCall: (args) => ({ card: 'generic', title: `删除文档 ${String(args.doc)}`, kind: 'delete' }),
|
|
2191
|
+
presentResult: () => ({ card: 'generic', title: '删除文档' }),
|
|
2192
|
+
});
|
|
2193
|
+
// ── constraint tools (Ansatz solver) ──────────────────────────────────────
|
|
2194
|
+
const constraintTools = createConstraintTools({
|
|
2195
|
+
deps,
|
|
2196
|
+
// Read the ACTIVE document per call: multi-document sessions swap it.
|
|
2197
|
+
getDocument: () => document,
|
|
2198
|
+
meshCache,
|
|
2199
|
+
constraintState,
|
|
2200
|
+
resolveDoc,
|
|
2201
|
+
syncAssembly,
|
|
2202
|
+
assemblyMetaOf,
|
|
2203
|
+
});
|
|
2204
|
+
return [
|
|
2205
|
+
cadCreatePrim,
|
|
2206
|
+
cadSketchNew,
|
|
2207
|
+
cadSketchEdit,
|
|
2208
|
+
cadSketchList,
|
|
2209
|
+
cadSketchDelete,
|
|
2210
|
+
cadExtrude,
|
|
2211
|
+
cadRevolve,
|
|
2212
|
+
cadChamfer,
|
|
2213
|
+
cadShell,
|
|
2214
|
+
cadDraft,
|
|
2215
|
+
cadPattern,
|
|
2216
|
+
cadLoft,
|
|
2217
|
+
cadSweep,
|
|
2218
|
+
cadBoolean,
|
|
2219
|
+
cadFillet,
|
|
2220
|
+
cadTransform,
|
|
2221
|
+
cadExport,
|
|
2222
|
+
cadDelete,
|
|
2223
|
+
cadVolume,
|
|
2224
|
+
cadDrawing,
|
|
2225
|
+
cadAssemblyInsert,
|
|
2226
|
+
cadAssemblyMove,
|
|
2227
|
+
cadAssemblyRemove,
|
|
2228
|
+
cadAssemblyJoint,
|
|
2229
|
+
cadAssemblyConnect,
|
|
2230
|
+
cadAssemblyDrive,
|
|
2231
|
+
cadAssemblySolve,
|
|
2232
|
+
cadAssemblyExplode,
|
|
2233
|
+
cadAssemblyInfo,
|
|
2234
|
+
cadStepImport,
|
|
2235
|
+
cadDocs,
|
|
2236
|
+
cadDocNew,
|
|
2237
|
+
cadDocOpen,
|
|
2238
|
+
cadDocRename,
|
|
2239
|
+
cadDocDelete,
|
|
2240
|
+
...constraintTools,
|
|
2241
|
+
];
|
|
2242
|
+
}
|
|
2243
|
+
export const MODEL_TOOL_NAMES = [
|
|
2244
|
+
'cad_create_prim',
|
|
2245
|
+
'cad_sketch_new',
|
|
2246
|
+
'cad_sketch_edit',
|
|
2247
|
+
'cad_sketch_list',
|
|
2248
|
+
'cad_sketch_delete',
|
|
2249
|
+
'cad_extrude_profile',
|
|
2250
|
+
'cad_revolve',
|
|
2251
|
+
'cad_chamfer',
|
|
2252
|
+
'cad_shell',
|
|
2253
|
+
'cad_draft',
|
|
2254
|
+
'cad_pattern',
|
|
2255
|
+
'cad_loft',
|
|
2256
|
+
'cad_sweep',
|
|
2257
|
+
'cad_boolean',
|
|
2258
|
+
'cad_fillet',
|
|
2259
|
+
'cad_transform',
|
|
2260
|
+
'cad_export',
|
|
2261
|
+
'cad_delete',
|
|
2262
|
+
'cad_volume',
|
|
2263
|
+
'cad_drawing',
|
|
2264
|
+
'cad_assembly_insert',
|
|
2265
|
+
'cad_assembly_move',
|
|
2266
|
+
'cad_assembly_remove',
|
|
2267
|
+
'cad_assembly_joint',
|
|
2268
|
+
'cad_assembly_connect',
|
|
2269
|
+
'cad_assembly_drive',
|
|
2270
|
+
'cad_assembly_solve',
|
|
2271
|
+
'cad_assembly_explode',
|
|
2272
|
+
'cad_assembly_info',
|
|
2273
|
+
'cad_step_import',
|
|
2274
|
+
'cad_docs',
|
|
2275
|
+
'cad_doc_new',
|
|
2276
|
+
'cad_doc_open',
|
|
2277
|
+
'cad_doc_rename',
|
|
2278
|
+
'cad_doc_delete',
|
|
2279
|
+
'cad_constraint',
|
|
2280
|
+
'cad_solve',
|
|
2281
|
+
'cad_motion',
|
|
2282
|
+
];
|