cyclecad 0.5.0 → 0.8.5

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.
@@ -1894,10 +1894,5 @@ function addFeature(id, type, mesh, params) {
1894
1894
  export {
1895
1895
  on,
1896
1896
  off,
1897
- emit,
1898
- undo,
1899
- redo,
1900
- canUndo,
1901
- canRedo,
1902
- getModules
1897
+ emit
1903
1898
  };
@@ -75,74 +75,137 @@ export function createMaterial(preset = 'steel', overrides = {}) {
75
75
  * @returns {THREE.Shape}
76
76
  */
77
77
  function entitiesToShape(entities) {
78
- const shape = new THREE.Shape();
79
- let hasStartPoint = false;
78
+ if (!entities || entities.length === 0) {
79
+ throw new Error('No sketch entities to convert');
80
+ }
80
81
 
81
- // Sort entities to identify outer profile vs holes
82
- const profiles = [];
83
- const holes = [];
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
- if (entity.type === 'rect') {
87
- const { x, y, width, height } = entity;
88
- const profile = new THREE.Path();
89
- profile.moveTo(x, y);
90
- profile.lineTo(x + width, y);
91
- profile.lineTo(x + width, y + height);
92
- profile.lineTo(x, y + height);
93
- profile.lineTo(x, y);
94
- profiles.push(profile);
95
- } else if (entity.type === 'circle') {
96
- const { x, y, radius } = entity;
97
- const profile = new THREE.Path();
98
- profile.absarc(x, y, radius, 0, Math.PI * 2);
99
- profiles.push({ circle: { x, y, radius } });
100
- } else if (entity.type === 'polyline') {
101
- const profile = new THREE.Path();
102
- entity.points.forEach((pt, i) => {
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
- // Determine which circles are holes (inside rectangles)
111
- for (let i = 0; i < profiles.length; i++) {
112
- const p = profiles[i];
113
- if (p.circle) {
114
- let isHole = false;
115
- for (let j = 0; j < profiles.length; j++) {
116
- if (i !== j && !profiles[j].circle) {
117
- // Simple containment check: circle center is inside rect
118
- // This is a simplified check; real implementation would be more robust
119
- isHole = true;
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
- if (isHole) {
123
- holes.push(p.circle);
124
- } else {
125
- profiles[i] = p;
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
- // Build main shape from first profile (usually outer boundary)
131
- if (profiles.length > 0 && !profiles[0].circle) {
132
- shape.moveTo(0, 0);
133
- for (let i = 0; i < 10; i++) {
134
- shape.lineTo(i * 0.1, Math.sin(i * 0.1) * 0.5);
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
- // Add holes
139
- for (const hole of holes) {
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
- holePath.absarc(hole.x, hole.y, hole.radius, 0, Math.PI * 2);
142
- shape.holes.push(holePath);
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 shape;
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
- // Calculate extrusion settings
172
- const extrudeHeight = symmetric ? height / 2 : height;
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
- // Center if symmetric
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
- canvas.width = window.innerWidth;
58
- canvas.height = window.innerHeight;
59
- canvas.style.position = 'fixed';
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 = '9999';
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 = (clientX / canvas.width) * 2 - 1;
184
- const ndcY = -(clientY / canvas.height) * 2 + 1;
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
- if (sketchState.currentTool === 'line' && sketchState.currentPoints.length > 0) {
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 (sketchState.currentTool === 'polyline' && sketchState.currentPoints.length >= 2) {
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
- endSketch();
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
- sketchState.canvas.width = window.innerWidth;
655
- sketchState.canvas.height = window.innerHeight;
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
- <p>Start with a sketch</p>
47
+ <div style="text-align:center;padding:20px 12px;">
48
+ <div style="font-size:32px;margin-bottom:8px;opacity:0.5;">&#x1F4D0;</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
  `;
@@ -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.5.0",
3
+ "version": "0.8.5",
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": "BSL-1.1",
35
+ "license": "MIT",
36
36
  "repository": {
37
37
  "type": "git",
38
38
  "url": "https://github.com/vvlars-cmd/cyclecad.git"