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.
- package/API-BUILD-MANIFEST.txt +339 -0
- package/API-SERVER.md +535 -0
- package/Architecture-Deck.pptx +0 -0
- package/CLAUDE.md +172 -11
- package/CLI-BUILD-SUMMARY.md +504 -0
- package/CLI-INDEX.md +356 -0
- package/CLI-README.md +466 -0
- package/COLLABORATION-INTEGRATION-GUIDE.md +325 -0
- package/CONNECTED_FABS_GUIDE.md +612 -0
- package/CONNECTED_FABS_README.md +310 -0
- package/DELIVERABLES.md +343 -0
- package/DFM-ANALYZER-INTEGRATION.md +368 -0
- package/DFM-QUICK-START.js +253 -0
- package/Dockerfile +69 -0
- package/IMPLEMENTATION.md +327 -0
- package/LICENSE +31 -0
- package/MARKETPLACE_QUICK_REFERENCE.txt +294 -0
- package/MCP-INDEX.md +264 -0
- package/QUICKSTART-API.md +388 -0
- package/QUICKSTART-CLI.md +211 -0
- package/QUICKSTART-MCP.md +196 -0
- package/README-MCP.md +208 -0
- package/TEST-TOKEN-ENGINE.md +319 -0
- package/TOKEN-ENGINE-SUMMARY.md +266 -0
- package/TOKENS-README.md +263 -0
- package/TOOLS-REFERENCE.md +254 -0
- package/app/index.html +373 -3
- package/app/js/TOKEN-INTEGRATION.md +391 -0
- package/app/js/agent-api.js +3 -3
- package/app/js/ai-copilot.js +1435 -0
- package/app/js/cad-vr.js +917 -0
- package/app/js/cam-operations.js +638 -0
- package/app/js/cam-pipeline.js +840 -0
- package/app/js/collaboration-ui.js +995 -0
- package/app/js/collaboration.js +1116 -0
- package/app/js/connected-fabs-example.js +404 -0
- package/app/js/connected-fabs.js +1449 -0
- package/app/js/dfm-analyzer.js +1760 -0
- package/app/js/gcode-generator.js +485 -0
- package/app/js/gdt-training.js +1144 -0
- package/app/js/machine-profiles.js +534 -0
- package/app/js/marketplace-v2.js +766 -0
- package/app/js/marketplace.js +1994 -0
- package/app/js/material-library.js +2115 -0
- package/app/js/misumi-catalog.js +904 -0
- package/app/js/section-view.js +666 -0
- package/app/js/sketch-enhance.js +779 -0
- package/app/js/stock-manager.js +482 -0
- package/app/js/text-to-cad.js +806 -0
- package/app/js/token-dashboard.js +563 -0
- package/app/js/token-engine.js +743 -0
- package/app/js/tool-library.js +593 -0
- package/app/test-agent.html +1801 -0
- package/app/tutorials/advanced.html +1924 -0
- package/app/tutorials/basic.html +1160 -0
- package/app/tutorials/intermediate.html +1456 -0
- package/bin/cyclecad-cli.js +662 -0
- package/bin/cyclecad-mcp +2 -0
- package/bin/server.js +242 -0
- package/cycleCAD-Architecture.pptx +0 -0
- package/cycleCAD-Investor-Deck.pptx +0 -0
- package/demo-mcp.sh +60 -0
- package/docs/API-SERVER-SUMMARY.md +375 -0
- package/docs/API-SERVER.md +667 -0
- package/docs/CAM-EXAMPLES.md +344 -0
- package/docs/CAM-INTEGRATION.md +612 -0
- package/docs/CAM-QUICK-REFERENCE.md +199 -0
- package/docs/CLI-INTEGRATION.md +510 -0
- package/docs/CLI.md +872 -0
- package/docs/MARKETPLACE-API-SCHEMA.json +564 -0
- package/docs/MARKETPLACE-INTEGRATION.md +467 -0
- package/docs/MARKETPLACE-SETUP.html +439 -0
- package/docs/MCP-SERVER.md +403 -0
- package/examples/api-client-example.js +488 -0
- package/examples/api-client-example.py +359 -0
- package/examples/batch-manufacturing.txt +28 -0
- package/examples/batch-simple.txt +26 -0
- package/linkedin-post-combined.md +31 -0
- package/model-marketplace.html +1273 -0
- package/package.json +14 -3
- package/server/api-server.js +1120 -0
- package/server/mcp-server.js +1161 -0
- package/test-api-server.js +432 -0
- package/test-mcp.js +198 -0
- 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
|
+
})();
|