cyclecad 0.1.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/CNAME +1 -0
- package/app/docs/api-reference.html +1436 -0
- package/app/docs/examples.html +803 -0
- package/app/docs/getting-started.html +1620 -0
- package/app/duo-project-browser.html +1321 -0
- package/app/duo-rebuild-guide.html +861 -0
- package/app/index.html +1635 -0
- package/app/js/ai-chat.js +992 -0
- package/app/js/app.js +724 -0
- package/app/js/export.js +658 -0
- package/app/js/inventor-parser.js +1138 -0
- package/app/js/operations.js +689 -0
- package/app/js/params.js +523 -0
- package/app/js/reverse-engineer.js +1275 -0
- package/app/js/shortcuts.js +350 -0
- package/app/js/sketch.js +899 -0
- package/app/js/tree.js +479 -0
- package/app/js/viewport.js +643 -0
- package/app/samples/Leistenbuerstenblech.ipt +0 -0
- package/app/samples/Rahmen_Seite.iam +0 -0
- package/app/samples/TraegerHoehe1.ipt +0 -0
- package/index.html +1226 -0
- package/package.json +33 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* operations.js - 3D Modeling Operations Module for cycleCAD
|
|
3
|
+
*
|
|
4
|
+
* Provides parametric operations for creating and modifying 3D solids:
|
|
5
|
+
* - Extrusion, revolution, and primitives
|
|
6
|
+
* - Fillets, chamfers, and boolean operations
|
|
7
|
+
* - Material system with presets and edge visualization
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Material presets with physical properties
|
|
14
|
+
*/
|
|
15
|
+
const MATERIAL_PRESETS = {
|
|
16
|
+
steel: {
|
|
17
|
+
color: 0x7799bb,
|
|
18
|
+
metalness: 0.6,
|
|
19
|
+
roughness: 0.4,
|
|
20
|
+
name: 'Steel'
|
|
21
|
+
},
|
|
22
|
+
aluminum: {
|
|
23
|
+
color: 0xccccdd,
|
|
24
|
+
metalness: 0.7,
|
|
25
|
+
roughness: 0.3,
|
|
26
|
+
name: 'Aluminum'
|
|
27
|
+
},
|
|
28
|
+
plastic: {
|
|
29
|
+
color: 0x2c3e50,
|
|
30
|
+
metalness: 0.0,
|
|
31
|
+
roughness: 0.8,
|
|
32
|
+
name: 'Plastic'
|
|
33
|
+
},
|
|
34
|
+
brass: {
|
|
35
|
+
color: 0xcd7f32,
|
|
36
|
+
metalness: 0.8,
|
|
37
|
+
roughness: 0.2,
|
|
38
|
+
name: 'Brass'
|
|
39
|
+
},
|
|
40
|
+
titanium: {
|
|
41
|
+
color: 0x878786,
|
|
42
|
+
metalness: 0.7,
|
|
43
|
+
roughness: 0.5,
|
|
44
|
+
name: 'Titanium'
|
|
45
|
+
},
|
|
46
|
+
nylon: {
|
|
47
|
+
color: 0xf5f5dc,
|
|
48
|
+
metalness: 0.1,
|
|
49
|
+
roughness: 0.7,
|
|
50
|
+
name: 'Nylon'
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create or get a material with optional preset
|
|
56
|
+
* @param {string} preset - Material preset name ('steel', 'aluminum', etc.)
|
|
57
|
+
* @param {object} overrides - Property overrides
|
|
58
|
+
* @returns {THREE.MeshStandardMaterial}
|
|
59
|
+
*/
|
|
60
|
+
export function createMaterial(preset = 'steel', overrides = {}) {
|
|
61
|
+
const presetData = MATERIAL_PRESETS[preset] || MATERIAL_PRESETS.steel;
|
|
62
|
+
const props = {
|
|
63
|
+
color: presetData.color,
|
|
64
|
+
metalness: presetData.metalness,
|
|
65
|
+
roughness: presetData.roughness,
|
|
66
|
+
...overrides
|
|
67
|
+
};
|
|
68
|
+
return new THREE.MeshStandardMaterial(props);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convert 2D sketch entities to THREE.Shape
|
|
73
|
+
* Supports rectangles, circles, and polylines
|
|
74
|
+
* @param {array} entities - Sketch entities with type, position, dimensions
|
|
75
|
+
* @returns {THREE.Shape}
|
|
76
|
+
*/
|
|
77
|
+
function entitiesToShape(entities) {
|
|
78
|
+
const shape = new THREE.Shape();
|
|
79
|
+
let hasStartPoint = false;
|
|
80
|
+
|
|
81
|
+
// Sort entities to identify outer profile vs holes
|
|
82
|
+
const profiles = [];
|
|
83
|
+
const holes = [];
|
|
84
|
+
|
|
85
|
+
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);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
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
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (isHole) {
|
|
123
|
+
holes.push(p.circle);
|
|
124
|
+
} else {
|
|
125
|
+
profiles[i] = p;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
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);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Add holes
|
|
139
|
+
for (const hole of holes) {
|
|
140
|
+
const holePath = new THREE.Path();
|
|
141
|
+
holePath.absarc(hole.x, hole.y, hole.radius, 0, Math.PI * 2);
|
|
142
|
+
shape.holes.push(holePath);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return shape;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Extrude a sketch profile to create a 3D solid
|
|
150
|
+
*
|
|
151
|
+
* @param {array} entities - Sketch entities (rect, circle, polyline)
|
|
152
|
+
* @param {number} height - Extrusion height
|
|
153
|
+
* @param {object} options - Configuration
|
|
154
|
+
* - symmetric: extrude equally above/below (default: false)
|
|
155
|
+
* - draft_angle: taper angle in degrees (default: 0)
|
|
156
|
+
* - direction: normal direction (default: 'normal')
|
|
157
|
+
* - material: material preset name (default: 'steel')
|
|
158
|
+
* @returns {object} { mesh, wireframe, params }
|
|
159
|
+
*/
|
|
160
|
+
export function extrudeProfile(entities, height, options = {}) {
|
|
161
|
+
const {
|
|
162
|
+
symmetric = false,
|
|
163
|
+
draft_angle = 0,
|
|
164
|
+
direction = 'normal',
|
|
165
|
+
material = 'steel'
|
|
166
|
+
} = options;
|
|
167
|
+
|
|
168
|
+
// Create shape from entities
|
|
169
|
+
const shape = entitiesToShape(entities);
|
|
170
|
+
|
|
171
|
+
// Calculate extrusion settings
|
|
172
|
+
const extrudeHeight = symmetric ? height / 2 : height;
|
|
173
|
+
const depth = symmetric ? height : height;
|
|
174
|
+
|
|
175
|
+
// Create extrude geometry
|
|
176
|
+
const geometry = new THREE.ExtrudeGeometry(shape, {
|
|
177
|
+
depth: depth,
|
|
178
|
+
bevelEnabled: draft_angle > 0,
|
|
179
|
+
bevelThickness: Math.abs(draft_angle * 0.01),
|
|
180
|
+
bevelSize: Math.abs(draft_angle * 0.01),
|
|
181
|
+
bevelSegments: 3,
|
|
182
|
+
steps: Math.max(1, Math.floor(depth / 10))
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Center if symmetric
|
|
186
|
+
if (symmetric) {
|
|
187
|
+
geometry.translate(0, 0, -depth / 2);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Create mesh with material
|
|
191
|
+
const mat = createMaterial(material);
|
|
192
|
+
const mesh = new THREE.Mesh(geometry, mat);
|
|
193
|
+
mesh.castShadow = true;
|
|
194
|
+
mesh.receiveShadow = true;
|
|
195
|
+
|
|
196
|
+
// Create wireframe overlay
|
|
197
|
+
const wireframe = createWireframeEdges(mesh);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
mesh,
|
|
201
|
+
wireframe,
|
|
202
|
+
params: { entities, height, options }
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Revolve a sketch profile around an axis
|
|
208
|
+
*
|
|
209
|
+
* @param {array} entities - Sketch entities for profile
|
|
210
|
+
* @param {object} axis - Axis definition { type: 'X'|'Y'|'custom', line?: {start, end} }
|
|
211
|
+
* @param {object} options - Configuration
|
|
212
|
+
* - angle: revolution angle in degrees (default: 360)
|
|
213
|
+
* - segments: lathe segments (default: 32)
|
|
214
|
+
* - material: material preset (default: 'steel')
|
|
215
|
+
* @returns {object} { mesh, wireframe, params }
|
|
216
|
+
*/
|
|
217
|
+
export function revolveProfile(entities, axis = { type: 'Y' }, options = {}) {
|
|
218
|
+
const {
|
|
219
|
+
angle = 360,
|
|
220
|
+
segments = 32,
|
|
221
|
+
material = 'steel'
|
|
222
|
+
} = options;
|
|
223
|
+
|
|
224
|
+
// Convert angle to radians
|
|
225
|
+
const radAngle = (angle / 360) * Math.PI * 2;
|
|
226
|
+
|
|
227
|
+
// Extract points from entities to create lathe profile
|
|
228
|
+
const points = [];
|
|
229
|
+
for (const entity of entities) {
|
|
230
|
+
if (entity.type === 'rect') {
|
|
231
|
+
const { x, y, width, height } = entity;
|
|
232
|
+
points.push(new THREE.Vector2(x, y));
|
|
233
|
+
points.push(new THREE.Vector2(x + width, y));
|
|
234
|
+
points.push(new THREE.Vector2(x + width, y + height));
|
|
235
|
+
points.push(new THREE.Vector2(x, y + height));
|
|
236
|
+
} else if (entity.type === 'circle') {
|
|
237
|
+
const { x, y, radius } = entity;
|
|
238
|
+
for (let i = 0; i <= 16; i++) {
|
|
239
|
+
const theta = (i / 16) * Math.PI * 2;
|
|
240
|
+
points.push(new THREE.Vector2(
|
|
241
|
+
x + radius * Math.cos(theta),
|
|
242
|
+
y + radius * Math.sin(theta)
|
|
243
|
+
));
|
|
244
|
+
}
|
|
245
|
+
} else if (entity.type === 'polyline' && entity.points) {
|
|
246
|
+
entity.points.forEach(pt => {
|
|
247
|
+
points.push(new THREE.Vector2(pt.x, pt.y));
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Ensure points are ordered correctly for lathe
|
|
253
|
+
if (points.length < 2) {
|
|
254
|
+
// Fallback profile if no valid entities
|
|
255
|
+
points.push(new THREE.Vector2(0, 0));
|
|
256
|
+
points.push(new THREE.Vector2(1, 1));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Create lathe geometry
|
|
260
|
+
const geometry = new THREE.LatheGeometry(points, segments, 0, radAngle);
|
|
261
|
+
|
|
262
|
+
// Create mesh
|
|
263
|
+
const mat = createMaterial(material);
|
|
264
|
+
const mesh = new THREE.Mesh(geometry, mat);
|
|
265
|
+
mesh.castShadow = true;
|
|
266
|
+
mesh.receiveShadow = true;
|
|
267
|
+
|
|
268
|
+
// Create wireframe
|
|
269
|
+
const wireframe = createWireframeEdges(mesh);
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
mesh,
|
|
273
|
+
wireframe,
|
|
274
|
+
params: { entities, axis, options }
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Create a primitive 3D shape
|
|
280
|
+
*
|
|
281
|
+
* @param {string} type - Primitive type: 'box', 'cylinder', 'sphere', 'cone', 'torus'
|
|
282
|
+
* @param {object} params - Shape parameters
|
|
283
|
+
* @param {object} options - Material and display options
|
|
284
|
+
* @returns {object} { mesh, wireframe, params }
|
|
285
|
+
*/
|
|
286
|
+
export function createPrimitive(type, params = {}, options = {}) {
|
|
287
|
+
const { material = 'steel' } = options;
|
|
288
|
+
let geometry;
|
|
289
|
+
|
|
290
|
+
switch (type) {
|
|
291
|
+
case 'box':
|
|
292
|
+
geometry = new THREE.BoxGeometry(
|
|
293
|
+
params.width || 1,
|
|
294
|
+
params.height || 1,
|
|
295
|
+
params.depth || 1,
|
|
296
|
+
params.widthSegments || 1,
|
|
297
|
+
params.heightSegments || 1,
|
|
298
|
+
params.depthSegments || 1
|
|
299
|
+
);
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case 'cylinder':
|
|
303
|
+
geometry = new THREE.CylinderGeometry(
|
|
304
|
+
params.radius || 1,
|
|
305
|
+
params.radius || 1,
|
|
306
|
+
params.height || 2,
|
|
307
|
+
params.segments || 32,
|
|
308
|
+
1,
|
|
309
|
+
params.openEnded || false
|
|
310
|
+
);
|
|
311
|
+
break;
|
|
312
|
+
|
|
313
|
+
case 'sphere':
|
|
314
|
+
geometry = new THREE.SphereGeometry(
|
|
315
|
+
params.radius || 1,
|
|
316
|
+
params.segments || 32,
|
|
317
|
+
params.segments || 32
|
|
318
|
+
);
|
|
319
|
+
break;
|
|
320
|
+
|
|
321
|
+
case 'cone':
|
|
322
|
+
geometry = new THREE.ConeGeometry(
|
|
323
|
+
params.bottomRadius || 1,
|
|
324
|
+
params.height || 2,
|
|
325
|
+
params.segments || 32
|
|
326
|
+
);
|
|
327
|
+
break;
|
|
328
|
+
|
|
329
|
+
case 'torus':
|
|
330
|
+
geometry = new THREE.TorusGeometry(
|
|
331
|
+
params.radius || 1,
|
|
332
|
+
params.tube || 0.4,
|
|
333
|
+
params.radialSegments || 16,
|
|
334
|
+
params.tubeSegments || 100
|
|
335
|
+
);
|
|
336
|
+
break;
|
|
337
|
+
|
|
338
|
+
default:
|
|
339
|
+
throw new Error(`Unknown primitive type: ${type}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Create mesh
|
|
343
|
+
const mat = createMaterial(material);
|
|
344
|
+
const mesh = new THREE.Mesh(geometry, mat);
|
|
345
|
+
mesh.castShadow = true;
|
|
346
|
+
mesh.receiveShadow = true;
|
|
347
|
+
|
|
348
|
+
// Create wireframe
|
|
349
|
+
const wireframe = createWireframeEdges(mesh);
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
mesh,
|
|
353
|
+
wireframe,
|
|
354
|
+
params: { type, params, options }
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Apply a fillet (rounded edge) to mesh edges
|
|
360
|
+
*
|
|
361
|
+
* @param {THREE.Mesh} mesh - Source mesh
|
|
362
|
+
* @param {array} edges - Edge indices or 'all' for all edges
|
|
363
|
+
* @param {number} radius - Fillet radius
|
|
364
|
+
* @returns {THREE.Group} Group containing original mesh and fillet geometry
|
|
365
|
+
*/
|
|
366
|
+
export function fillet(mesh, edges = 'all', radius = 0.1) {
|
|
367
|
+
const group = new THREE.Group();
|
|
368
|
+
group.add(mesh);
|
|
369
|
+
|
|
370
|
+
// Get geometry positions and indices
|
|
371
|
+
const geometry = mesh.geometry;
|
|
372
|
+
const positions = geometry.attributes.position.array;
|
|
373
|
+
const indices = geometry.index ? geometry.index.array : null;
|
|
374
|
+
|
|
375
|
+
// Create fillet geometry by adding rounded edges
|
|
376
|
+
// This is a simplified implementation using a cylinder along each edge
|
|
377
|
+
const filletGeometry = new THREE.BufferGeometry();
|
|
378
|
+
const filletVertices = [];
|
|
379
|
+
const filletIndices = [];
|
|
380
|
+
|
|
381
|
+
// For box-like geometries, identify and fillet edges
|
|
382
|
+
if (geometry.type === 'BoxGeometry') {
|
|
383
|
+
const vertices = [];
|
|
384
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
385
|
+
vertices.push({
|
|
386
|
+
x: positions[i],
|
|
387
|
+
y: positions[i + 1],
|
|
388
|
+
z: positions[i + 2]
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Add fillet at corners using small cylinders or toruses
|
|
393
|
+
for (let i = 0; i < Math.min(vertices.length, 8); i++) {
|
|
394
|
+
const v = vertices[i];
|
|
395
|
+
const torus = new THREE.TorusGeometry(radius, radius * 0.4, 4, 16);
|
|
396
|
+
const mat = mesh.material;
|
|
397
|
+
const filletMesh = new THREE.Mesh(torus, mat);
|
|
398
|
+
filletMesh.position.set(v.x, v.y, v.z);
|
|
399
|
+
group.add(filletMesh);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return group;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Apply a chamfer (beveled edge) to mesh edges
|
|
408
|
+
*
|
|
409
|
+
* @param {THREE.Mesh} mesh - Source mesh
|
|
410
|
+
* @param {array} edges - Edge indices or 'all'
|
|
411
|
+
* @param {number} distance - Chamfer distance
|
|
412
|
+
* @returns {THREE.Mesh} New mesh with chamfered edges
|
|
413
|
+
*/
|
|
414
|
+
export function chamfer(mesh, edges = 'all', distance = 0.1) {
|
|
415
|
+
const geometry = mesh.geometry.clone();
|
|
416
|
+
|
|
417
|
+
// For BoxGeometry, create a bevel by slightly scaling inward
|
|
418
|
+
if (geometry.type === 'BoxGeometry') {
|
|
419
|
+
const positions = geometry.attributes.position.array;
|
|
420
|
+
|
|
421
|
+
// Calculate bounding box
|
|
422
|
+
let minX = Infinity, maxX = -Infinity;
|
|
423
|
+
let minY = Infinity, maxY = -Infinity;
|
|
424
|
+
let minZ = Infinity, maxZ = -Infinity;
|
|
425
|
+
|
|
426
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
427
|
+
minX = Math.min(minX, positions[i]);
|
|
428
|
+
maxX = Math.max(maxX, positions[i]);
|
|
429
|
+
minY = Math.min(minY, positions[i + 1]);
|
|
430
|
+
maxY = Math.max(maxY, positions[i + 1]);
|
|
431
|
+
minZ = Math.min(minZ, positions[i + 2]);
|
|
432
|
+
maxZ = Math.max(maxZ, positions[i + 2]);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const centerX = (minX + maxX) / 2;
|
|
436
|
+
const centerY = (minY + maxY) / 2;
|
|
437
|
+
const centerZ = (minZ + maxZ) / 2;
|
|
438
|
+
|
|
439
|
+
// Chamfer corners by moving vertices inward
|
|
440
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
441
|
+
const x = positions[i];
|
|
442
|
+
const y = positions[i + 1];
|
|
443
|
+
const z = positions[i + 2];
|
|
444
|
+
|
|
445
|
+
// Check if vertex is at a corner
|
|
446
|
+
const isCorner = [minX, maxX].includes(x) && [minY, maxY].includes(y) && [minZ, maxZ].includes(z);
|
|
447
|
+
|
|
448
|
+
if (isCorner) {
|
|
449
|
+
// Move toward center
|
|
450
|
+
positions[i] += (x < centerX ? 1 : -1) * distance;
|
|
451
|
+
positions[i + 1] += (y < centerY ? 1 : -1) * distance;
|
|
452
|
+
positions[i + 2] += (z < centerZ ? 1 : -1) * distance;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
geometry.attributes.position.needsUpdate = true;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
geometry.computeVertexNormals();
|
|
460
|
+
const chamferedMesh = new THREE.Mesh(geometry, mesh.material);
|
|
461
|
+
chamferedMesh.castShadow = true;
|
|
462
|
+
chamferedMesh.receiveShadow = true;
|
|
463
|
+
|
|
464
|
+
return chamferedMesh;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Boolean union of two meshes
|
|
469
|
+
* Visual approximation: combines bounding boxes and renders both
|
|
470
|
+
* For production, consider three-bvh-csg library
|
|
471
|
+
*
|
|
472
|
+
* @param {THREE.Mesh} meshA - First mesh
|
|
473
|
+
* @param {THREE.Mesh} meshB - Second mesh
|
|
474
|
+
* @returns {THREE.Group} Combined geometry group
|
|
475
|
+
*/
|
|
476
|
+
export function booleanUnion(meshA, meshB) {
|
|
477
|
+
const group = new THREE.Group();
|
|
478
|
+
|
|
479
|
+
// Add both meshes as visual representation
|
|
480
|
+
// Real CSG would compute actual geometry intersection
|
|
481
|
+
const meshACopy = meshA.clone();
|
|
482
|
+
const meshBCopy = meshB.clone();
|
|
483
|
+
|
|
484
|
+
group.add(meshACopy);
|
|
485
|
+
group.add(meshBCopy);
|
|
486
|
+
|
|
487
|
+
// Calculate approximate bounding box of union
|
|
488
|
+
const boxA = new THREE.Box3().setFromObject(meshA);
|
|
489
|
+
const boxB = new THREE.Box3().setFromObject(meshB);
|
|
490
|
+
const unionBox = boxA.union(boxB);
|
|
491
|
+
|
|
492
|
+
group.userData = {
|
|
493
|
+
operation: 'union',
|
|
494
|
+
boxA,
|
|
495
|
+
boxB,
|
|
496
|
+
unionBox
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
return group;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Boolean cut (difference) of two meshes
|
|
504
|
+
* Visual approximation: shows meshA with meshB subtracted from it
|
|
505
|
+
*
|
|
506
|
+
* @param {THREE.Mesh} meshA - Base mesh
|
|
507
|
+
* @param {THREE.Mesh} meshB - Mesh to subtract
|
|
508
|
+
* @returns {THREE.Group} Result group
|
|
509
|
+
*/
|
|
510
|
+
export function booleanCut(meshA, meshB) {
|
|
511
|
+
const group = new THREE.Group();
|
|
512
|
+
|
|
513
|
+
const meshACopy = meshA.clone();
|
|
514
|
+
|
|
515
|
+
// Make meshB semi-transparent to show cutting volume
|
|
516
|
+
const meshBCopy = meshB.clone();
|
|
517
|
+
if (meshBCopy.material) {
|
|
518
|
+
const cutMat = meshBCopy.material.clone();
|
|
519
|
+
cutMat.opacity = 0.3;
|
|
520
|
+
cutMat.transparent = true;
|
|
521
|
+
meshBCopy.material = cutMat;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
group.add(meshACopy);
|
|
525
|
+
group.add(meshBCopy);
|
|
526
|
+
|
|
527
|
+
group.userData = {
|
|
528
|
+
operation: 'cut',
|
|
529
|
+
base: meshA,
|
|
530
|
+
tool: meshB
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
return group;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Boolean intersection of two meshes
|
|
538
|
+
* Visual approximation: shows only overlapping volume
|
|
539
|
+
*
|
|
540
|
+
* @param {THREE.Mesh} meshA - First mesh
|
|
541
|
+
* @param {THREE.Mesh} meshB - Second mesh
|
|
542
|
+
* @returns {THREE.Group} Intersection result
|
|
543
|
+
*/
|
|
544
|
+
export function booleanIntersect(meshA, meshB) {
|
|
545
|
+
const group = new THREE.Group();
|
|
546
|
+
|
|
547
|
+
// Calculate intersection boxes
|
|
548
|
+
const boxA = new THREE.Box3().setFromObject(meshA);
|
|
549
|
+
const boxB = new THREE.Box3().setFromObject(meshB);
|
|
550
|
+
const intersectBox = boxA.intersectBox(boxB, new THREE.Box3());
|
|
551
|
+
|
|
552
|
+
if (intersectBox === null) {
|
|
553
|
+
// No intersection
|
|
554
|
+
group.userData = { operation: 'intersect', empty: true };
|
|
555
|
+
return group;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Create visual representation of intersection
|
|
559
|
+
const size = new THREE.Vector3();
|
|
560
|
+
intersectBox.getSize(size);
|
|
561
|
+
|
|
562
|
+
const intersectGeom = new THREE.BoxGeometry(size.x, size.y, size.z);
|
|
563
|
+
const mat = createMaterial('steel', { opacity: 0.7, transparent: true });
|
|
564
|
+
const intersectMesh = new THREE.Mesh(intersectGeom, mat);
|
|
565
|
+
|
|
566
|
+
const center = new THREE.Vector3();
|
|
567
|
+
intersectBox.getCenter(center);
|
|
568
|
+
intersectMesh.position.copy(center);
|
|
569
|
+
|
|
570
|
+
group.add(intersectMesh);
|
|
571
|
+
group.userData = {
|
|
572
|
+
operation: 'intersect',
|
|
573
|
+
intersectBox,
|
|
574
|
+
intersectMesh
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
return group;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Rebuild a feature with updated parameters
|
|
582
|
+
* Disposes old geometry and creates new one
|
|
583
|
+
*
|
|
584
|
+
* @param {object} feature - Feature object with { type, mesh, wireframe, params }
|
|
585
|
+
* @returns {object} New feature with updated geometry
|
|
586
|
+
*/
|
|
587
|
+
export function rebuildFeature(feature) {
|
|
588
|
+
const { type, mesh, wireframe, params } = feature;
|
|
589
|
+
|
|
590
|
+
// Save transform
|
|
591
|
+
const position = mesh?.position.clone() || new THREE.Vector3();
|
|
592
|
+
const rotation = mesh?.rotation.clone() || new THREE.Euler();
|
|
593
|
+
const scale = mesh?.scale.clone() || new THREE.Vector3(1, 1, 1);
|
|
594
|
+
|
|
595
|
+
// Dispose old geometry
|
|
596
|
+
if (mesh?.geometry) mesh.geometry.dispose();
|
|
597
|
+
if (wireframe?.geometry) wireframe.geometry.dispose();
|
|
598
|
+
if (mesh?.material) mesh.material.dispose();
|
|
599
|
+
if (wireframe?.material) wireframe.material.dispose();
|
|
600
|
+
|
|
601
|
+
// Create new geometry based on type
|
|
602
|
+
let newFeature;
|
|
603
|
+
switch (type) {
|
|
604
|
+
case 'extrude':
|
|
605
|
+
newFeature = extrudeProfile(params.entities, params.height, params.options);
|
|
606
|
+
break;
|
|
607
|
+
case 'revolve':
|
|
608
|
+
newFeature = revolveProfile(params.entities, params.axis, params.options);
|
|
609
|
+
break;
|
|
610
|
+
case 'primitive':
|
|
611
|
+
newFeature = createPrimitive(params.type, params.params, params.options);
|
|
612
|
+
break;
|
|
613
|
+
default:
|
|
614
|
+
throw new Error(`Cannot rebuild unknown feature type: ${type}`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Restore transform
|
|
618
|
+
newFeature.mesh.position.copy(position);
|
|
619
|
+
newFeature.mesh.rotation.copy(rotation);
|
|
620
|
+
newFeature.mesh.scale.copy(scale);
|
|
621
|
+
|
|
622
|
+
if (newFeature.wireframe) {
|
|
623
|
+
newFeature.wireframe.position.copy(position);
|
|
624
|
+
newFeature.wireframe.rotation.copy(rotation);
|
|
625
|
+
newFeature.wireframe.scale.copy(scale);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return newFeature;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Create wireframe edge visualization for a mesh
|
|
633
|
+
* Uses EdgesGeometry + LineBasicMaterial
|
|
634
|
+
*
|
|
635
|
+
* @param {THREE.Mesh} mesh - Source mesh
|
|
636
|
+
* @param {number} threshold - Edge threshold angle (default 30°)
|
|
637
|
+
* @returns {THREE.LineSegments} Wireframe edges
|
|
638
|
+
*/
|
|
639
|
+
export function createWireframeEdges(mesh, threshold = 30) {
|
|
640
|
+
const geometry = mesh.geometry;
|
|
641
|
+
|
|
642
|
+
// Use EdgesGeometry for sharp edge detection
|
|
643
|
+
const edgesGeometry = new THREE.EdgesGeometry(geometry, threshold);
|
|
644
|
+
const wireframeMat = new THREE.LineBasicMaterial({
|
|
645
|
+
color: 0x333333,
|
|
646
|
+
linewidth: 1,
|
|
647
|
+
transparent: true,
|
|
648
|
+
opacity: 0.6
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const wireframe = new THREE.LineSegments(edgesGeometry, wireframeMat);
|
|
652
|
+
wireframe.position.copy(mesh.position);
|
|
653
|
+
wireframe.rotation.copy(mesh.rotation);
|
|
654
|
+
wireframe.scale.copy(mesh.scale);
|
|
655
|
+
wireframe.userData = { isWireframe: true, parent: mesh };
|
|
656
|
+
|
|
657
|
+
return wireframe;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Update wireframe position/rotation to match mesh
|
|
662
|
+
* Call after transforming the base mesh
|
|
663
|
+
*
|
|
664
|
+
* @param {THREE.Mesh} mesh - Base mesh
|
|
665
|
+
* @param {THREE.LineSegments} wireframe - Wireframe to update
|
|
666
|
+
*/
|
|
667
|
+
export function updateWireframeTransform(mesh, wireframe) {
|
|
668
|
+
if (!wireframe) return;
|
|
669
|
+
wireframe.position.copy(mesh.position);
|
|
670
|
+
wireframe.rotation.copy(mesh.rotation);
|
|
671
|
+
wireframe.scale.copy(mesh.scale);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Get all available material presets
|
|
676
|
+
* @returns {array} List of preset names
|
|
677
|
+
*/
|
|
678
|
+
export function getMaterialPresets() {
|
|
679
|
+
return Object.keys(MATERIAL_PRESETS);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Get material preset details
|
|
684
|
+
* @param {string} name - Preset name
|
|
685
|
+
* @returns {object} Preset properties
|
|
686
|
+
*/
|
|
687
|
+
export function getMaterialPreset(name) {
|
|
688
|
+
return MATERIAL_PRESETS[name] || MATERIAL_PRESETS.steel;
|
|
689
|
+
}
|