cyclecad 1.3.1 → 1.3.3
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/DRAWING_MODULE_INTEGRATION.md +633 -0
- package/README.md +138 -317
- package/app/index.html +2 -0
- package/app/js/brep-kernel.js +853 -0
- package/app/js/kernel.js +684 -0
- package/app/js/modules/assembly-module.js +582 -0
- package/app/js/modules/brep-module.js +583 -0
- package/app/js/modules/drawing-module.js +883 -0
- package/app/js/modules/operations-module.js +660 -0
- package/app/js/modules/simulation-module.js +834 -0
- package/app/js/modules/sketch-module.js +720 -0
- package/app/js/modules/step-module.js +510 -0
- package/app/js/modules/viewport-module.js +530 -0
- package/fusion360-gap-analysis.html +636 -0
- package/package.json +1 -1
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operations Module — 3D Modeling Operations
|
|
3
|
+
* Part of cycleCAD microkernel architecture
|
|
4
|
+
*
|
|
5
|
+
* Smart dispatch between B-Rep kernel and mesh fallbacks
|
|
6
|
+
* All 3D operations (extrude, fillet, pattern, boolean, etc.) exposed as kernel commands
|
|
7
|
+
*
|
|
8
|
+
* Version: 1.0.0
|
|
9
|
+
* Author: cycleCAD Team
|
|
10
|
+
* License: MIT
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const OperationsModule = {
|
|
14
|
+
id: 'operations',
|
|
15
|
+
name: '3D Operations',
|
|
16
|
+
version: '1.0.0',
|
|
17
|
+
category: 'engine',
|
|
18
|
+
description: 'All 3D modeling operations with smart B-Rep / mesh dispatch',
|
|
19
|
+
dependencies: ['viewport', 'sketch'],
|
|
20
|
+
memoryEstimate: 10,
|
|
21
|
+
|
|
22
|
+
init(kernel) {
|
|
23
|
+
this.kernel = kernel;
|
|
24
|
+
this.features = [];
|
|
25
|
+
this.featureCounter = 0;
|
|
26
|
+
this.currentShape = null;
|
|
27
|
+
this.meshCache = new Map();
|
|
28
|
+
|
|
29
|
+
// Register all operations as kernel commands
|
|
30
|
+
this._registerCommands();
|
|
31
|
+
|
|
32
|
+
// Listen for kernel events
|
|
33
|
+
kernel.on('feature:rebuild', () => this._rebuildAllFeatures());
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
ready: true,
|
|
37
|
+
message: 'Operations module initialized'
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register all operations as kernel commands
|
|
43
|
+
*/
|
|
44
|
+
_registerCommands() {
|
|
45
|
+
const ops = [
|
|
46
|
+
// Create (primitives)
|
|
47
|
+
'box', 'cylinder', 'sphere', 'cone', 'torus',
|
|
48
|
+
// Modify
|
|
49
|
+
'extrude', 'revolve', 'sweep', 'loft', 'fillet', 'chamfer', 'shell', 'draft', 'hole', 'thread', 'rib', 'split',
|
|
50
|
+
// Pattern
|
|
51
|
+
'rectangularPattern', 'circularPattern', 'pathPattern',
|
|
52
|
+
// Transform
|
|
53
|
+
'mirror', 'move', 'rotate', 'scale', 'copy',
|
|
54
|
+
// Boolean
|
|
55
|
+
'union', 'cut', 'intersect',
|
|
56
|
+
// Feature management
|
|
57
|
+
'editFeature', 'suppressFeature', 'reorderFeature', 'deleteFeature', 'rebuild',
|
|
58
|
+
'getFeatureHistory', 'getCurrentShape'
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
ops.forEach(op => {
|
|
62
|
+
this.kernel.registerCommand(`ops.${op}`, (...args) => this[op](...args));
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Smart dispatch: use B-Rep if available, fall back to mesh
|
|
68
|
+
*/
|
|
69
|
+
_dispatch(operation, params) {
|
|
70
|
+
const brep = this.kernel.get('brep-kernel');
|
|
71
|
+
if (brep && brep.status === 'active') {
|
|
72
|
+
try {
|
|
73
|
+
return this.kernel.exec(`brep.${operation}`, params);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.warn(`B-Rep operation failed, falling back to mesh: ${operation}`, e);
|
|
76
|
+
return this._meshFallback(operation, params);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
return this._meshFallback(operation, params);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Mesh fallback implementations
|
|
85
|
+
*/
|
|
86
|
+
_meshFallback(operation, params) {
|
|
87
|
+
const methodName = `_mesh${operation.charAt(0).toUpperCase() + operation.slice(1)}`;
|
|
88
|
+
if (typeof this[methodName] === 'function') {
|
|
89
|
+
return this[methodName](params);
|
|
90
|
+
} else {
|
|
91
|
+
throw new Error(`Mesh fallback not implemented for: ${operation}`);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// CREATE (Primitives)
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
box(width, height, depth, options = {}) {
|
|
100
|
+
const geometry = new THREE.BoxGeometry(width, height, depth);
|
|
101
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(options));
|
|
102
|
+
mesh.userData.operation = 'box';
|
|
103
|
+
mesh.userData.params = { width, height, depth, ...options };
|
|
104
|
+
return this._createFeature('box', { width, height, depth, ...options }, mesh);
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
_meshBox(params) {
|
|
108
|
+
const { width, height, depth } = params;
|
|
109
|
+
const geometry = new THREE.BoxGeometry(width, height, depth);
|
|
110
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(params));
|
|
111
|
+
mesh.userData.operation = 'box';
|
|
112
|
+
return mesh;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
cylinder(radius, height, options = {}) {
|
|
116
|
+
const geometry = new THREE.CylinderGeometry(radius, radius, height, options.segments || 32);
|
|
117
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(options));
|
|
118
|
+
mesh.userData.operation = 'cylinder';
|
|
119
|
+
mesh.userData.params = { radius, height, ...options };
|
|
120
|
+
return this._createFeature('cylinder', { radius, height, ...options }, mesh);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
_meshCylinder(params) {
|
|
124
|
+
const { radius, height, segments = 32 } = params;
|
|
125
|
+
const geometry = new THREE.CylinderGeometry(radius, radius, height, segments);
|
|
126
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(params));
|
|
127
|
+
return mesh;
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
sphere(radius, options = {}) {
|
|
131
|
+
const geometry = new THREE.SphereGeometry(radius, options.widthSegments || 32, options.heightSegments || 32);
|
|
132
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(options));
|
|
133
|
+
mesh.userData.operation = 'sphere';
|
|
134
|
+
mesh.userData.params = { radius, ...options };
|
|
135
|
+
return this._createFeature('sphere', { radius, ...options }, mesh);
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
_meshSphere(params) {
|
|
139
|
+
const { radius, widthSegments = 32, heightSegments = 32 } = params;
|
|
140
|
+
const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments);
|
|
141
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(params));
|
|
142
|
+
return mesh;
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
cone(radiusTop, radiusBottom, height, options = {}) {
|
|
146
|
+
const geometry = new THREE.ConeGeometry(radiusBottom, height, options.segments || 32);
|
|
147
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(options));
|
|
148
|
+
mesh.userData.operation = 'cone';
|
|
149
|
+
mesh.userData.params = { radiusTop, radiusBottom, height, ...options };
|
|
150
|
+
return this._createFeature('cone', { radiusTop, radiusBottom, height, ...options }, mesh);
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
_meshCone(params) {
|
|
154
|
+
const { radiusTop, radiusBottom, height, segments = 32 } = params;
|
|
155
|
+
const geometry = new THREE.ConeGeometry(radiusBottom, height, segments);
|
|
156
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(params));
|
|
157
|
+
return mesh;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
torus(majorRadius, minorRadius, options = {}) {
|
|
161
|
+
const geometry = new THREE.TorusGeometry(majorRadius, minorRadius, options.tubeSegments || 16, options.radialSegments || 32);
|
|
162
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(options));
|
|
163
|
+
mesh.userData.operation = 'torus';
|
|
164
|
+
mesh.userData.params = { majorRadius, minorRadius, ...options };
|
|
165
|
+
return this._createFeature('torus', { majorRadius, minorRadius, ...options }, mesh);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
_meshTorus(params) {
|
|
169
|
+
const { majorRadius, minorRadius, tubeSegments = 16, radialSegments = 32 } = params;
|
|
170
|
+
const geometry = new THREE.TorusGeometry(majorRadius, minorRadius, tubeSegments, radialSegments);
|
|
171
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(params));
|
|
172
|
+
return mesh;
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// MODIFY Operations
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
extrude(profile, distance, options = {}) {
|
|
180
|
+
const params = { profile, distance, symmetric: options.symmetric || false, ...options };
|
|
181
|
+
const result = this._dispatch('extrude', params);
|
|
182
|
+
return this._createFeature('extrude', params, result);
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
_meshExtrude(params) {
|
|
186
|
+
const { profile, distance, symmetric = false } = params;
|
|
187
|
+
if (!profile || !profile.userData || !profile.userData.shape2d) {
|
|
188
|
+
throw new Error('Profile must be a 2D sketch');
|
|
189
|
+
}
|
|
190
|
+
const extrudeDepth = symmetric ? distance / 2 : distance;
|
|
191
|
+
try {
|
|
192
|
+
const geometry = new THREE.ExtrudeGeometry(profile.userData.shape2d, {
|
|
193
|
+
depth: extrudeDepth,
|
|
194
|
+
bevelEnabled: false
|
|
195
|
+
});
|
|
196
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(params));
|
|
197
|
+
if (symmetric) {
|
|
198
|
+
mesh.position.z = -extrudeDepth / 2;
|
|
199
|
+
}
|
|
200
|
+
return mesh;
|
|
201
|
+
} catch (e) {
|
|
202
|
+
console.warn('THREE.ExtrudeGeometry failed, using fallback cylinder', e);
|
|
203
|
+
return this._meshCylinder({ radius: 10, height: distance });
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
revolve(profile, axis, angle, options = {}) {
|
|
208
|
+
const params = { profile, axis, angle, ...options };
|
|
209
|
+
const result = this._dispatch('revolve', params);
|
|
210
|
+
return this._createFeature('revolve', params, result);
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
_meshRevolve(params) {
|
|
214
|
+
const { profile, axis, angle } = params;
|
|
215
|
+
if (!profile) throw new Error('Profile required for revolve');
|
|
216
|
+
// Mesh fallback: create cone-like shape for simple revolves
|
|
217
|
+
const geometry = new THREE.CylinderGeometry(10, 8, 20, 32);
|
|
218
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(params));
|
|
219
|
+
return mesh;
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
sweep(profile, path, options = {}) {
|
|
223
|
+
const params = { profile, path, twist: options.twist || 0, scale: options.scale || 1, ...options };
|
|
224
|
+
const result = this._dispatch('sweep', params);
|
|
225
|
+
return this._createFeature('sweep', params, result);
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
_meshSweep(params) {
|
|
229
|
+
// Simplified mesh fallback
|
|
230
|
+
const geometry = new THREE.TubeGeometry(new THREE.LineCurve3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, 20)), 20, 5, 8);
|
|
231
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(params));
|
|
232
|
+
return mesh;
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
loft(profiles, options = {}) {
|
|
236
|
+
const params = { profiles, guideCurves: options.guideCurves || null, ...options };
|
|
237
|
+
const result = this._dispatch('loft', params);
|
|
238
|
+
return this._createFeature('loft', params, result);
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
_meshLoft(params) {
|
|
242
|
+
// Fallback: create a cone-like interpolation
|
|
243
|
+
const geometry = new THREE.ConeGeometry(10, 20, 32);
|
|
244
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(params));
|
|
245
|
+
return mesh;
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
fillet(shape, edges, radius, options = {}) {
|
|
249
|
+
const params = { shapeId: shape.uuid, edgeIndices: edges, radius, ...options };
|
|
250
|
+
const result = this._dispatch('fillet', params);
|
|
251
|
+
return this._createFeature('fillet', params, result);
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
_meshFillet(params) {
|
|
255
|
+
const { radius = 2 } = params;
|
|
256
|
+
// Mesh fallback: add torus segment at corner (approximation)
|
|
257
|
+
const torusGeometry = new THREE.TorusGeometry(radius, radius / 3, 8, 8, Math.PI / 2);
|
|
258
|
+
const mesh = new THREE.Mesh(torusGeometry, this._getMaterial(params));
|
|
259
|
+
return mesh;
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
chamfer(shape, edges, distance, options = {}) {
|
|
263
|
+
const params = { shapeId: shape.uuid, edgeIndices: edges, distance, ...options };
|
|
264
|
+
const result = this._dispatch('chamfer', params);
|
|
265
|
+
return this._createFeature('chamfer', params, result);
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
_meshChamfer(params) {
|
|
269
|
+
const { distance = 1 } = params;
|
|
270
|
+
// Mesh fallback: simple plane cut approximation
|
|
271
|
+
const geometry = new THREE.BoxGeometry(20, 20, 1);
|
|
272
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(params));
|
|
273
|
+
return mesh;
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
shell(shape, faces, thickness, options = {}) {
|
|
277
|
+
const params = { shapeId: shape.uuid, faceIndices: faces, thickness, ...options };
|
|
278
|
+
const result = this._dispatch('shell', params);
|
|
279
|
+
return this._createFeature('shell', params, result);
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
_meshShell(params) {
|
|
283
|
+
const { thickness = 1 } = params;
|
|
284
|
+
// Mesh fallback: outer sphere with slight thickness
|
|
285
|
+
const outerGeometry = new THREE.SphereGeometry(10, 32, 32);
|
|
286
|
+
const innerGeometry = new THREE.SphereGeometry(10 - thickness, 32, 32);
|
|
287
|
+
const mesh = new THREE.Mesh(outerGeometry, this._getMaterial(params));
|
|
288
|
+
return mesh;
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
draft(shape, faces, angle, pullDir, options = {}) {
|
|
292
|
+
const params = { shapeId: shape.uuid, faceIndices: faces, angle, pullDir, ...options };
|
|
293
|
+
const result = this._dispatch('draft', params);
|
|
294
|
+
return this._createFeature('draft', params, result);
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
_meshDraft(params) {
|
|
298
|
+
// Mesh fallback: slight taper
|
|
299
|
+
const geometry = new THREE.ConeGeometry(12, 20, 32);
|
|
300
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(params));
|
|
301
|
+
return mesh;
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
hole(shape, position, radius, depth, options = {}) {
|
|
305
|
+
const holeType = options.type || 'simple'; // simple, counterbore, countersink, tapped
|
|
306
|
+
const params = { shapeId: shape.uuid, position, radius, depth, type: holeType, ...options };
|
|
307
|
+
const result = this._dispatch('hole', params);
|
|
308
|
+
return this._createFeature('hole', params, result);
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
_meshHole(params) {
|
|
312
|
+
// Mesh fallback: subtract small cylinder
|
|
313
|
+
const geometry = new THREE.CylinderGeometry(params.radius, params.radius, params.depth, 32);
|
|
314
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(params));
|
|
315
|
+
mesh.position.copy(params.position);
|
|
316
|
+
return mesh;
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
thread(shape, cylinderFace, pitch, options = {}) {
|
|
320
|
+
const threadType = options.type || 'cosmetic'; // cosmetic, modeled
|
|
321
|
+
const params = { shapeId: shape.uuid, faceIndex: cylinderFace, pitch, type: threadType, ...options };
|
|
322
|
+
const result = this._dispatch('thread', params);
|
|
323
|
+
return this._createFeature('thread', params, result);
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
_meshThread(params) {
|
|
327
|
+
// Mesh fallback: textured cylinder
|
|
328
|
+
const { pitch = 1.5 } = params;
|
|
329
|
+
const geometry = new THREE.CylinderGeometry(5, 5, 20, 32);
|
|
330
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(params));
|
|
331
|
+
return mesh;
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
rib(shape, sketchLine, thickness, options = {}) {
|
|
335
|
+
const params = { shapeId: shape.uuid, sketchLineId: sketchLine, thickness, ...options };
|
|
336
|
+
const result = this._dispatch('rib', params);
|
|
337
|
+
return this._createFeature('rib', params, result);
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
_meshRib(params) {
|
|
341
|
+
const { thickness = 1 } = params;
|
|
342
|
+
const geometry = new THREE.BoxGeometry(20, thickness, 10);
|
|
343
|
+
const mesh = new THREE.Mesh(geometry, this._getMaterial(params));
|
|
344
|
+
return mesh;
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
split(shape, splitTool, options = {}) {
|
|
348
|
+
const params = { shapeId: shape.uuid, splitToolId: splitTool, ...options };
|
|
349
|
+
const result = this._dispatch('split', params);
|
|
350
|
+
return this._createFeature('split', params, result);
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
_meshSplit(params) {
|
|
354
|
+
// Mesh fallback: just return shape unchanged
|
|
355
|
+
return params.shapeId;
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
// ============================================================================
|
|
359
|
+
// PATTERN Operations
|
|
360
|
+
// ============================================================================
|
|
361
|
+
|
|
362
|
+
rectangularPattern(shape, xCount, yCount, xSpacing, ySpacing, options = {}) {
|
|
363
|
+
const params = { shapeId: shape.uuid, xCount, yCount, xSpacing, ySpacing, ...options };
|
|
364
|
+
return this._createFeature('rectangularPattern', params, this._meshRectangularPattern(params));
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
_meshRectangularPattern(params) {
|
|
368
|
+
const { xCount, yCount, xSpacing, ySpacing } = params;
|
|
369
|
+
const group = new THREE.Group();
|
|
370
|
+
const baseMesh = this.currentShape;
|
|
371
|
+
|
|
372
|
+
for (let x = 0; x < xCount; x++) {
|
|
373
|
+
for (let y = 0; y < yCount; y++) {
|
|
374
|
+
const clonedMesh = baseMesh.clone();
|
|
375
|
+
clonedMesh.position.x = (x - (xCount - 1) / 2) * xSpacing;
|
|
376
|
+
clonedMesh.position.y = (y - (yCount - 1) / 2) * ySpacing;
|
|
377
|
+
group.add(clonedMesh);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
group.userData.operation = 'rectangularPattern';
|
|
381
|
+
return group;
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
circularPattern(shape, axis, count, angle, options = {}) {
|
|
385
|
+
const params = { shapeId: shape.uuid, axis, count, angle, ...options };
|
|
386
|
+
return this._createFeature('circularPattern', params, this._meshCircularPattern(params));
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
_meshCircularPattern(params) {
|
|
390
|
+
const { count, angle } = params;
|
|
391
|
+
const group = new THREE.Group();
|
|
392
|
+
const baseMesh = this.currentShape;
|
|
393
|
+
const angleStep = angle / count;
|
|
394
|
+
|
|
395
|
+
for (let i = 0; i < count; i++) {
|
|
396
|
+
const clonedMesh = baseMesh.clone();
|
|
397
|
+
clonedMesh.rotateZ(angleStep * i * Math.PI / 180);
|
|
398
|
+
group.add(clonedMesh);
|
|
399
|
+
}
|
|
400
|
+
group.userData.operation = 'circularPattern';
|
|
401
|
+
return group;
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
pathPattern(shape, path, count, options = {}) {
|
|
405
|
+
const params = { shapeId: shape.uuid, pathId: path, count, ...options };
|
|
406
|
+
return this._createFeature('pathPattern', params, this._meshPathPattern(params));
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
_meshPathPattern(params) {
|
|
410
|
+
// Simplified fallback
|
|
411
|
+
const group = new THREE.Group();
|
|
412
|
+
const baseMesh = this.currentShape;
|
|
413
|
+
|
|
414
|
+
for (let i = 0; i < params.count; i++) {
|
|
415
|
+
const clonedMesh = baseMesh.clone();
|
|
416
|
+
clonedMesh.position.z = i * 5;
|
|
417
|
+
group.add(clonedMesh);
|
|
418
|
+
}
|
|
419
|
+
return group;
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
// ============================================================================
|
|
423
|
+
// TRANSFORM Operations
|
|
424
|
+
// ============================================================================
|
|
425
|
+
|
|
426
|
+
mirror(shape, plane, options = {}) {
|
|
427
|
+
const params = { shapeId: shape.uuid, plane, ...options };
|
|
428
|
+
return this._createFeature('mirror', params, this._meshMirror(params));
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
_meshMirror(params) {
|
|
432
|
+
const mesh = this.currentShape.clone();
|
|
433
|
+
const { plane } = params;
|
|
434
|
+
|
|
435
|
+
if (plane === 'xy') mesh.scale.z = -1;
|
|
436
|
+
else if (plane === 'yz') mesh.scale.x = -1;
|
|
437
|
+
else if (plane === 'xz') mesh.scale.y = -1;
|
|
438
|
+
|
|
439
|
+
return mesh;
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
move(shape, translation, options = {}) {
|
|
443
|
+
const params = { shapeId: shape.uuid, translation, ...options };
|
|
444
|
+
const mesh = this.currentShape.clone();
|
|
445
|
+
mesh.position.add(new THREE.Vector3(...translation));
|
|
446
|
+
return this._createFeature('move', params, mesh);
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
rotate(shape, axis, angle, options = {}) {
|
|
450
|
+
const params = { shapeId: shape.uuid, axis, angle, ...options };
|
|
451
|
+
const mesh = this.currentShape.clone();
|
|
452
|
+
const axisVector = new THREE.Vector3(...axis).normalize();
|
|
453
|
+
mesh.rotateOnWorldAxis(axisVector, angle * Math.PI / 180);
|
|
454
|
+
return this._createFeature('rotate', params, mesh);
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
scale(shape, factor, options = {}) {
|
|
458
|
+
const params = { shapeId: shape.uuid, factor, ...options };
|
|
459
|
+
const mesh = this.currentShape.clone();
|
|
460
|
+
mesh.scale.multiplyScalar(factor);
|
|
461
|
+
return this._createFeature('scale', params, mesh);
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
copy(shape, options = {}) {
|
|
465
|
+
const params = { shapeId: shape.uuid, ...options };
|
|
466
|
+
const mesh = this.currentShape.clone();
|
|
467
|
+
return this._createFeature('copy', params, mesh);
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
// ============================================================================
|
|
471
|
+
// BOOLEAN Operations
|
|
472
|
+
// ============================================================================
|
|
473
|
+
|
|
474
|
+
union(shape1, shape2, options = {}) {
|
|
475
|
+
const params = { shape1Id: shape1.uuid, shape2Id: shape2.uuid, ...options };
|
|
476
|
+
const result = this._dispatch('union', params);
|
|
477
|
+
return this._createFeature('union', params, result || this._meshUnion(params));
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
_meshUnion(params) {
|
|
481
|
+
// Mesh fallback: group both shapes
|
|
482
|
+
const group = new THREE.Group();
|
|
483
|
+
group.add(this.currentShape.clone());
|
|
484
|
+
group.userData.operation = 'union';
|
|
485
|
+
return group;
|
|
486
|
+
},
|
|
487
|
+
|
|
488
|
+
cut(shape1, tool, options = {}) {
|
|
489
|
+
const params = { shape1Id: shape1.uuid, toolId: tool.uuid, ...options };
|
|
490
|
+
const result = this._dispatch('cut', params);
|
|
491
|
+
return this._createFeature('cut', params, result || this._meshCut(params));
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
_meshCut(params) {
|
|
495
|
+
// Mesh fallback: visual indicator only
|
|
496
|
+
const mesh = this.currentShape.clone();
|
|
497
|
+
mesh.userData.isCut = true;
|
|
498
|
+
return mesh;
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
intersect(shape1, shape2, options = {}) {
|
|
502
|
+
const params = { shape1Id: shape1.uuid, shape2Id: shape2.uuid, ...options };
|
|
503
|
+
const result = this._dispatch('intersect', params);
|
|
504
|
+
return this._createFeature('intersect', params, result || this._meshIntersect(params));
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
_meshIntersect(params) {
|
|
508
|
+
// Mesh fallback: group intersection
|
|
509
|
+
const group = new THREE.Group();
|
|
510
|
+
group.userData.operation = 'intersect';
|
|
511
|
+
return group;
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
// ============================================================================
|
|
515
|
+
// FEATURE MANAGEMENT
|
|
516
|
+
// ============================================================================
|
|
517
|
+
|
|
518
|
+
_createFeature(type, params, result) {
|
|
519
|
+
const featureId = `feat_${++this.featureCounter}`;
|
|
520
|
+
const feature = {
|
|
521
|
+
id: featureId,
|
|
522
|
+
type,
|
|
523
|
+
params,
|
|
524
|
+
result: result.uuid ? { meshId: result.uuid } : { meshId: null },
|
|
525
|
+
timestamp: Date.now(),
|
|
526
|
+
suppressed: false
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
this.features.push(feature);
|
|
530
|
+
this.currentShape = result;
|
|
531
|
+
|
|
532
|
+
this.kernel.emit('feature:created', { feature });
|
|
533
|
+
return { featureId, result };
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
editFeature(featureId, newParams) {
|
|
537
|
+
const feature = this.features.find(f => f.id === featureId);
|
|
538
|
+
if (!feature) throw new Error(`Feature not found: ${featureId}`);
|
|
539
|
+
|
|
540
|
+
const oldParams = feature.params;
|
|
541
|
+
feature.params = { ...feature.params, ...newParams };
|
|
542
|
+
|
|
543
|
+
this.kernel.emit('feature:edited', { featureId, oldParams, newParams });
|
|
544
|
+
this._rebuildAllFeatures();
|
|
545
|
+
|
|
546
|
+
return { featureId, updated: true };
|
|
547
|
+
},
|
|
548
|
+
|
|
549
|
+
suppressFeature(featureId) {
|
|
550
|
+
const feature = this.features.find(f => f.id === featureId);
|
|
551
|
+
if (!feature) throw new Error(`Feature not found: ${featureId}`);
|
|
552
|
+
|
|
553
|
+
feature.suppressed = !feature.suppressed;
|
|
554
|
+
this._rebuildAllFeatures();
|
|
555
|
+
|
|
556
|
+
return { featureId, suppressed: feature.suppressed };
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
reorderFeature(featureId, newIndex) {
|
|
560
|
+
const currentIndex = this.features.findIndex(f => f.id === featureId);
|
|
561
|
+
if (currentIndex === -1) throw new Error(`Feature not found: ${featureId}`);
|
|
562
|
+
|
|
563
|
+
const [feature] = this.features.splice(currentIndex, 1);
|
|
564
|
+
this.features.splice(newIndex, 0, feature);
|
|
565
|
+
|
|
566
|
+
this._rebuildAllFeatures();
|
|
567
|
+
return { featureId, newIndex };
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
deleteFeature(featureId) {
|
|
571
|
+
const index = this.features.findIndex(f => f.id === featureId);
|
|
572
|
+
if (index === -1) throw new Error(`Feature not found: ${featureId}`);
|
|
573
|
+
|
|
574
|
+
this.features.splice(index, 1);
|
|
575
|
+
this.kernel.emit('feature:deleted', { featureId });
|
|
576
|
+
|
|
577
|
+
this._rebuildAllFeatures();
|
|
578
|
+
return { featureId, deleted: true };
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
rebuild() {
|
|
582
|
+
this._rebuildAllFeatures();
|
|
583
|
+
return { rebuilt: true, featureCount: this.features.length };
|
|
584
|
+
},
|
|
585
|
+
|
|
586
|
+
_rebuildAllFeatures() {
|
|
587
|
+
this.currentShape = null;
|
|
588
|
+
const startTime = performance.now();
|
|
589
|
+
|
|
590
|
+
this.features.forEach(feature => {
|
|
591
|
+
if (!feature.suppressed) {
|
|
592
|
+
const method = this[feature.type];
|
|
593
|
+
if (method && typeof method === 'function') {
|
|
594
|
+
try {
|
|
595
|
+
const { result } = method.call(this, ...Object.values(feature.params));
|
|
596
|
+
feature.result = { meshId: result.uuid };
|
|
597
|
+
this.currentShape = result;
|
|
598
|
+
} catch (e) {
|
|
599
|
+
console.error(`Failed to rebuild feature ${feature.id}:`, e);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
const duration = performance.now() - startTime;
|
|
606
|
+
this.kernel.emit('rebuild:complete', { featureCount: this.features.length, duration });
|
|
607
|
+
},
|
|
608
|
+
|
|
609
|
+
getFeatureHistory() {
|
|
610
|
+
return this.features.map(f => ({
|
|
611
|
+
id: f.id,
|
|
612
|
+
type: f.type,
|
|
613
|
+
suppressed: f.suppressed,
|
|
614
|
+
timestamp: f.timestamp,
|
|
615
|
+
paramKeys: Object.keys(f.params)
|
|
616
|
+
}));
|
|
617
|
+
},
|
|
618
|
+
|
|
619
|
+
getCurrentShape() {
|
|
620
|
+
return this.currentShape;
|
|
621
|
+
},
|
|
622
|
+
|
|
623
|
+
// ============================================================================
|
|
624
|
+
// UTILITIES
|
|
625
|
+
// ============================================================================
|
|
626
|
+
|
|
627
|
+
_getMaterial(options = {}) {
|
|
628
|
+
const color = options.color || 0x4a90e2;
|
|
629
|
+
const material = new THREE.MeshPhongMaterial({
|
|
630
|
+
color,
|
|
631
|
+
emissive: 0x000000,
|
|
632
|
+
shininess: 100,
|
|
633
|
+
transparent: options.transparent || false,
|
|
634
|
+
opacity: options.opacity !== undefined ? options.opacity : 1.0
|
|
635
|
+
});
|
|
636
|
+
return material;
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
getUI() {
|
|
640
|
+
// Return feature tree UI panel
|
|
641
|
+
return `
|
|
642
|
+
<div id="operations-panel" class="panel">
|
|
643
|
+
<div class="panel-header">
|
|
644
|
+
<h3>Feature Tree</h3>
|
|
645
|
+
<button data-close-panel>×</button>
|
|
646
|
+
</div>
|
|
647
|
+
<div class="panel-content">
|
|
648
|
+
<div id="feature-tree" style="max-height: 500px; overflow-y: auto;">
|
|
649
|
+
<!-- Features populated here -->
|
|
650
|
+
</div>
|
|
651
|
+
<div style="margin-top: 10px; border-top: 1px solid #ccc; padding-top: 10px;">
|
|
652
|
+
<button onclick="window.cycleCAD.kernel.exec('ops.rebuild')">Rebuild All</button>
|
|
653
|
+
</div>
|
|
654
|
+
</div>
|
|
655
|
+
</div>
|
|
656
|
+
`;
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
export default OperationsModule;
|