cyclecad 0.5.0 → 0.8.6
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/LICENSE +17 -27
- package/app/index.html +984 -431
- package/app/js/agent-api.js +1 -6
- package/app/js/operations.js +117 -55
- package/app/js/sketch.js +44 -14
- package/app/js/tree.js +6 -1
- package/app/js/viewport.js +8 -0
- package/package.json +2 -2
package/app/js/agent-api.js
CHANGED
package/app/js/operations.js
CHANGED
|
@@ -75,74 +75,137 @@ export function createMaterial(preset = 'steel', overrides = {}) {
|
|
|
75
75
|
* @returns {THREE.Shape}
|
|
76
76
|
*/
|
|
77
77
|
function entitiesToShape(entities) {
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
if (!entities || entities.length === 0) {
|
|
79
|
+
throw new Error('No sketch entities to convert');
|
|
80
|
+
}
|
|
80
81
|
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
const
|
|
82
|
+
// Separate entities by type
|
|
83
|
+
const rects = [];
|
|
84
|
+
const circles = [];
|
|
85
|
+
const lines = [];
|
|
86
|
+
const arcs = [];
|
|
87
|
+
const polylines = [];
|
|
84
88
|
|
|
85
89
|
for (const entity of entities) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (i === 0) profile.moveTo(pt.x, pt.y);
|
|
104
|
-
else profile.lineTo(pt.x, pt.y);
|
|
105
|
-
});
|
|
106
|
-
profiles.push(profile);
|
|
90
|
+
switch (entity.type) {
|
|
91
|
+
case 'rect':
|
|
92
|
+
case 'rectangle':
|
|
93
|
+
rects.push(entity);
|
|
94
|
+
break;
|
|
95
|
+
case 'circle':
|
|
96
|
+
circles.push(entity);
|
|
97
|
+
break;
|
|
98
|
+
case 'line':
|
|
99
|
+
lines.push(entity);
|
|
100
|
+
break;
|
|
101
|
+
case 'arc':
|
|
102
|
+
arcs.push(entity);
|
|
103
|
+
break;
|
|
104
|
+
case 'polyline':
|
|
105
|
+
polylines.push(entity);
|
|
106
|
+
break;
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
110
|
+
// Build shapes from each entity type
|
|
111
|
+
const shapes = [];
|
|
112
|
+
|
|
113
|
+
// Rectangles → closed shape
|
|
114
|
+
for (const r of rects) {
|
|
115
|
+
const s = new THREE.Shape();
|
|
116
|
+
// Sketch stores corner in points[0], opposite corner in points[1], dims in dimensions
|
|
117
|
+
const x = r.points?.[0]?.x ?? r.x ?? 0;
|
|
118
|
+
const y = r.points?.[0]?.y ?? r.y ?? 0;
|
|
119
|
+
const w = r.dimensions?.width ?? r.width ?? 10;
|
|
120
|
+
const h = r.dimensions?.height ?? r.height ?? 10;
|
|
121
|
+
s.moveTo(x, y);
|
|
122
|
+
s.lineTo(x + w, y);
|
|
123
|
+
s.lineTo(x + w, y + h);
|
|
124
|
+
s.lineTo(x, y + h);
|
|
125
|
+
s.closePath();
|
|
126
|
+
shapes.push({ shape: s, area: Math.abs(w * h), type: 'rect' });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Circles → closed shape
|
|
130
|
+
for (const c of circles) {
|
|
131
|
+
const s = new THREE.Shape();
|
|
132
|
+
// Sketch stores center in points[0], radius in dimensions.radius
|
|
133
|
+
const cx = c.points?.[0]?.x ?? c.x ?? c.center?.x ?? 0;
|
|
134
|
+
const cy = c.points?.[0]?.y ?? c.y ?? c.center?.y ?? 0;
|
|
135
|
+
const r = c.dimensions?.radius ?? c.radius ?? 10;
|
|
136
|
+
s.absarc(cx, cy, r, 0, Math.PI * 2, false);
|
|
137
|
+
shapes.push({ shape: s, area: Math.PI * r * r, type: 'circle' });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Polylines → closed shape (if closed or has enough points)
|
|
141
|
+
for (const p of polylines) {
|
|
142
|
+
if (p.points && p.points.length >= 3) {
|
|
143
|
+
const s = new THREE.Shape();
|
|
144
|
+
s.moveTo(p.points[0].x, p.points[0].y);
|
|
145
|
+
for (let i = 1; i < p.points.length; i++) {
|
|
146
|
+
s.lineTo(p.points[i].x, p.points[i].y);
|
|
121
147
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
148
|
+
s.closePath();
|
|
149
|
+
// Approximate area using shoelace formula
|
|
150
|
+
let area = 0;
|
|
151
|
+
const pts = p.points;
|
|
152
|
+
for (let i = 0; i < pts.length; i++) {
|
|
153
|
+
const j = (i + 1) % pts.length;
|
|
154
|
+
area += pts[i].x * pts[j].y;
|
|
155
|
+
area -= pts[j].x * pts[i].y;
|
|
126
156
|
}
|
|
157
|
+
shapes.push({ shape: s, area: Math.abs(area / 2), type: 'polyline' });
|
|
127
158
|
}
|
|
128
159
|
}
|
|
129
160
|
|
|
130
|
-
//
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
161
|
+
// Lines → try to build closed path if they form a loop
|
|
162
|
+
if (lines.length >= 3) {
|
|
163
|
+
const s = new THREE.Shape();
|
|
164
|
+
// Start from first line's start point
|
|
165
|
+
const firstLine = lines[0];
|
|
166
|
+
const sx = firstLine.start?.x || firstLine.x1 || 0;
|
|
167
|
+
const sy = firstLine.start?.y || firstLine.y1 || 0;
|
|
168
|
+
s.moveTo(sx, sy);
|
|
169
|
+
|
|
170
|
+
for (const line of lines) {
|
|
171
|
+
const ex = line.end?.x || line.x2 || 0;
|
|
172
|
+
const ey = line.end?.y || line.y2 || 0;
|
|
173
|
+
s.lineTo(ex, ey);
|
|
135
174
|
}
|
|
175
|
+
s.closePath();
|
|
176
|
+
shapes.push({ shape: s, area: 1, type: 'lines' });
|
|
136
177
|
}
|
|
137
178
|
|
|
138
|
-
//
|
|
139
|
-
|
|
179
|
+
// If no shapes could be built, create a default 20mm square
|
|
180
|
+
if (shapes.length === 0) {
|
|
181
|
+
const s = new THREE.Shape();
|
|
182
|
+
s.moveTo(-10, -10);
|
|
183
|
+
s.lineTo(10, -10);
|
|
184
|
+
s.lineTo(10, 10);
|
|
185
|
+
s.lineTo(-10, 10);
|
|
186
|
+
s.closePath();
|
|
187
|
+
shapes.push({ shape: s, area: 400, type: 'default' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Sort by area — largest is the outer profile, smaller ones are holes
|
|
191
|
+
shapes.sort((a, b) => b.area - a.area);
|
|
192
|
+
|
|
193
|
+
// Largest shape is the outer boundary
|
|
194
|
+
const mainShape = shapes[0].shape;
|
|
195
|
+
|
|
196
|
+
// Any smaller shapes become holes in the main shape
|
|
197
|
+
for (let i = 1; i < shapes.length; i++) {
|
|
198
|
+
const holePts = shapes[i].shape.getPoints(32);
|
|
140
199
|
const holePath = new THREE.Path();
|
|
141
|
-
|
|
142
|
-
|
|
200
|
+
holePts.forEach((pt, idx) => {
|
|
201
|
+
if (idx === 0) holePath.moveTo(pt.x, pt.y);
|
|
202
|
+
else holePath.lineTo(pt.x, pt.y);
|
|
203
|
+
});
|
|
204
|
+
holePath.closePath();
|
|
205
|
+
mainShape.holes.push(holePath);
|
|
143
206
|
}
|
|
144
207
|
|
|
145
|
-
return
|
|
208
|
+
return mainShape;
|
|
146
209
|
}
|
|
147
210
|
|
|
148
211
|
/**
|
|
@@ -168,9 +231,8 @@ export function extrudeProfile(entities, height, options = {}) {
|
|
|
168
231
|
// Create shape from entities
|
|
169
232
|
const shape = entitiesToShape(entities);
|
|
170
233
|
|
|
171
|
-
//
|
|
172
|
-
const
|
|
173
|
-
const depth = symmetric ? height : height;
|
|
234
|
+
// Always use positive depth — cut logic is handled by doExtrude in index.html
|
|
235
|
+
const depth = symmetric ? Math.abs(height) : Math.abs(height);
|
|
174
236
|
|
|
175
237
|
// Create extrude geometry
|
|
176
238
|
const geometry = new THREE.ExtrudeGeometry(shape, {
|
|
@@ -182,7 +244,7 @@ export function extrudeProfile(entities, height, options = {}) {
|
|
|
182
244
|
steps: Math.max(1, Math.floor(depth / 10))
|
|
183
245
|
});
|
|
184
246
|
|
|
185
|
-
//
|
|
247
|
+
// Position the extrusion
|
|
186
248
|
if (symmetric) {
|
|
187
249
|
geometry.translate(0, 0, -depth / 2);
|
|
188
250
|
}
|
package/app/js/sketch.js
CHANGED
|
@@ -50,17 +50,20 @@ export function startSketch(plane, camera, controls) {
|
|
|
50
50
|
// Disable orbit controls
|
|
51
51
|
if (controls) controls.enabled = false;
|
|
52
52
|
|
|
53
|
-
// Create canvas overlay
|
|
54
|
-
const container = document.body;
|
|
53
|
+
// Create canvas overlay — constrained to viewport area only
|
|
54
|
+
const container = document.getElementById('viewport-container') || document.body;
|
|
55
55
|
const canvas = document.createElement('canvas');
|
|
56
56
|
canvas.id = 'sketch-canvas';
|
|
57
|
-
|
|
58
|
-
canvas.
|
|
59
|
-
canvas.
|
|
57
|
+
const vpRect = container.getBoundingClientRect();
|
|
58
|
+
canvas.width = vpRect.width || window.innerWidth;
|
|
59
|
+
canvas.height = vpRect.height || window.innerHeight;
|
|
60
|
+
canvas.style.position = 'absolute';
|
|
60
61
|
canvas.style.top = '0';
|
|
61
62
|
canvas.style.left = '0';
|
|
63
|
+
canvas.style.width = '100%';
|
|
64
|
+
canvas.style.height = '100%';
|
|
62
65
|
canvas.style.cursor = 'crosshair';
|
|
63
|
-
canvas.style.zIndex = '
|
|
66
|
+
canvas.style.zIndex = '30';
|
|
64
67
|
canvas.style.backgroundColor = 'rgba(0,0,0,0)';
|
|
65
68
|
|
|
66
69
|
container.appendChild(canvas);
|
|
@@ -179,9 +182,14 @@ export function canvasToWorld(clientX, clientY, camera, plane) {
|
|
|
179
182
|
const canvas = sketchState.canvas;
|
|
180
183
|
if (!canvas) return { x: 0, y: 0, z: 0 };
|
|
181
184
|
|
|
185
|
+
// Account for canvas position offset (left panel, toolbar, etc.)
|
|
186
|
+
const rect = canvas.getBoundingClientRect();
|
|
187
|
+
const localX = clientX - rect.left;
|
|
188
|
+
const localY = clientY - rect.top;
|
|
189
|
+
|
|
182
190
|
// Normalized device coordinates
|
|
183
|
-
const ndcX = (
|
|
184
|
-
const ndcY = -(
|
|
191
|
+
const ndcX = (localX / rect.width) * 2 - 1;
|
|
192
|
+
const ndcY = -(localY / rect.height) * 2 + 1;
|
|
185
193
|
|
|
186
194
|
// Ray from camera through pixel
|
|
187
195
|
const raycaster = sketchState.raycaster;
|
|
@@ -192,7 +200,10 @@ export function canvasToWorld(clientX, clientY, camera, plane) {
|
|
|
192
200
|
const planeObj = new THREE.Plane(planeNormal, 0);
|
|
193
201
|
const intersection = new THREE.Vector3();
|
|
194
202
|
|
|
195
|
-
raycaster.ray.intersectPlane(planeObj, intersection);
|
|
203
|
+
const hit = raycaster.ray.intersectPlane(planeObj, intersection);
|
|
204
|
+
if (!hit) {
|
|
205
|
+
return { x: 0, y: 0, z: 0 }; // fallback if ray is parallel to plane
|
|
206
|
+
}
|
|
196
207
|
|
|
197
208
|
return { x: intersection.x, y: intersection.y, z: intersection.z };
|
|
198
209
|
}
|
|
@@ -602,17 +613,24 @@ function _onSketchClick(e) {
|
|
|
602
613
|
function _onSketchContextMenu(e) {
|
|
603
614
|
e.preventDefault();
|
|
604
615
|
|
|
605
|
-
|
|
616
|
+
const tool = sketchState.currentTool;
|
|
617
|
+
|
|
618
|
+
if (tool === 'line' && sketchState.currentPoints.length > 0) {
|
|
606
619
|
// Stop line chain mode
|
|
607
620
|
if (sketchState.currentPoints.length >= 2) {
|
|
608
621
|
_finalizeEntity();
|
|
609
622
|
}
|
|
610
623
|
sketchState.currentPoints = [];
|
|
611
624
|
sketchState.isDrawing = false;
|
|
612
|
-
} else if (
|
|
625
|
+
} else if (tool === 'polyline' && sketchState.currentPoints.length >= 2) {
|
|
613
626
|
// Close polyline
|
|
614
627
|
_finalizeEntity();
|
|
615
628
|
sketchState.isDrawing = false;
|
|
629
|
+
} else if ((tool === 'rectangle' || tool === 'rect' || tool === 'circle' || tool === 'arc') && sketchState.isDrawing) {
|
|
630
|
+
// Cancel in-progress rect/circle/arc on right-click
|
|
631
|
+
sketchState.currentPoints = [];
|
|
632
|
+
sketchState.isDrawing = false;
|
|
633
|
+
sketchState.inProgressEntity = null;
|
|
616
634
|
}
|
|
617
635
|
|
|
618
636
|
_renderSketchCanvas();
|
|
@@ -640,7 +658,17 @@ function _onSketchKeyDown(e) {
|
|
|
640
658
|
}
|
|
641
659
|
_renderSketchCanvas();
|
|
642
660
|
} else if (e.key === 'Escape') {
|
|
643
|
-
|
|
661
|
+
// Cancel current drawing, don't destroy the sketch
|
|
662
|
+
if (sketchState.currentPoints.length > 0 || sketchState.inProgressEntity) {
|
|
663
|
+
sketchState.currentPoints = [];
|
|
664
|
+
sketchState.inProgressEntity = null;
|
|
665
|
+
sketchState.isDrawing = false;
|
|
666
|
+
_renderSketchCanvas();
|
|
667
|
+
} else if (sketchState.currentTool) {
|
|
668
|
+
sketchState.currentTool = null;
|
|
669
|
+
_renderSketchCanvas();
|
|
670
|
+
}
|
|
671
|
+
// If no tool and no in-progress, do nothing — user clicks Extrude to finish
|
|
644
672
|
} else if (e.key === 'z' && (e.ctrlKey || e.metaKey)) {
|
|
645
673
|
undo();
|
|
646
674
|
}
|
|
@@ -651,8 +679,10 @@ function _onSketchKeyDown(e) {
|
|
|
651
679
|
*/
|
|
652
680
|
function _onSketchResize() {
|
|
653
681
|
if (sketchState.canvas) {
|
|
654
|
-
|
|
655
|
-
|
|
682
|
+
const vp = document.getElementById('viewport-container');
|
|
683
|
+
const rect = vp ? vp.getBoundingClientRect() : { width: window.innerWidth, height: window.innerHeight };
|
|
684
|
+
sketchState.canvas.width = rect.width;
|
|
685
|
+
sketchState.canvas.height = rect.height;
|
|
656
686
|
_renderSketchCanvas();
|
|
657
687
|
}
|
|
658
688
|
}
|
package/app/js/tree.js
CHANGED
|
@@ -44,7 +44,12 @@ export function initTree(containerEl) {
|
|
|
44
44
|
</div>
|
|
45
45
|
<div class="tree-list" id="tree-list"></div>
|
|
46
46
|
<div class="tree-empty" id="tree-empty">
|
|
47
|
-
<
|
|
47
|
+
<div style="text-align:center;padding:20px 12px;">
|
|
48
|
+
<div style="font-size:32px;margin-bottom:8px;opacity:0.5;">📐</div>
|
|
49
|
+
<p style="font-size:12px;font-weight:600;margin-bottom:6px;color:var(--text-primary, #e0e0e0);">No features yet</p>
|
|
50
|
+
<p style="font-size:11px;color:var(--text-secondary, #a0a0a0);line-height:1.5;">Click <b>New Sketch</b> to draw a 2D profile, then <b>Extrude</b> to make it 3D.</p>
|
|
51
|
+
<p style="font-size:10px;color:var(--text-muted, #696969);margin-top:8px;">Features will appear here as you build.</p>
|
|
52
|
+
</div>
|
|
48
53
|
</div>
|
|
49
54
|
</div>
|
|
50
55
|
`;
|
package/app/js/viewport.js
CHANGED
|
@@ -628,6 +628,14 @@ export function getControls() {
|
|
|
628
628
|
return controls;
|
|
629
629
|
}
|
|
630
630
|
|
|
631
|
+
/**
|
|
632
|
+
* Fit camera to show entire scene
|
|
633
|
+
* @param {number} padding - Padding multiplier (default 1.2)
|
|
634
|
+
*/
|
|
635
|
+
export function fitAll(padding = 1.2) {
|
|
636
|
+
if (scene) fitToObject(scene, padding);
|
|
637
|
+
}
|
|
638
|
+
|
|
631
639
|
// ============================================================================
|
|
632
640
|
// Event Handlers
|
|
633
641
|
// ============================================================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cyclecad",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"description": "Browser-based parametric 3D CAD modeler with AI-powered tools, native Inventor file parsing, and smart assembly management. No install required.",
|
|
5
5
|
"main": "index.html",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"cyclewash"
|
|
33
33
|
],
|
|
34
34
|
"author": "vvlars <vvlars@googlemail.com>",
|
|
35
|
-
"license": "
|
|
35
|
+
"license": "MIT",
|
|
36
36
|
"repository": {
|
|
37
37
|
"type": "git",
|
|
38
38
|
"url": "https://github.com/vvlars-cmd/cyclecad.git"
|