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.
@@ -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;