cyclecad 0.2.2 → 0.3.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 (85) hide show
  1. package/API-BUILD-MANIFEST.txt +339 -0
  2. package/API-SERVER.md +535 -0
  3. package/Architecture-Deck.pptx +0 -0
  4. package/CLAUDE.md +172 -11
  5. package/CLI-BUILD-SUMMARY.md +504 -0
  6. package/CLI-INDEX.md +356 -0
  7. package/CLI-README.md +466 -0
  8. package/COLLABORATION-INTEGRATION-GUIDE.md +325 -0
  9. package/CONNECTED_FABS_GUIDE.md +612 -0
  10. package/CONNECTED_FABS_README.md +310 -0
  11. package/DELIVERABLES.md +343 -0
  12. package/DFM-ANALYZER-INTEGRATION.md +368 -0
  13. package/DFM-QUICK-START.js +253 -0
  14. package/Dockerfile +69 -0
  15. package/IMPLEMENTATION.md +327 -0
  16. package/LICENSE +31 -0
  17. package/MARKETPLACE_QUICK_REFERENCE.txt +294 -0
  18. package/MCP-INDEX.md +264 -0
  19. package/QUICKSTART-API.md +388 -0
  20. package/QUICKSTART-CLI.md +211 -0
  21. package/QUICKSTART-MCP.md +196 -0
  22. package/README-MCP.md +208 -0
  23. package/TEST-TOKEN-ENGINE.md +319 -0
  24. package/TOKEN-ENGINE-SUMMARY.md +266 -0
  25. package/TOKENS-README.md +263 -0
  26. package/TOOLS-REFERENCE.md +254 -0
  27. package/app/index.html +373 -3
  28. package/app/js/TOKEN-INTEGRATION.md +391 -0
  29. package/app/js/agent-api.js +3 -3
  30. package/app/js/ai-copilot.js +1435 -0
  31. package/app/js/cad-vr.js +917 -0
  32. package/app/js/cam-operations.js +638 -0
  33. package/app/js/cam-pipeline.js +840 -0
  34. package/app/js/collaboration-ui.js +995 -0
  35. package/app/js/collaboration.js +1116 -0
  36. package/app/js/connected-fabs-example.js +404 -0
  37. package/app/js/connected-fabs.js +1449 -0
  38. package/app/js/dfm-analyzer.js +1760 -0
  39. package/app/js/gcode-generator.js +485 -0
  40. package/app/js/gdt-training.js +1144 -0
  41. package/app/js/machine-profiles.js +534 -0
  42. package/app/js/marketplace-v2.js +766 -0
  43. package/app/js/marketplace.js +1994 -0
  44. package/app/js/material-library.js +2115 -0
  45. package/app/js/misumi-catalog.js +904 -0
  46. package/app/js/section-view.js +666 -0
  47. package/app/js/sketch-enhance.js +779 -0
  48. package/app/js/stock-manager.js +482 -0
  49. package/app/js/text-to-cad.js +806 -0
  50. package/app/js/token-dashboard.js +563 -0
  51. package/app/js/token-engine.js +743 -0
  52. package/app/js/tool-library.js +593 -0
  53. package/app/test-agent.html +1801 -0
  54. package/app/tutorials/advanced.html +1924 -0
  55. package/app/tutorials/basic.html +1160 -0
  56. package/app/tutorials/intermediate.html +1456 -0
  57. package/bin/cyclecad-cli.js +662 -0
  58. package/bin/cyclecad-mcp +2 -0
  59. package/bin/server.js +242 -0
  60. package/cycleCAD-Architecture.pptx +0 -0
  61. package/cycleCAD-Investor-Deck.pptx +0 -0
  62. package/demo-mcp.sh +60 -0
  63. package/docs/API-SERVER-SUMMARY.md +375 -0
  64. package/docs/API-SERVER.md +667 -0
  65. package/docs/CAM-EXAMPLES.md +344 -0
  66. package/docs/CAM-INTEGRATION.md +612 -0
  67. package/docs/CAM-QUICK-REFERENCE.md +199 -0
  68. package/docs/CLI-INTEGRATION.md +510 -0
  69. package/docs/CLI.md +872 -0
  70. package/docs/MARKETPLACE-API-SCHEMA.json +564 -0
  71. package/docs/MARKETPLACE-INTEGRATION.md +467 -0
  72. package/docs/MARKETPLACE-SETUP.html +439 -0
  73. package/docs/MCP-SERVER.md +403 -0
  74. package/examples/api-client-example.js +488 -0
  75. package/examples/api-client-example.py +359 -0
  76. package/examples/batch-manufacturing.txt +28 -0
  77. package/examples/batch-simple.txt +26 -0
  78. package/linkedin-post-combined.md +31 -0
  79. package/model-marketplace.html +1273 -0
  80. package/package.json +14 -3
  81. package/server/api-server.js +1120 -0
  82. package/server/mcp-server.js +1161 -0
  83. package/test-api-server.js +432 -0
  84. package/test-mcp.js +198 -0
  85. package/~$cycleCAD-Investor-Deck.pptx +0 -0
@@ -0,0 +1,779 @@
1
+ /**
2
+ * sketch-enhance.js — Enhanced sketch tools for cycleCAD
3
+ *
4
+ * New sketch entities: Polygon, Spline, Text, Ellipse, Slot
5
+ * Modification tools: Trim, Extend, Split, Offset, Mirror, Fillet2D, Chamfer2D
6
+ * Enhanced snap system with visual indicators
7
+ * Region detection for closed loops
8
+ *
9
+ * Usage: window.cycleCAD.sketchEnhance.polygon({...})
10
+ * Pattern: IIFE, no imports
11
+ */
12
+
13
+ (function() {
14
+ 'use strict';
15
+
16
+ const sketchEnhance = {
17
+ // Snap modes (bitmask)
18
+ SNAP_ENDPOINT: 1,
19
+ SNAP_MIDPOINT: 2,
20
+ SNAP_CENTER: 4,
21
+ SNAP_PERPENDICULAR: 8,
22
+ SNAP_TANGENT: 16,
23
+ SNAP_INTERSECTION: 32,
24
+ SNAP_GRID: 64,
25
+ SNAP_NEAREST: 128,
26
+
27
+ // Enabled snap modes (default: all)
28
+ enabledSnaps: 0xFF,
29
+ snapRadius: 8, // pixels
30
+ showSnapIndicators: true,
31
+
32
+ /**
33
+ * Create a regular polygon
34
+ * @param {Object} options - {cx, cy, sides, radius, name}
35
+ * @returns {Object} polygon entity
36
+ */
37
+ polygon(options = {}) {
38
+ const {
39
+ cx = 0, cy = 0,
40
+ sides = 6,
41
+ radius = 50,
42
+ name = `Polygon_${sides}`
43
+ } = options;
44
+
45
+ // Clamp sides to 3-12
46
+ const n = Math.max(3, Math.min(12, sides));
47
+ const points = [];
48
+
49
+ // Generate polygon points
50
+ for (let i = 0; i < n; i++) {
51
+ const angle = (i / n) * Math.PI * 2;
52
+ points.push({
53
+ x: cx + radius * Math.cos(angle),
54
+ y: cy + radius * Math.sin(angle)
55
+ });
56
+ }
57
+
58
+ return {
59
+ type: 'polygon',
60
+ name,
61
+ points,
62
+ cx, cy, sides: n, radius,
63
+ edges: this._pointsToEdges(points, true), // closed
64
+ closed: true,
65
+ color: '#00FF00'
66
+ };
67
+ },
68
+
69
+ /**
70
+ * Create a smooth spline through control points
71
+ * @param {Object} options - {points, tension, closed, name}
72
+ * @returns {Object} spline entity
73
+ */
74
+ spline(options = {}) {
75
+ const {
76
+ points = [],
77
+ tension = 0.5, // Catmull-Rom parameter
78
+ closed = false,
79
+ name = 'Spline'
80
+ } = options;
81
+
82
+ if (points.length < 2) return null;
83
+
84
+ // Generate spline vertices using Catmull-Rom
85
+ const splinePoints = this._catmullRom(points, tension, closed);
86
+
87
+ return {
88
+ type: 'spline',
89
+ name,
90
+ controlPoints: points,
91
+ points: splinePoints,
92
+ tension,
93
+ closed,
94
+ edges: this._pointsToEdges(splinePoints, closed),
95
+ color: '#0080FF',
96
+ handles: points.map((p, i) => ({
97
+ id: i,
98
+ x: p.x, y: p.y,
99
+ point: p
100
+ }))
101
+ };
102
+ },
103
+
104
+ /**
105
+ * Create text outline in 2D sketch
106
+ * @param {Object} options - {text, x, y, fontSize, fontFamily, name}
107
+ * @returns {Object} text entity with outline edges
108
+ */
109
+ text(options = {}) {
110
+ const {
111
+ text = 'Text',
112
+ x = 0, y = 0,
113
+ fontSize = 24,
114
+ fontFamily = 'Arial',
115
+ name = text
116
+ } = options;
117
+
118
+ // Simple text outline generator (approximation)
119
+ const canvas = document.createElement('canvas');
120
+ canvas.width = 256;
121
+ canvas.height = 64;
122
+ const ctx = canvas.getContext('2d');
123
+ ctx.font = `${fontSize}px ${fontFamily}`;
124
+ ctx.fillStyle = '#000';
125
+ ctx.fillText(text, 10, 40);
126
+
127
+ // Trace outline from canvas pixels (simplified)
128
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
129
+ const data = imageData.data;
130
+ const outline = this._traceTextOutline(data, canvas.width, canvas.height);
131
+
132
+ // Scale and offset outline
133
+ const scale = fontSize / 24;
134
+ const points = outline.map(p => ({
135
+ x: x + p.x * scale,
136
+ y: y + p.y * scale
137
+ }));
138
+
139
+ return {
140
+ type: 'text',
141
+ name,
142
+ text,
143
+ x, y,
144
+ fontSize,
145
+ fontFamily,
146
+ points,
147
+ edges: this._pointsToEdges(points, true),
148
+ color: '#FFAA00'
149
+ };
150
+ },
151
+
152
+ /**
153
+ * Create an ellipse
154
+ * @param {Object} options - {cx, cy, rx, ry, name}
155
+ * @returns {Object} ellipse entity
156
+ */
157
+ ellipse(options = {}) {
158
+ const {
159
+ cx = 0, cy = 0,
160
+ rx = 60, ry = 40,
161
+ name = 'Ellipse'
162
+ } = options;
163
+
164
+ // Generate ellipse points
165
+ const points = [];
166
+ const segments = 64;
167
+ for (let i = 0; i < segments; i++) {
168
+ const angle = (i / segments) * Math.PI * 2;
169
+ points.push({
170
+ x: cx + rx * Math.cos(angle),
171
+ y: cy + ry * Math.sin(angle)
172
+ });
173
+ }
174
+
175
+ return {
176
+ type: 'ellipse',
177
+ name,
178
+ cx, cy, rx, ry,
179
+ points,
180
+ edges: this._pointsToEdges(points, true),
181
+ closed: true,
182
+ color: '#FF00FF'
183
+ };
184
+ },
185
+
186
+ /**
187
+ * Create a slot (stadium shape) — rounded rectangle
188
+ * @param {Object} options - {cx, cy, length, width, name}
189
+ * @returns {Object} slot entity
190
+ */
191
+ slot(options = {}) {
192
+ const {
193
+ cx = 0, cy = 0,
194
+ length = 100, width = 40,
195
+ name = 'Slot'
196
+ } = options;
197
+
198
+ const hw = width / 2; // half width
199
+ const hl = length / 2; // half length
200
+ const points = [];
201
+
202
+ // Left semicircle
203
+ for (let i = 0; i <= 32; i++) {
204
+ const angle = (i / 32) * Math.PI;
205
+ points.push({
206
+ x: cx - hl + hw * Math.cos(angle + Math.PI),
207
+ y: cy + hw * Math.sin(angle + Math.PI)
208
+ });
209
+ }
210
+
211
+ // Right semicircle
212
+ for (let i = 0; i <= 32; i++) {
213
+ const angle = (i / 32) * Math.PI;
214
+ points.push({
215
+ x: cx + hl + hw * Math.cos(angle),
216
+ y: cy + hw * Math.sin(angle)
217
+ });
218
+ }
219
+
220
+ return {
221
+ type: 'slot',
222
+ name,
223
+ cx, cy, length, width,
224
+ points,
225
+ edges: this._pointsToEdges(points, true),
226
+ closed: true,
227
+ color: '#00FFAA'
228
+ };
229
+ },
230
+
231
+ /**
232
+ * Trim entity at intersection point
233
+ * @param {Object} entity1 - entity to trim
234
+ * @param {Object} entity2 - reference entity for intersection
235
+ * @returns {Object} trimmed entity (first segment before intersection)
236
+ */
237
+ trim(entity1, entity2) {
238
+ if (!entity1 || !entity1.points) return entity1;
239
+
240
+ // Find intersection
241
+ const intersection = this._findIntersection(entity1, entity2);
242
+ if (!intersection) return entity1; // No trim if no intersection
243
+
244
+ // Trim at intersection point
245
+ const trimmedPoints = [];
246
+ for (const p of entity1.points) {
247
+ trimmedPoints.push(p);
248
+ if (this._distToPoint(p, intersection) < 0.5) break;
249
+ }
250
+
251
+ entity1.points = trimmedPoints;
252
+ entity1.edges = this._pointsToEdges(trimmedPoints, entity1.closed);
253
+ return entity1;
254
+ },
255
+
256
+ /**
257
+ * Extend entity to reach target
258
+ * @param {Object} entity - entity to extend
259
+ * @param {Object} target - target entity to reach
260
+ * @returns {Object} extended entity
261
+ */
262
+ extend(entity, target) {
263
+ if (!entity || !entity.points || entity.points.length < 2) return entity;
264
+
265
+ // Find nearest point on target
266
+ const lastPt = entity.points[entity.points.length - 1];
267
+ const nearestPt = this._nearestPointOnEntity(lastPt, target);
268
+
269
+ if (nearestPt) {
270
+ entity.points.push(nearestPt);
271
+ entity.edges = this._pointsToEdges(entity.points, entity.closed);
272
+ }
273
+
274
+ return entity;
275
+ },
276
+
277
+ /**
278
+ * Split entity at a point
279
+ * @param {Object} entity - entity to split
280
+ * @param {Object} point - split point
281
+ * @returns {Array} [segment1, segment2]
282
+ */
283
+ split(entity, point) {
284
+ if (!entity || !entity.points) return [entity];
285
+
286
+ // Find closest point index
287
+ let minDist = Infinity;
288
+ let splitIndex = 0;
289
+ for (let i = 0; i < entity.points.length; i++) {
290
+ const d = this._distToPoint(entity.points[i], point);
291
+ if (d < minDist) {
292
+ minDist = d;
293
+ splitIndex = i;
294
+ }
295
+ }
296
+
297
+ const seg1 = {
298
+ ...entity,
299
+ points: entity.points.slice(0, splitIndex + 1),
300
+ closed: false
301
+ };
302
+ seg1.edges = this._pointsToEdges(seg1.points, seg1.closed);
303
+
304
+ const seg2 = {
305
+ ...entity,
306
+ name: entity.name + '_2',
307
+ points: entity.points.slice(splitIndex),
308
+ closed: false
309
+ };
310
+ seg2.edges = this._pointsToEdges(seg2.points, seg2.closed);
311
+
312
+ return [seg1, seg2];
313
+ },
314
+
315
+ /**
316
+ * Create parallel offset of entity
317
+ * @param {Object} entity - entity to offset
318
+ * @param {number} distance - offset distance
319
+ * @param {boolean} inside - if true, offset inward; else outward
320
+ * @returns {Object} offset entity
321
+ */
322
+ offset(entity, distance, inside = false) {
323
+ if (!entity || !entity.points) return entity;
324
+
325
+ const offsetPoints = [];
326
+ const pts = entity.points;
327
+ const n = pts.length;
328
+ const dir = inside ? -1 : 1;
329
+
330
+ for (let i = 0; i < n; i++) {
331
+ const prev = pts[(i - 1 + n) % n];
332
+ const curr = pts[i];
333
+ const next = pts[(i + 1) % n];
334
+
335
+ // Calculate perpendicular offset
336
+ const v1 = { x: curr.x - prev.x, y: curr.y - prev.y };
337
+ const v2 = { x: next.x - curr.x, y: next.y - curr.y };
338
+
339
+ const perp1 = { x: -v1.y, y: v1.x };
340
+ const perp2 = { x: -v2.y, y: v2.x };
341
+
342
+ const len1 = Math.hypot(perp1.x, perp1.y);
343
+ const len2 = Math.hypot(perp2.x, perp2.y);
344
+
345
+ if (len1 > 0.01) {
346
+ perp1.x /= len1; perp1.y /= len1;
347
+ }
348
+ if (len2 > 0.01) {
349
+ perp2.x /= len2; perp2.y /= len2;
350
+ }
351
+
352
+ // Average perpendicular at corner
353
+ const avgPerp = {
354
+ x: (perp1.x + perp2.x) * 0.5,
355
+ y: (perp1.y + perp2.y) * 0.5
356
+ };
357
+ const avgLen = Math.hypot(avgPerp.x, avgPerp.y);
358
+ if (avgLen > 0.01) {
359
+ avgPerp.x /= avgLen; avgPerp.y /= avgLen;
360
+ }
361
+
362
+ offsetPoints.push({
363
+ x: curr.x + dir * distance * avgPerp.x,
364
+ y: curr.y + dir * distance * avgPerp.y
365
+ });
366
+ }
367
+
368
+ return {
369
+ ...entity,
370
+ name: entity.name + '_offset',
371
+ points: offsetPoints,
372
+ edges: this._pointsToEdges(offsetPoints, entity.closed),
373
+ color: '#CCCCFF'
374
+ };
375
+ },
376
+
377
+ /**
378
+ * Mirror entities across axis/line
379
+ * @param {Object} entity - entity to mirror
380
+ * @param {string} axis - 'x', 'y', or line {x1,y1,x2,y2}
381
+ * @returns {Object} mirrored entity
382
+ */
383
+ mirror(entity, axis = 'x') {
384
+ if (!entity || !entity.points) return entity;
385
+
386
+ const mirroredPoints = entity.points.map(p => {
387
+ if (typeof axis === 'string') {
388
+ // Mirror across x or y axis
389
+ if (axis === 'x') return { x: -p.x, y: p.y };
390
+ if (axis === 'y') return { x: p.x, y: -p.y };
391
+ } else if (typeof axis === 'object') {
392
+ // Mirror across arbitrary line
393
+ return this._mirrorAcrossLine(p, axis);
394
+ }
395
+ return p;
396
+ });
397
+
398
+ return {
399
+ ...entity,
400
+ name: entity.name + '_mirrored',
401
+ points: mirroredPoints,
402
+ edges: this._pointsToEdges(mirroredPoints, entity.closed)
403
+ };
404
+ },
405
+
406
+ /**
407
+ * Round corner between two intersecting lines (2D fillet)
408
+ * @param {Object} line1 - first line
409
+ * @param {Object} line2 - second line
410
+ * @param {number} radius - fillet radius
411
+ * @returns {Object} fillet arc
412
+ */
413
+ fillet2D(line1, line2, radius = 5) {
414
+ // Find intersection
415
+ const intersection = this._findIntersection(line1, line2);
416
+ if (!intersection) return null;
417
+
418
+ // Create fillet arc at intersection
419
+ const v1 = { x: line1.points[1].x - line1.points[0].x, y: line1.points[1].y - line1.points[0].y };
420
+ const v2 = { x: line2.points[1].x - line2.points[0].x, y: line2.points[1].y - line2.points[0].y };
421
+
422
+ const len1 = Math.hypot(v1.x, v1.y);
423
+ const len2 = Math.hypot(v2.x, v2.y);
424
+ v1.x /= len1; v1.y /= len1;
425
+ v2.x /= len2; v2.y /= len2;
426
+
427
+ // Generate arc
428
+ const startAngle = Math.atan2(v1.y, v1.x);
429
+ const endAngle = Math.atan2(v2.y, v2.x);
430
+ const arcPoints = [];
431
+ const steps = 16;
432
+
433
+ for (let i = 0; i <= steps; i++) {
434
+ const angle = startAngle + (endAngle - startAngle) * (i / steps);
435
+ arcPoints.push({
436
+ x: intersection.x + radius * Math.cos(angle),
437
+ y: intersection.y + radius * Math.sin(angle)
438
+ });
439
+ }
440
+
441
+ return {
442
+ type: 'arc',
443
+ name: 'Fillet_2D',
444
+ points: arcPoints,
445
+ edges: this._pointsToEdges(arcPoints, false),
446
+ color: '#FF6666',
447
+ radius
448
+ };
449
+ },
450
+
451
+ /**
452
+ * Bevel corner between two intersecting lines (2D chamfer)
453
+ * @param {Object} line1 - first line
454
+ * @param {Object} line2 - second line
455
+ * @param {number} size - chamfer size
456
+ * @returns {Object} chamfer line
457
+ */
458
+ chamfer2D(line1, line2, size = 5) {
459
+ const intersection = this._findIntersection(line1, line2);
460
+ if (!intersection) return null;
461
+
462
+ // Points at distance 'size' along each line from intersection
463
+ const v1 = { x: line1.points[1].x - line1.points[0].x, y: line1.points[1].y - line1.points[0].y };
464
+ const v2 = { x: line2.points[1].x - line2.points[0].x, y: line2.points[1].y - line2.points[0].y };
465
+
466
+ const len1 = Math.hypot(v1.x, v1.y);
467
+ const len2 = Math.hypot(v2.x, v2.y);
468
+ v1.x /= len1; v1.y /= len1;
469
+ v2.x /= len2; v2.y /= len2;
470
+
471
+ const pt1 = { x: intersection.x + size * v1.x, y: intersection.y + size * v1.y };
472
+ const pt2 = { x: intersection.x + size * v2.x, y: intersection.y + size * v2.y };
473
+
474
+ return {
475
+ type: 'line',
476
+ name: 'Chamfer_2D',
477
+ points: [pt1, pt2],
478
+ edges: [{ start: pt1, end: pt2 }],
479
+ color: '#FF99FF'
480
+ };
481
+ },
482
+
483
+ /**
484
+ * Detect closed regions from sketch entities
485
+ * @param {Array} entities - sketch entities
486
+ * @returns {Array} detected regions (each is a closed polygon)
487
+ */
488
+ detectRegions(entities = []) {
489
+ const regions = [];
490
+
491
+ // Build adjacency graph
492
+ const adjacency = new Map();
493
+ for (const e of entities) {
494
+ if (!e.points || e.points.length < 2) continue;
495
+ const start = e.points[0];
496
+ const end = e.points[e.points.length - 1];
497
+
498
+ const key = `${start.x.toFixed(2)},${start.y.toFixed(2)}`;
499
+ if (!adjacency.has(key)) adjacency.set(key, []);
500
+ adjacency.get(key).push({ entity: e, end });
501
+ }
502
+
503
+ // Trace closed loops
504
+ const visited = new Set();
505
+ for (const entity of entities) {
506
+ if (!entity.points || entity.points.length < 2) continue;
507
+
508
+ const startKey = `${entity.points[0].x.toFixed(2)},${entity.points[0].y.toFixed(2)}`;
509
+ if (visited.has(startKey)) continue;
510
+
511
+ const loop = this._traceLoop(entity, adjacency, visited);
512
+ if (loop && loop.length > 2) {
513
+ regions.push({
514
+ type: 'region',
515
+ name: `Region_${regions.length + 1}`,
516
+ boundary: loop,
517
+ closed: true,
518
+ color: 'rgba(100, 150, 255, 0.2)'
519
+ });
520
+ }
521
+ }
522
+
523
+ return regions;
524
+ },
525
+
526
+ /**
527
+ * Toggle snap mode
528
+ * @param {number} snapMode - SNAP_* constant
529
+ * @param {boolean} enabled - enable or disable
530
+ */
531
+ setSnapMode(snapMode, enabled = true) {
532
+ if (enabled) {
533
+ this.enabledSnaps |= snapMode;
534
+ } else {
535
+ this.enabledSnaps &= ~snapMode;
536
+ }
537
+ },
538
+
539
+ /**
540
+ * Find snap point near cursor
541
+ * @param {Object} cursor - {x, y}
542
+ * @param {Array} entities - sketch entities
543
+ * @returns {Object|null} snap point or null
544
+ */
545
+ findSnapPoint(cursor, entities = []) {
546
+ let bestSnap = null;
547
+ let bestDist = this.snapRadius;
548
+
549
+ for (const entity of entities) {
550
+ if (!entity.points) continue;
551
+
552
+ for (let i = 0; i < entity.points.length; i++) {
553
+ const pt = entity.points[i];
554
+
555
+ // Endpoint snap
556
+ if ((this.enabledSnaps & this.SNAP_ENDPOINT) && i === 0) {
557
+ const d = this._distToPoint(cursor, pt);
558
+ if (d < bestDist) {
559
+ bestDist = d;
560
+ bestSnap = { ...pt, type: 'endpoint', entity, index: i };
561
+ }
562
+ }
563
+
564
+ // Midpoint snap
565
+ if (this.enabledSnaps & this.SNAP_MIDPOINT) {
566
+ if (i < entity.points.length - 1) {
567
+ const next = entity.points[i + 1];
568
+ const mid = { x: (pt.x + next.x) / 2, y: (pt.y + next.y) / 2 };
569
+ const d = this._distToPoint(cursor, mid);
570
+ if (d < bestDist) {
571
+ bestDist = d;
572
+ bestSnap = { ...mid, type: 'midpoint', entity, index: i };
573
+ }
574
+ }
575
+ }
576
+
577
+ // Center snap (for circles/arcs)
578
+ if ((this.enabledSnaps & this.SNAP_CENTER) && entity.cx !== undefined) {
579
+ const d = this._distToPoint(cursor, { x: entity.cx, y: entity.cy });
580
+ if (d < bestDist) {
581
+ bestDist = d;
582
+ bestSnap = { x: entity.cx, y: entity.cy, type: 'center', entity };
583
+ }
584
+ }
585
+ }
586
+ }
587
+
588
+ return bestSnap;
589
+ },
590
+
591
+ /**
592
+ * Draw snap indicator (visual feedback)
593
+ * @param {Object} snapPoint - snap point with type
594
+ * @param {CanvasRenderingContext2D} ctx - canvas context
595
+ */
596
+ drawSnapIndicator(snapPoint, ctx) {
597
+ if (!snapPoint || !this.showSnapIndicators) return;
598
+
599
+ const size = 8;
600
+ const { x, y, type } = snapPoint;
601
+
602
+ ctx.save();
603
+ ctx.fillStyle = '#FF00FF';
604
+ ctx.strokeStyle = '#FFFFFF';
605
+ ctx.lineWidth = 1;
606
+
607
+ switch (type) {
608
+ case 'endpoint':
609
+ ctx.fillRect(x - size / 2, y - size / 2, size, size);
610
+ break;
611
+ case 'midpoint':
612
+ ctx.beginPath();
613
+ ctx.arc(x, y, size / 2, 0, Math.PI * 2);
614
+ ctx.fill();
615
+ break;
616
+ case 'center':
617
+ ctx.beginPath();
618
+ ctx.arc(x, y, size / 2, 0, Math.PI * 2);
619
+ ctx.fill();
620
+ ctx.stroke();
621
+ break;
622
+ case 'intersection':
623
+ ctx.beginPath();
624
+ ctx.arc(x, y, size / 2, 0, Math.PI * 2);
625
+ ctx.fill();
626
+ ctx.stroke();
627
+ break;
628
+ }
629
+
630
+ ctx.restore();
631
+ },
632
+
633
+ // =========== PRIVATE HELPERS ===========
634
+
635
+ _pointsToEdges(points, closed = false) {
636
+ const edges = [];
637
+ for (let i = 0; i < points.length - 1; i++) {
638
+ edges.push({ start: points[i], end: points[i + 1] });
639
+ }
640
+ if (closed && points.length > 0) {
641
+ edges.push({ start: points[points.length - 1], end: points[0] });
642
+ }
643
+ return edges;
644
+ },
645
+
646
+ _catmullRom(points, tension, closed) {
647
+ if (points.length < 2) return points;
648
+
649
+ const result = [];
650
+ const n = closed ? points.length : points.length - 1;
651
+ const segments = 16;
652
+
653
+ for (let i = 0; i < n; i++) {
654
+ const p0 = points[(i - 1 + points.length) % points.length];
655
+ const p1 = points[i];
656
+ const p2 = points[(i + 1) % points.length];
657
+ const p3 = points[(i + 2) % points.length];
658
+
659
+ for (let t = 0; t < segments; t++) {
660
+ const s = t / segments;
661
+ const s2 = s * s;
662
+ const s3 = s2 * s;
663
+
664
+ const c0 = -0.5 * s3 + s2 - 0.5 * s;
665
+ const c1 = 1.5 * s3 - 2.5 * s2 + 1.0;
666
+ const c2 = -1.5 * s3 + 2.0 * s2 + 0.5 * s;
667
+ const c3 = 0.5 * s3 - 0.5 * s2;
668
+
669
+ result.push({
670
+ x: c0 * p0.x + c1 * p1.x + c2 * p2.x + c3 * p3.x,
671
+ y: c0 * p0.y + c1 * p1.y + c2 * p2.y + c3 * p3.y
672
+ });
673
+ }
674
+ }
675
+
676
+ return result;
677
+ },
678
+
679
+ _traceTextOutline(imageData, width, height) {
680
+ // Simplified: return a rectangle outline
681
+ return [
682
+ { x: 0, y: 0 }, { x: width, y: 0 },
683
+ { x: width, y: height }, { x: 0, y: height }
684
+ ];
685
+ },
686
+
687
+ _distToPoint(p1, p2) {
688
+ return Math.hypot(p1.x - p2.x, p1.y - p2.y);
689
+ },
690
+
691
+ _findIntersection(entity1, entity2) {
692
+ if (!entity1.edges || !entity2.edges) return null;
693
+
694
+ for (const e1 of entity1.edges) {
695
+ for (const e2 of entity2.edges) {
696
+ const pt = this._lineIntersection(e1.start, e1.end, e2.start, e2.end);
697
+ if (pt) return pt;
698
+ }
699
+ }
700
+ return null;
701
+ },
702
+
703
+ _lineIntersection(p1, p2, p3, p4) {
704
+ const x1 = p1.x, y1 = p1.y, x2 = p2.x, y2 = p2.y;
705
+ const x3 = p3.x, y3 = p3.y, x4 = p4.x, y4 = p4.y;
706
+
707
+ const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
708
+ if (Math.abs(denom) < 0.0001) return null;
709
+
710
+ const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
711
+ if (t < 0 || t > 1) return null;
712
+
713
+ return {
714
+ x: x1 + t * (x2 - x1),
715
+ y: y1 + t * (y2 - y1)
716
+ };
717
+ },
718
+
719
+ _nearestPointOnEntity(point, entity) {
720
+ let nearest = null;
721
+ let minDist = Infinity;
722
+
723
+ if (entity.points) {
724
+ for (const p of entity.points) {
725
+ const d = this._distToPoint(point, p);
726
+ if (d < minDist) {
727
+ minDist = d;
728
+ nearest = p;
729
+ }
730
+ }
731
+ }
732
+
733
+ return nearest;
734
+ },
735
+
736
+ _mirrorAcrossLine(point, line) {
737
+ const { x1, y1, x2, y2 } = line;
738
+ const dx = x2 - x1, dy = y2 - y1;
739
+ const len = Math.hypot(dx, dy);
740
+ const nx = dx / len, ny = dy / len;
741
+
742
+ const px = point.x - x1, py = point.y - y1;
743
+ const proj = px * nx + py * ny;
744
+ const closestX = x1 + proj * nx;
745
+ const closestY = y1 + proj * ny;
746
+
747
+ return {
748
+ x: 2 * closestX - point.x,
749
+ y: 2 * closestY - point.y
750
+ };
751
+ },
752
+
753
+ _traceLoop(startEntity, adjacency, visited) {
754
+ const loop = [];
755
+ let current = startEntity;
756
+
757
+ while (current) {
758
+ const endKey = `${current.points[current.points.length - 1].x.toFixed(2)},${current.points[current.points.length - 1].y.toFixed(2)}`;
759
+ visited.add(endKey);
760
+ loop.push(...current.points);
761
+
762
+ const nextList = adjacency.get(endKey) || [];
763
+ current = nextList.length > 0 ? nextList[0].entity : null;
764
+
765
+ if (current && visited.has(`${current.points[0].x.toFixed(2)},${current.points[0].y.toFixed(2)}`)) {
766
+ break;
767
+ }
768
+ }
769
+
770
+ return loop;
771
+ }
772
+ };
773
+
774
+ // Register on window
775
+ window.cycleCAD = window.cycleCAD || {};
776
+ window.cycleCAD.sketchEnhance = sketchEnhance;
777
+
778
+ console.log('[sketchEnhance] Loaded: polygon, spline, text, ellipse, slot, trim, extend, split, offset, mirror, fillet2D, chamfer2D, detectRegions');
779
+ })();