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.
- package/DOCKER-SETUP-VERIFICATION.md +399 -0
- package/DOCKER-TESTING.md +463 -0
- package/FUSION360_MODULES.md +478 -0
- package/FUSION_MODULES_README.md +352 -0
- package/INTEGRATION_SNIPPETS.md +608 -0
- package/KILLER-FEATURES-DELIVERY.md +469 -0
- package/MODULES_SUMMARY.txt +337 -0
- package/QUICK_REFERENCE.txt +298 -0
- package/README-DOCKER-TESTING.txt +438 -0
- package/app/index.html +23 -10
- package/app/js/fusion-help.json +1808 -0
- package/app/js/help-module-v3.js +1096 -0
- package/app/js/killer-features-help.json +395 -0
- package/app/js/killer-features.js +1508 -0
- package/app/js/modules/fusion-assembly.js +842 -0
- package/app/js/modules/fusion-cam.js +785 -0
- package/app/js/modules/fusion-data.js +814 -0
- package/app/js/modules/fusion-drawing.js +844 -0
- package/app/js/modules/fusion-inspection.js +756 -0
- package/app/js/modules/fusion-render.js +774 -0
- package/app/js/modules/fusion-simulation.js +986 -0
- package/app/js/modules/fusion-sketch.js +1044 -0
- package/app/js/modules/fusion-solid.js +1095 -0
- package/app/js/modules/fusion-surface.js +949 -0
- package/app/tests/FUSION_TEST_SUITE.md +266 -0
- package/app/tests/README.md +77 -0
- package/app/tests/TESTING-CHECKLIST.md +177 -0
- package/app/tests/TEST_SUITE_SUMMARY.txt +236 -0
- package/app/tests/brep-live-test.html +848 -0
- package/app/tests/docker-integration-test.html +811 -0
- package/app/tests/fusion-all-tests.html +670 -0
- package/app/tests/fusion-assembly-tests.html +461 -0
- package/app/tests/fusion-cam-tests.html +421 -0
- package/app/tests/fusion-simulation-tests.html +421 -0
- package/app/tests/fusion-sketch-tests.html +613 -0
- package/app/tests/fusion-solid-tests.html +529 -0
- package/app/tests/index.html +453 -0
- package/app/tests/killer-features-test.html +509 -0
- package/app/tests/run-tests.html +874 -0
- package/app/tests/step-import-live-test.html +1115 -0
- package/app/tests/test-agent-v3.html +93 -696
- package/architecture-dashboard.html +1970 -0
- package/docs/API-REFERENCE.md +1423 -0
- package/docs/BREP-LIVE-TEST-GUIDE.md +453 -0
- package/docs/DEVELOPER-GUIDE-v3.md +795 -0
- package/docs/DOCKER-QUICK-TEST.md +376 -0
- package/docs/FUSION-FEATURES-GUIDE.md +2513 -0
- package/docs/FUSION-TUTORIAL.md +1203 -0
- package/docs/INFRASTRUCTURE-GUIDE-INDEX.md +327 -0
- package/docs/KEYBOARD-SHORTCUTS.md +402 -0
- package/docs/KILLER-FEATURES-INTEGRATION.md +412 -0
- package/docs/KILLER-FEATURES-SUMMARY.md +424 -0
- package/docs/KILLER-FEATURES-TUTORIAL.md +784 -0
- package/docs/KILLER-FEATURES.md +562 -0
- package/docs/QUICK-REFERENCE.md +282 -0
- package/docs/README-v3-DOCS.md +274 -0
- package/docs/TUTORIAL-v3.md +1190 -0
- package/docs/architecture-dashboard.html +1970 -0
- package/docs/architecture-v3.html +1038 -0
- package/linkedin-post-v3.md +58 -0
- package/package.json +1 -1
- package/scripts/dev-setup.sh +338 -0
- package/scripts/docker-health-check.sh +159 -0
- package/scripts/integration-test.sh +311 -0
- 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
|
+
};
|