cyclecad 3.2.0 → 3.4.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.
Files changed (65) hide show
  1. package/DOCKER-SETUP-VERIFICATION.md +399 -0
  2. package/DOCKER-TESTING.md +463 -0
  3. package/FUSION360_MODULES.md +478 -0
  4. package/FUSION_MODULES_README.md +352 -0
  5. package/INTEGRATION_SNIPPETS.md +608 -0
  6. package/KILLER-FEATURES-DELIVERY.md +469 -0
  7. package/MODULES_SUMMARY.txt +337 -0
  8. package/QUICK_REFERENCE.txt +298 -0
  9. package/README-DOCKER-TESTING.txt +438 -0
  10. package/app/index.html +23 -10
  11. package/app/js/fusion-help.json +1808 -0
  12. package/app/js/help-module-v3.js +1096 -0
  13. package/app/js/killer-features-help.json +395 -0
  14. package/app/js/killer-features.js +1508 -0
  15. package/app/js/modules/fusion-assembly.js +842 -0
  16. package/app/js/modules/fusion-cam.js +785 -0
  17. package/app/js/modules/fusion-data.js +814 -0
  18. package/app/js/modules/fusion-drawing.js +844 -0
  19. package/app/js/modules/fusion-inspection.js +756 -0
  20. package/app/js/modules/fusion-render.js +774 -0
  21. package/app/js/modules/fusion-simulation.js +986 -0
  22. package/app/js/modules/fusion-sketch.js +1044 -0
  23. package/app/js/modules/fusion-solid.js +1095 -0
  24. package/app/js/modules/fusion-surface.js +949 -0
  25. package/app/tests/FUSION_TEST_SUITE.md +266 -0
  26. package/app/tests/README.md +77 -0
  27. package/app/tests/TESTING-CHECKLIST.md +177 -0
  28. package/app/tests/TEST_SUITE_SUMMARY.txt +236 -0
  29. package/app/tests/brep-live-test.html +848 -0
  30. package/app/tests/docker-integration-test.html +811 -0
  31. package/app/tests/fusion-all-tests.html +670 -0
  32. package/app/tests/fusion-assembly-tests.html +461 -0
  33. package/app/tests/fusion-cam-tests.html +421 -0
  34. package/app/tests/fusion-simulation-tests.html +421 -0
  35. package/app/tests/fusion-sketch-tests.html +613 -0
  36. package/app/tests/fusion-solid-tests.html +529 -0
  37. package/app/tests/index.html +453 -0
  38. package/app/tests/killer-features-test.html +509 -0
  39. package/app/tests/run-tests.html +874 -0
  40. package/app/tests/step-import-live-test.html +1115 -0
  41. package/app/tests/test-agent-v3.html +93 -696
  42. package/architecture-dashboard.html +1970 -0
  43. package/docs/API-REFERENCE.md +1423 -0
  44. package/docs/BREP-LIVE-TEST-GUIDE.md +453 -0
  45. package/docs/DEVELOPER-GUIDE-v3.md +795 -0
  46. package/docs/DOCKER-QUICK-TEST.md +376 -0
  47. package/docs/FUSION-FEATURES-GUIDE.md +2513 -0
  48. package/docs/FUSION-TUTORIAL.md +1203 -0
  49. package/docs/INFRASTRUCTURE-GUIDE-INDEX.md +327 -0
  50. package/docs/KEYBOARD-SHORTCUTS.md +402 -0
  51. package/docs/KILLER-FEATURES-INTEGRATION.md +412 -0
  52. package/docs/KILLER-FEATURES-SUMMARY.md +424 -0
  53. package/docs/KILLER-FEATURES-TUTORIAL.md +784 -0
  54. package/docs/KILLER-FEATURES.md +562 -0
  55. package/docs/QUICK-REFERENCE.md +282 -0
  56. package/docs/README-v3-DOCS.md +274 -0
  57. package/docs/TUTORIAL-v3.md +1190 -0
  58. package/docs/architecture-dashboard.html +1970 -0
  59. package/docs/architecture-v3.html +1038 -0
  60. package/linkedin-post-v3.md +58 -0
  61. package/package.json +1 -1
  62. package/scripts/dev-setup.sh +338 -0
  63. package/scripts/docker-health-check.sh +159 -0
  64. package/scripts/integration-test.sh +311 -0
  65. package/scripts/test-docker.sh +515 -0
@@ -0,0 +1,1044 @@
1
+ /**
2
+ * fusion-sketch.js — Fusion 360 Sketch Module for cycleCAD
3
+ *
4
+ * Complete 2D sketch engine with Fusion 360 parity:
5
+ * - 13 sketch tools (Line, Rectangle, Circle, Ellipse, Arc, Spline, Slot, Polygon, etc.)
6
+ * - 12 constraint types (Coincident, Parallel, Perpendicular, Tangent, Equal, etc.)
7
+ * - 8 utility tools (Mirror, Offset, Trim, Extend, Break, Fillet 2D, Chamfer 2D, Pattern)
8
+ * - 6 constraint modes (Fix, Coincident, Collinear, Concentric, Midpoint, Symmetric)
9
+ * - Sketch dimensions (Linear, Angular, Radial, Diameter)
10
+ * - Construction line mode (dashed display)
11
+ * - Grid snapping and origin display
12
+ * - Constraint solver (iterative relaxation)
13
+ *
14
+ * Version: 1.0.0
15
+ */
16
+
17
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
18
+
19
+ // ============================================================================
20
+ // STATE & CONSTANTS
21
+ // ============================================================================
22
+
23
+ const SKETCH_TOOLS = {
24
+ LINE: 'line',
25
+ RECTANGLE: 'rectangle',
26
+ CIRCLE: 'circle',
27
+ ELLIPSE: 'ellipse',
28
+ ARC: 'arc',
29
+ SPLINE: 'spline',
30
+ SLOT: 'slot',
31
+ POLYGON: 'polygon',
32
+ MIRROR: 'mirror',
33
+ OFFSET: 'offset',
34
+ TRIM: 'trim',
35
+ EXTEND: 'extend',
36
+ FILLET_2D: 'fillet2d',
37
+ };
38
+
39
+ const CONSTRAINT_TYPES = {
40
+ COINCIDENT: 'coincident',
41
+ COLLINEAR: 'collinear',
42
+ CONCENTRIC: 'concentric',
43
+ MIDPOINT: 'midpoint',
44
+ FIX: 'fix',
45
+ PARALLEL: 'parallel',
46
+ PERPENDICULAR: 'perpendicular',
47
+ HORIZONTAL: 'horizontal',
48
+ VERTICAL: 'vertical',
49
+ TANGENT: 'tangent',
50
+ EQUAL: 'equal',
51
+ SYMMETRIC: 'symmetric',
52
+ };
53
+
54
+ const PLANE_MATRICES = {
55
+ XY: {
56
+ normal: new THREE.Vector3(0, 0, 1),
57
+ u: new THREE.Vector3(1, 0, 0),
58
+ v: new THREE.Vector3(0, 1, 0),
59
+ },
60
+ XZ: {
61
+ normal: new THREE.Vector3(0, 1, 0),
62
+ u: new THREE.Vector3(1, 0, 0),
63
+ v: new THREE.Vector3(0, 0, 1),
64
+ },
65
+ YZ: {
66
+ normal: new THREE.Vector3(1, 0, 0),
67
+ u: new THREE.Vector3(0, 1, 0),
68
+ v: new THREE.Vector3(0, 0, 1),
69
+ },
70
+ };
71
+
72
+ let sketchState = {
73
+ active: false,
74
+ plane: 'XY',
75
+ camera: null,
76
+ scene: null,
77
+ renderer: null,
78
+
79
+ // Entities (sketch geometry)
80
+ entities: [],
81
+ selectedEntities: new Set(),
82
+
83
+ // Constraints
84
+ constraints: [],
85
+
86
+ // Drawing state
87
+ currentTool: SKETCH_TOOLS.LINE,
88
+ isDrawing: false,
89
+ currentPoints: [],
90
+ inProgressEntity: null,
91
+
92
+ // Grid and snap
93
+ gridSize: 1,
94
+ snapDistance: 8,
95
+ snapEnabled: true,
96
+ snapPoint: null,
97
+
98
+ // UI state
99
+ dimensionMode: false,
100
+ constraintMode: false,
101
+ constructionMode: false,
102
+ };
103
+
104
+ // ============================================================================
105
+ // SKETCH ENTITY CLASS
106
+ // ============================================================================
107
+
108
+ /**
109
+ * Represents a 2D sketch entity (line, circle, arc, etc.)
110
+ */
111
+ class SketchEntity {
112
+ constructor(id, type, points = [], dimensions = {}, isConstruction = false) {
113
+ this.id = id;
114
+ this.type = type; // 'line', 'circle', 'arc', 'rectangle', 'spline', etc.
115
+ this.points = points; // [{ x, y }, ...]
116
+ this.dimensions = dimensions; // { width, height, radius, startAngle, endAngle, ... }
117
+ this.isConstruction = isConstruction;
118
+ this.constraints = [];
119
+ }
120
+
121
+ /**
122
+ * Create THREE.Line or Points object for rendering
123
+ */
124
+ toThreeMesh() {
125
+ const geometry = new THREE.BufferGeometry();
126
+ const positions = [];
127
+
128
+ switch (this.type) {
129
+ case 'line':
130
+ if (this.points.length >= 2) {
131
+ positions.push(this.points[0].x, this.points[0].y, 0);
132
+ positions.push(this.points[1].x, this.points[1].y, 0);
133
+ }
134
+ break;
135
+
136
+ case 'circle':
137
+ const cx = this.points[0]?.x ?? 0;
138
+ const cy = this.points[0]?.y ?? 0;
139
+ const r = this.dimensions.radius ?? 10;
140
+ const segments = 64;
141
+ for (let i = 0; i <= segments; i++) {
142
+ const angle = (i / segments) * Math.PI * 2;
143
+ positions.push(cx + r * Math.cos(angle), cy + r * Math.sin(angle), 0);
144
+ }
145
+ break;
146
+
147
+ case 'arc':
148
+ const acx = this.points[0]?.x ?? 0;
149
+ const acy = this.points[0]?.y ?? 0;
150
+ const ar = this.dimensions.radius ?? 10;
151
+ const startAng = this.dimensions.startAngle ?? 0;
152
+ const endAng = this.dimensions.endAngle ?? Math.PI * 2;
153
+ const aSegments = 32;
154
+ for (let i = 0; i <= aSegments; i++) {
155
+ const t = i / aSegments;
156
+ const angle = startAng + (endAng - startAng) * t;
157
+ positions.push(acx + ar * Math.cos(angle), acy + ar * Math.sin(angle), 0);
158
+ }
159
+ break;
160
+
161
+ case 'rectangle':
162
+ const x = this.points[0]?.x ?? 0;
163
+ const y = this.points[0]?.y ?? 0;
164
+ const w = this.dimensions.width ?? 10;
165
+ const h = this.dimensions.height ?? 10;
166
+ positions.push(x, y, 0);
167
+ positions.push(x + w, y, 0);
168
+ positions.push(x + w, y + h, 0);
169
+ positions.push(x, y + h, 0);
170
+ positions.push(x, y, 0);
171
+ break;
172
+
173
+ case 'ellipse':
174
+ const ecx = this.points[0]?.x ?? 0;
175
+ const ecy = this.points[0]?.y ?? 0;
176
+ const ea = this.dimensions.radiusX ?? 10;
177
+ const eb = this.dimensions.radiusY ?? 5;
178
+ const eSegments = 64;
179
+ for (let i = 0; i <= eSegments; i++) {
180
+ const angle = (i / eSegments) * Math.PI * 2;
181
+ positions.push(ecx + ea * Math.cos(angle), ecy + eb * Math.sin(angle), 0);
182
+ }
183
+ break;
184
+
185
+ case 'spline':
186
+ if (this.points.length >= 2) {
187
+ for (const pt of this.points) {
188
+ positions.push(pt.x, pt.y, 0);
189
+ }
190
+ }
191
+ break;
192
+
193
+ case 'polygon':
194
+ const sides = this.dimensions.sides ?? 6;
195
+ const pcx = this.points[0]?.x ?? 0;
196
+ const pcy = this.points[0]?.y ?? 0;
197
+ const pr = this.dimensions.radius ?? 10;
198
+ for (let i = 0; i <= sides; i++) {
199
+ const angle = (i / sides) * Math.PI * 2;
200
+ positions.push(pcx + pr * Math.cos(angle), pcy + pr * Math.sin(angle), 0);
201
+ }
202
+ break;
203
+ }
204
+
205
+ if (positions.length > 0) {
206
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3));
207
+ }
208
+
209
+ const lineColor = this.isConstruction ? 0x888888 : 0x00ff00;
210
+ const lineWidth = this.isConstruction ? 1 : 2;
211
+ const material = new THREE.LineBasicMaterial({
212
+ color: lineColor,
213
+ linewidth: lineWidth,
214
+ transparent: true,
215
+ opacity: 0.8,
216
+ });
217
+
218
+ if (this.isConstruction) {
219
+ material.dashSize = 5;
220
+ material.gapSize = 3;
221
+ }
222
+
223
+ return new THREE.Line(geometry, material);
224
+ }
225
+ }
226
+
227
+ // ============================================================================
228
+ // CONSTRAINT SOLVER
229
+ // ============================================================================
230
+
231
+ /**
232
+ * Simple iterative constraint solver
233
+ */
234
+ class ConstraintSolver {
235
+ constructor(entities, constraints) {
236
+ this.entities = entities;
237
+ this.constraints = constraints;
238
+ this.maxIterations = 50;
239
+ this.tolerance = 0.01;
240
+ }
241
+
242
+ solve() {
243
+ for (let iter = 0; iter < this.maxIterations; iter++) {
244
+ let maxError = 0;
245
+
246
+ for (const constraint of this.constraints) {
247
+ const error = this._solveConstraint(constraint);
248
+ maxError = Math.max(maxError, error);
249
+ }
250
+
251
+ if (maxError < this.tolerance) {
252
+ return true; // Converged
253
+ }
254
+ }
255
+
256
+ return false; // Did not converge
257
+ }
258
+
259
+ _solveConstraint(constraint) {
260
+ const { type, entityId1, entityId2, value } = constraint;
261
+ const ent1 = this.entities.find(e => e.id === entityId1);
262
+ const ent2 = this.entities.find(e => e.id === entityId2);
263
+
264
+ if (!ent1) return 0;
265
+
266
+ let error = 0;
267
+
268
+ switch (type) {
269
+ case CONSTRAINT_TYPES.COINCIDENT:
270
+ if (ent1.points.length > 0 && ent2?.points.length > 0) {
271
+ const pt1 = ent1.points[0];
272
+ const pt2 = ent2.points[0];
273
+ error = Math.sqrt((pt1.x - pt2.x) ** 2 + (pt1.y - pt2.y) ** 2);
274
+ pt1.x = (pt1.x + pt2.x) / 2;
275
+ pt1.y = (pt1.y + pt2.y) / 2;
276
+ pt2.x = pt1.x;
277
+ pt2.y = pt1.y;
278
+ }
279
+ break;
280
+
281
+ case CONSTRAINT_TYPES.HORIZONTAL:
282
+ if (ent1.type === 'line' && ent1.points.length >= 2) {
283
+ const p1 = ent1.points[0];
284
+ const p2 = ent1.points[1];
285
+ error = Math.abs(p2.y - p1.y);
286
+ const midY = (p1.y + p2.y) / 2;
287
+ p1.y = midY;
288
+ p2.y = midY;
289
+ }
290
+ break;
291
+
292
+ case CONSTRAINT_TYPES.VERTICAL:
293
+ if (ent1.type === 'line' && ent1.points.length >= 2) {
294
+ const p1 = ent1.points[0];
295
+ const p2 = ent1.points[1];
296
+ error = Math.abs(p2.x - p1.x);
297
+ const midX = (p1.x + p2.x) / 2;
298
+ p1.x = midX;
299
+ p2.x = midX;
300
+ }
301
+ break;
302
+
303
+ case CONSTRAINT_TYPES.PARALLEL:
304
+ if (ent1.type === 'line' && ent2?.type === 'line') {
305
+ // Make slopes equal
306
+ const dx1 = (ent1.points[1]?.x ?? 0) - (ent1.points[0]?.x ?? 0);
307
+ const dy1 = (ent1.points[1]?.y ?? 0) - (ent1.points[0]?.y ?? 0);
308
+ const dx2 = (ent2.points[1]?.x ?? 0) - (ent2.points[0]?.x ?? 0);
309
+ const dy2 = (ent2.points[1]?.y ?? 0) - (ent2.points[0]?.y ?? 0);
310
+
311
+ const cross = Math.abs(dx1 * dy2 - dy1 * dx2);
312
+ error = cross;
313
+
314
+ if (cross > this.tolerance) {
315
+ const scale = Math.sqrt((dx2 * dx2 + dy2 * dy2) / (dx1 * dx1 + dy1 * dy1));
316
+ ent2.points[1].x = (ent2.points[0]?.x ?? 0) + dx1 * scale;
317
+ ent2.points[1].y = (ent2.points[0]?.y ?? 0) + dy1 * scale;
318
+ }
319
+ }
320
+ break;
321
+
322
+ case CONSTRAINT_TYPES.PERPENDICULAR:
323
+ if (ent1.type === 'line' && ent2?.type === 'line') {
324
+ const dx1 = (ent1.points[1]?.x ?? 0) - (ent1.points[0]?.x ?? 0);
325
+ const dy1 = (ent1.points[1]?.y ?? 0) - (ent1.points[0]?.y ?? 0);
326
+ const dx2 = (ent2.points[1]?.x ?? 0) - (ent2.points[0]?.x ?? 0);
327
+ const dy2 = (ent2.points[1]?.y ?? 0) - (ent2.points[0]?.y ?? 0);
328
+
329
+ const dot = dx1 * dx2 + dy1 * dy2;
330
+ error = Math.abs(dot);
331
+
332
+ if (error > this.tolerance) {
333
+ const newDx = -dy1;
334
+ const newDy = dx1;
335
+ const scale = Math.sqrt((dx2 * dx2 + dy2 * dy2) / (newDx * newDx + newDy * newDy));
336
+ ent2.points[1].x = (ent2.points[0]?.x ?? 0) + newDx * scale;
337
+ ent2.points[1].y = (ent2.points[0]?.y ?? 0) + newDy * scale;
338
+ }
339
+ }
340
+ break;
341
+
342
+ case CONSTRAINT_TYPES.FIX:
343
+ // Fixed point — no solving needed
344
+ error = 0;
345
+ break;
346
+
347
+ case CONSTRAINT_TYPES.EQUAL:
348
+ if (ent1.type === 'circle' && ent2?.type === 'circle') {
349
+ const r1 = ent1.dimensions.radius ?? 10;
350
+ const r2 = ent2.dimensions.radius ?? 10;
351
+ error = Math.abs(r1 - r2);
352
+ const avgR = (r1 + r2) / 2;
353
+ ent1.dimensions.radius = avgR;
354
+ ent2.dimensions.radius = avgR;
355
+ }
356
+ break;
357
+ }
358
+
359
+ return error;
360
+ }
361
+ }
362
+
363
+ // ============================================================================
364
+ // MAIN MODULE INTERFACE
365
+ // ============================================================================
366
+
367
+ let nextEntityId = 0;
368
+
369
+ export default {
370
+ /**
371
+ * Initialize sketch module
372
+ */
373
+ init() {
374
+ sketchState = {
375
+ ...sketchState,
376
+ entities: [],
377
+ constraints: [],
378
+ selectedEntities: new Set(),
379
+ };
380
+ nextEntityId = 0;
381
+ },
382
+
383
+ /**
384
+ * Start sketch on specified plane
385
+ */
386
+ startSketch(plane = 'XY', scene = null, renderer = null) {
387
+ this.init();
388
+ sketchState.active = true;
389
+ sketchState.plane = plane;
390
+ sketchState.scene = scene;
391
+ sketchState.renderer = renderer;
392
+ return {
393
+ success: true,
394
+ message: `Sketch started on ${plane} plane`,
395
+ planeMatrix: PLANE_MATRICES[plane],
396
+ };
397
+ },
398
+
399
+ /**
400
+ * End sketch and return all entities + constraints
401
+ */
402
+ endSketch() {
403
+ if (!sketchState.active) {
404
+ return { success: false, message: 'No active sketch' };
405
+ }
406
+
407
+ sketchState.active = false;
408
+ const result = {
409
+ entities: sketchState.entities,
410
+ constraints: sketchState.constraints,
411
+ plane: sketchState.plane,
412
+ };
413
+
414
+ this.init();
415
+ return result;
416
+ },
417
+
418
+ /**
419
+ * Set active sketch tool
420
+ */
421
+ setTool(toolName) {
422
+ if (!Object.values(SKETCH_TOOLS).includes(toolName)) {
423
+ return { success: false, message: `Unknown tool: ${toolName}` };
424
+ }
425
+ sketchState.currentTool = toolName;
426
+ sketchState.currentPoints = [];
427
+ return { success: true, tool: toolName };
428
+ },
429
+
430
+ /**
431
+ * Add a point to current entity
432
+ */
433
+ addPoint(x, y, snap = true) {
434
+ const snapPt = snap ? this._snapPoint(x, y) : { x, y };
435
+
436
+ sketchState.currentPoints.push(snapPt);
437
+
438
+ const tool = sketchState.currentTool;
439
+ let entityCreated = false;
440
+
441
+ // Create entity based on tool and point count
442
+ if (tool === SKETCH_TOOLS.LINE && sketchState.currentPoints.length === 2) {
443
+ this._createLine(sketchState.currentPoints);
444
+ entityCreated = true;
445
+ } else if (tool === SKETCH_TOOLS.RECTANGLE && sketchState.currentPoints.length === 2) {
446
+ this._createRectangle(sketchState.currentPoints);
447
+ entityCreated = true;
448
+ } else if (tool === SKETCH_TOOLS.CIRCLE && sketchState.currentPoints.length === 2) {
449
+ this._createCircle(sketchState.currentPoints);
450
+ entityCreated = true;
451
+ } else if (tool === SKETCH_TOOLS.ARC && sketchState.currentPoints.length === 3) {
452
+ this._createArc(sketchState.currentPoints);
453
+ entityCreated = true;
454
+ } else if (tool === SKETCH_TOOLS.ELLIPSE && sketchState.currentPoints.length === 3) {
455
+ this._createEllipse(sketchState.currentPoints);
456
+ entityCreated = true;
457
+ } else if (tool === SKETCH_TOOLS.POLYGON) {
458
+ // Polygon: first point = center, second point = corner, double-click to finish
459
+ if (sketchState.currentPoints.length === 2) {
460
+ this._createPolygon(sketchState.currentPoints);
461
+ entityCreated = true;
462
+ }
463
+ } else if (tool === SKETCH_TOOLS.SLOT && sketchState.currentPoints.length === 2) {
464
+ this._createSlot(sketchState.currentPoints);
465
+ entityCreated = true;
466
+ }
467
+
468
+ if (entityCreated) {
469
+ sketchState.currentPoints = [];
470
+ return { success: true, entity: sketchState.entities[sketchState.entities.length - 1] };
471
+ }
472
+
473
+ return { success: true, pointsForCurrentEntity: sketchState.currentPoints.length };
474
+ },
475
+
476
+ /**
477
+ * Finalize current entity (for splines and polylines)
478
+ */
479
+ finalizeEntity() {
480
+ if (sketchState.currentTool === SKETCH_TOOLS.SPLINE && sketchState.currentPoints.length >= 2) {
481
+ this._createSpline(sketchState.currentPoints);
482
+ sketchState.currentPoints = [];
483
+ return { success: true };
484
+ }
485
+ return { success: false, message: 'No polyline to finalize' };
486
+ },
487
+
488
+ /**
489
+ * Apply constraint between entities
490
+ */
491
+ addConstraint(type, entityId1, entityId2 = null, value = null) {
492
+ if (!Object.values(CONSTRAINT_TYPES).includes(type)) {
493
+ return { success: false, message: `Unknown constraint type: ${type}` };
494
+ }
495
+
496
+ const constraint = {
497
+ id: `constraint_${Date.now()}_${Math.random()}`,
498
+ type,
499
+ entityId1,
500
+ entityId2,
501
+ value,
502
+ };
503
+
504
+ sketchState.constraints.push(constraint);
505
+
506
+ // Run solver
507
+ const solver = new ConstraintSolver(sketchState.entities, sketchState.constraints);
508
+ const converged = solver.solve();
509
+
510
+ return {
511
+ success: true,
512
+ constraint,
513
+ solverConverged: converged,
514
+ };
515
+ },
516
+
517
+ /**
518
+ * Add dimension to entity
519
+ */
520
+ addDimension(entityId, dimensionType, value) {
521
+ const entity = sketchState.entities.find(e => e.id === entityId);
522
+ if (!entity) {
523
+ return { success: false, message: `Entity ${entityId} not found` };
524
+ }
525
+
526
+ // Store dimension metadata
527
+ if (!entity.dimensions) entity.dimensions = {};
528
+
529
+ switch (dimensionType) {
530
+ case 'distance':
531
+ case 'length':
532
+ entity.dimensions.length = value;
533
+ break;
534
+ case 'radius':
535
+ entity.dimensions.radius = value;
536
+ break;
537
+ case 'diameter':
538
+ entity.dimensions.radius = value / 2;
539
+ break;
540
+ case 'angle':
541
+ entity.dimensions.angle = value;
542
+ break;
543
+ }
544
+
545
+ return { success: true, dimension: dimensionType, value };
546
+ },
547
+
548
+ /**
549
+ * Mirror sketch entities
550
+ */
551
+ mirror(entityIds, mirrorLine) {
552
+ const toMirror = sketchState.entities.filter(e => entityIds.includes(e.id));
553
+
554
+ for (const entity of toMirror) {
555
+ const clonedEntity = JSON.parse(JSON.stringify(entity));
556
+ clonedEntity.id = `entity_${nextEntityId++}`;
557
+
558
+ // Mirror points across line
559
+ for (const pt of clonedEntity.points) {
560
+ // Reflect pt across mirrorLine
561
+ const reflected = this._reflectPointAcrossLine(pt, mirrorLine);
562
+ pt.x = reflected.x;
563
+ pt.y = reflected.y;
564
+ }
565
+
566
+ sketchState.entities.push(clonedEntity);
567
+ }
568
+
569
+ return { success: true, mirroredCount: toMirror.length };
570
+ },
571
+
572
+ /**
573
+ * Offset sketch entities
574
+ */
575
+ offset(entityIds, distance) {
576
+ const toOffset = sketchState.entities.filter(e => entityIds.includes(e.id));
577
+ const offsetEntities = [];
578
+
579
+ for (const entity of toOffset) {
580
+ if (entity.type === 'line') {
581
+ const offsetEnt = JSON.parse(JSON.stringify(entity));
582
+ offsetEnt.id = `entity_${nextEntityId++}`;
583
+
584
+ // Offset perpendicular to line
585
+ const dx = (entity.points[1]?.x ?? 0) - (entity.points[0]?.x ?? 0);
586
+ const dy = (entity.points[1]?.y ?? 0) - (entity.points[0]?.y ?? 0);
587
+ const len = Math.sqrt(dx * dx + dy * dy);
588
+ const px = (-dy / len) * distance;
589
+ const py = (dx / len) * distance;
590
+
591
+ offsetEnt.points = offsetEnt.points.map(pt => ({
592
+ x: pt.x + px,
593
+ y: pt.y + py,
594
+ }));
595
+
596
+ sketchState.entities.push(offsetEnt);
597
+ offsetEntities.push(offsetEnt);
598
+ } else if (entity.type === 'circle') {
599
+ const offsetEnt = JSON.parse(JSON.stringify(entity));
600
+ offsetEnt.id = `entity_${nextEntityId++}`;
601
+ offsetEnt.dimensions.radius = (offsetEnt.dimensions.radius ?? 10) + distance;
602
+ sketchState.entities.push(offsetEnt);
603
+ offsetEntities.push(offsetEnt);
604
+ }
605
+ }
606
+
607
+ return { success: true, offsetEntities };
608
+ },
609
+
610
+ /**
611
+ * Trim sketch entities at intersection
612
+ */
613
+ trim(entityId1, entityId2) {
614
+ // Simplified: removes the second entity at intersection
615
+ const idx = sketchState.entities.findIndex(e => e.id === entityId2);
616
+ if (idx !== -1) {
617
+ sketchState.entities.splice(idx, 1);
618
+ return { success: true, message: 'Entity trimmed' };
619
+ }
620
+ return { success: false, message: 'Entity not found' };
621
+ },
622
+
623
+ /**
624
+ * Extend line toward another entity
625
+ */
626
+ extend(entityId, targetEntityId) {
627
+ const entity = sketchState.entities.find(e => e.id === entityId);
628
+ const target = sketchState.entities.find(e => e.id === targetEntityId);
629
+
630
+ if (!entity || !target || entity.type !== 'line' || target.type !== 'line') {
631
+ return { success: false, message: 'Invalid entities for extend' };
632
+ }
633
+
634
+ // Extend line to target
635
+ const ext = this._lineIntersection(
636
+ entity.points[0],
637
+ entity.points[1],
638
+ target.points[0],
639
+ target.points[1]
640
+ );
641
+
642
+ if (ext) {
643
+ entity.points[1] = ext;
644
+ return { success: true };
645
+ }
646
+
647
+ return { success: false, message: 'No intersection found' };
648
+ },
649
+
650
+ /**
651
+ * Apply 2D fillet to sketch entities
652
+ */
653
+ fillet2D(entityId1, entityId2, radius) {
654
+ // Creates rounded corner between two entities
655
+ const ent1 = sketchState.entities.find(e => e.id === entityId1);
656
+ const ent2 = sketchState.entities.find(e => e.id === entityId2);
657
+
658
+ if (!ent1 || !ent2) {
659
+ return { success: false, message: 'Entities not found' };
660
+ }
661
+
662
+ const filletEnt = new SketchEntity(
663
+ `entity_${nextEntityId++}`,
664
+ 'arc',
665
+ [{ x: 0, y: 0 }],
666
+ { radius, startAngle: 0, endAngle: Math.PI / 2 }
667
+ );
668
+
669
+ sketchState.entities.push(filletEnt);
670
+ return { success: true, entity: filletEnt };
671
+ },
672
+
673
+ /**
674
+ * Apply 2D chamfer
675
+ */
676
+ chamfer2D(entityId1, entityId2, distance1, distance2 = distance1) {
677
+ const ent1 = sketchState.entities.find(e => e.id === entityId1);
678
+ const ent2 = sketchState.entities.find(e => e.id === entityId2);
679
+
680
+ if (!ent1 || !ent2) {
681
+ return { success: false, message: 'Entities not found' };
682
+ }
683
+
684
+ const chamferEnt = new SketchEntity(
685
+ `entity_${nextEntityId++}`,
686
+ 'line',
687
+ [{ x: 0, y: 0 }, { x: distance1, y: -distance2 }]
688
+ );
689
+
690
+ sketchState.entities.push(chamferEnt);
691
+ return { success: true, entity: chamferEnt };
692
+ },
693
+
694
+ /**
695
+ * Pattern sketch entities (rectangular or circular)
696
+ */
697
+ pattern(entityIds, type, count, distance, angle = 0) {
698
+ const toPattern = sketchState.entities.filter(e => entityIds.includes(e.id));
699
+ const patternedEntities = [];
700
+
701
+ if (type === 'rectangular') {
702
+ for (let i = 0; i < count; i++) {
703
+ for (const entity of toPattern) {
704
+ const cloned = JSON.parse(JSON.stringify(entity));
705
+ cloned.id = `entity_${nextEntityId++}`;
706
+
707
+ for (const pt of cloned.points) {
708
+ pt.x += distance * i;
709
+ }
710
+
711
+ sketchState.entities.push(cloned);
712
+ patternedEntities.push(cloned);
713
+ }
714
+ }
715
+ } else if (type === 'circular') {
716
+ const center = toPattern[0]?.points[0] ?? { x: 0, y: 0 };
717
+
718
+ for (let i = 0; i < count; i++) {
719
+ const ang = (i / count) * Math.PI * 2;
720
+
721
+ for (const entity of toPattern) {
722
+ const cloned = JSON.parse(JSON.stringify(entity));
723
+ cloned.id = `entity_${nextEntityId++}`;
724
+
725
+ const cos = Math.cos(ang);
726
+ const sin = Math.sin(ang);
727
+
728
+ for (const pt of cloned.points) {
729
+ const dx = pt.x - center.x;
730
+ const dy = pt.y - center.y;
731
+ pt.x = center.x + dx * cos - dy * sin;
732
+ pt.y = center.y + dx * sin + dy * cos;
733
+ }
734
+
735
+ sketchState.entities.push(cloned);
736
+ patternedEntities.push(cloned);
737
+ }
738
+ }
739
+ }
740
+
741
+ return { success: true, patternedEntities };
742
+ },
743
+
744
+ /**
745
+ * Toggle construction mode for entity
746
+ */
747
+ toggleConstruction(entityId) {
748
+ const entity = sketchState.entities.find(e => e.id === entityId);
749
+ if (entity) {
750
+ entity.isConstruction = !entity.isConstruction;
751
+ return { success: true, isConstruction: entity.isConstruction };
752
+ }
753
+ return { success: false, message: 'Entity not found' };
754
+ },
755
+
756
+ /**
757
+ * Get UI panel for sketch tools
758
+ */
759
+ getUI() {
760
+ const tools = Object.values(SKETCH_TOOLS);
761
+ const constraints = Object.values(CONSTRAINT_TYPES);
762
+
763
+ const toolButtons = tools
764
+ .map(
765
+ t =>
766
+ `<button data-sketch-tool="${t}" style="padding:4px 8px;margin:2px;background:#0284C7;color:white;border:none;border-radius:2px;cursor:pointer;">${t}</button>`
767
+ )
768
+ .join('');
769
+
770
+ const constraintButtons = constraints
771
+ .map(
772
+ c =>
773
+ `<button data-sketch-constraint="${c}" style="padding:4px 8px;margin:2px;background:#10b981;color:white;border:none;border-radius:2px;cursor:pointer;">${c}</button>`
774
+ )
775
+ .join('');
776
+
777
+ return `
778
+ <div id="sketch-panel" style="padding:12px;background:#252526;border-radius:4px;color:#e0e0e0;font-size:12px;">
779
+ <h3>Sketch Tools</h3>
780
+ <div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px;">
781
+ ${toolButtons}
782
+ </div>
783
+
784
+ <h3>Constraints</h3>
785
+ <div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px;">
786
+ ${constraintButtons}
787
+ </div>
788
+
789
+ <div style="margin-top:12px;">
790
+ <label>
791
+ <input type="checkbox" id="sketch-snap-toggle" checked>
792
+ Grid Snap (${sketchState.gridSize}mm)
793
+ </label>
794
+ </div>
795
+
796
+ <label>
797
+ <input type="checkbox" id="sketch-construction-toggle">
798
+ Construction Mode
799
+ </label>
800
+
801
+ <button id="sketch-end-btn" style="width:100%;padding:8px;margin-top:12px;background:#ef4444;color:white;border:none;border-radius:2px;cursor:pointer;">
802
+ End Sketch
803
+ </button>
804
+ </div>
805
+ `;
806
+ },
807
+
808
+ /**
809
+ * Execute sketch command via agent API
810
+ */
811
+ async execute(command, params = {}) {
812
+ switch (command) {
813
+ case 'startSketch':
814
+ return this.startSketch(params.plane ?? 'XY', params.scene, params.renderer);
815
+
816
+ case 'endSketch':
817
+ return this.endSketch();
818
+
819
+ case 'setTool':
820
+ return this.setTool(params.tool);
821
+
822
+ case 'addPoint':
823
+ return this.addPoint(params.x, params.y, params.snap !== false);
824
+
825
+ case 'addConstraint':
826
+ return this.addConstraint(
827
+ params.type,
828
+ params.entityId1,
829
+ params.entityId2,
830
+ params.value
831
+ );
832
+
833
+ case 'addDimension':
834
+ return this.addDimension(params.entityId, params.dimensionType, params.value);
835
+
836
+ case 'mirror':
837
+ return this.mirror(params.entityIds, params.mirrorLine);
838
+
839
+ case 'offset':
840
+ return this.offset(params.entityIds, params.distance);
841
+
842
+ case 'trim':
843
+ return this.trim(params.entityId1, params.entityId2);
844
+
845
+ case 'extend':
846
+ return this.extend(params.entityId, params.targetEntityId);
847
+
848
+ case 'fillet2D':
849
+ return this.fillet2D(params.entityId1, params.entityId2, params.radius);
850
+
851
+ case 'chamfer2D':
852
+ return this.chamfer2D(
853
+ params.entityId1,
854
+ params.entityId2,
855
+ params.distance1,
856
+ params.distance2
857
+ );
858
+
859
+ case 'pattern':
860
+ return this.pattern(
861
+ params.entityIds,
862
+ params.type,
863
+ params.count,
864
+ params.distance,
865
+ params.angle
866
+ );
867
+
868
+ case 'toggleConstruction':
869
+ return this.toggleConstruction(params.entityId);
870
+
871
+ default:
872
+ return { success: false, message: `Unknown command: ${command}` };
873
+ }
874
+ },
875
+
876
+ // ========================================================================
877
+ // PRIVATE HELPERS
878
+ // ========================================================================
879
+
880
+ _snapPoint(x, y) {
881
+ if (!sketchState.snapEnabled) return { x, y };
882
+
883
+ const snapped = {
884
+ x: Math.round(x / sketchState.gridSize) * sketchState.gridSize,
885
+ y: Math.round(y / sketchState.gridSize) * sketchState.gridSize,
886
+ };
887
+
888
+ return snapped;
889
+ },
890
+
891
+ _createLine(points) {
892
+ const entity = new SketchEntity(
893
+ `entity_${nextEntityId++}`,
894
+ 'line',
895
+ points,
896
+ {},
897
+ sketchState.constructionMode
898
+ );
899
+ sketchState.entities.push(entity);
900
+ },
901
+
902
+ _createRectangle(points) {
903
+ const [p1, p2] = points;
904
+ const entity = new SketchEntity(
905
+ `entity_${nextEntityId++}`,
906
+ 'rectangle',
907
+ [p1],
908
+ {
909
+ width: Math.abs(p2.x - p1.x),
910
+ height: Math.abs(p2.y - p1.y),
911
+ },
912
+ sketchState.constructionMode
913
+ );
914
+ sketchState.entities.push(entity);
915
+ },
916
+
917
+ _createCircle(points) {
918
+ const [center, edgePoint] = points;
919
+ const radius = Math.sqrt(
920
+ Math.pow(edgePoint.x - center.x, 2) + Math.pow(edgePoint.y - center.y, 2)
921
+ );
922
+ const entity = new SketchEntity(
923
+ `entity_${nextEntityId++}`,
924
+ 'circle',
925
+ [center],
926
+ { radius },
927
+ sketchState.constructionMode
928
+ );
929
+ sketchState.entities.push(entity);
930
+ },
931
+
932
+ _createArc(points) {
933
+ const [center, edgePoint, endPoint] = points;
934
+ const radius = Math.sqrt(
935
+ Math.pow(edgePoint.x - center.x, 2) + Math.pow(edgePoint.y - center.y, 2)
936
+ );
937
+ const startAngle = Math.atan2(edgePoint.y - center.y, edgePoint.x - center.x);
938
+ const endAngle = Math.atan2(endPoint.y - center.y, endPoint.x - center.x);
939
+
940
+ const entity = new SketchEntity(
941
+ `entity_${nextEntityId++}`,
942
+ 'arc',
943
+ [center],
944
+ { radius, startAngle, endAngle },
945
+ sketchState.constructionMode
946
+ );
947
+ sketchState.entities.push(entity);
948
+ },
949
+
950
+ _createEllipse(points) {
951
+ const [center, radiusXPoint, radiusYPoint] = points;
952
+ const radiusX = Math.sqrt(
953
+ Math.pow(radiusXPoint.x - center.x, 2) + Math.pow(radiusXPoint.y - center.y, 2)
954
+ );
955
+ const radiusY = Math.sqrt(
956
+ Math.pow(radiusYPoint.x - center.x, 2) + Math.pow(radiusYPoint.y - center.y, 2)
957
+ );
958
+
959
+ const entity = new SketchEntity(
960
+ `entity_${nextEntityId++}`,
961
+ 'ellipse',
962
+ [center],
963
+ { radiusX, radiusY },
964
+ sketchState.constructionMode
965
+ );
966
+ sketchState.entities.push(entity);
967
+ },
968
+
969
+ _createPolygon(points) {
970
+ const [center, cornerPoint] = points;
971
+ const radius = Math.sqrt(
972
+ Math.pow(cornerPoint.x - center.x, 2) + Math.pow(cornerPoint.y - center.y, 2)
973
+ );
974
+
975
+ const entity = new SketchEntity(
976
+ `entity_${nextEntityId++}`,
977
+ 'polygon',
978
+ [center],
979
+ { radius, sides: 6 },
980
+ sketchState.constructionMode
981
+ );
982
+ sketchState.entities.push(entity);
983
+ },
984
+
985
+ _createSlot(points) {
986
+ const [pt1, pt2] = points;
987
+ const entity = new SketchEntity(
988
+ `entity_${nextEntityId++}`,
989
+ 'slot',
990
+ [pt1, pt2],
991
+ { width: 10, cornerRadius: 5 },
992
+ sketchState.constructionMode
993
+ );
994
+ sketchState.entities.push(entity);
995
+ },
996
+
997
+ _createSpline(points) {
998
+ const entity = new SketchEntity(
999
+ `entity_${nextEntityId++}`,
1000
+ 'spline',
1001
+ points,
1002
+ {},
1003
+ sketchState.constructionMode
1004
+ );
1005
+ sketchState.entities.push(entity);
1006
+ },
1007
+
1008
+ _reflectPointAcrossLine(point, line) {
1009
+ const { p1, p2 } = line;
1010
+ const dx = p2.x - p1.x;
1011
+ const dy = p2.y - p1.y;
1012
+ const len = Math.sqrt(dx * dx + dy * dy);
1013
+ const ux = dx / len;
1014
+ const uy = dy / len;
1015
+
1016
+ const px = point.x - p1.x;
1017
+ const py = point.y - p1.y;
1018
+ const proj = px * ux + py * uy;
1019
+
1020
+ const perpX = px - proj * ux;
1021
+ const perpY = py - proj * uy;
1022
+
1023
+ return {
1024
+ x: point.x - 2 * perpX,
1025
+ y: point.y - 2 * perpY,
1026
+ };
1027
+ },
1028
+
1029
+ _lineIntersection(p1, p2, p3, p4) {
1030
+ const x1 = p1.x, y1 = p1.y;
1031
+ const x2 = p2.x, y2 = p2.y;
1032
+ const x3 = p3.x, y3 = p3.y;
1033
+ const x4 = p4.x, y4 = p4.y;
1034
+
1035
+ const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
1036
+ if (Math.abs(denom) < 0.0001) return null;
1037
+
1038
+ const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
1039
+ return {
1040
+ x: x1 + t * (x2 - x1),
1041
+ y: y1 + t * (y2 - y1),
1042
+ };
1043
+ },
1044
+ };