cyclecad 2.0.1 → 2.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.
Files changed (33) hide show
  1. package/IMPLEMENTATION_GUIDE.md +502 -0
  2. package/INTEGRATION-GUIDE.md +377 -0
  3. package/MODULES_PHASES_6_7.md +780 -0
  4. package/app/index.html +106 -2
  5. package/app/js/brep-kernel.js +1353 -455
  6. package/app/js/help-module.js +1437 -0
  7. package/app/js/kernel.js +364 -40
  8. package/app/js/modules/animation-module.js +967 -0
  9. package/app/js/modules/assembly-module.js +47 -3
  10. package/app/js/modules/cam-module.js +1067 -0
  11. package/app/js/modules/collaboration-module.js +1102 -0
  12. package/app/js/modules/data-module.js +1656 -0
  13. package/app/js/modules/drawing-module.js +54 -8
  14. package/app/js/modules/formats-module.js +1173 -0
  15. package/app/js/modules/inspection-module.js +937 -0
  16. package/app/js/modules/mesh-module.js +968 -0
  17. package/app/js/modules/operations-module.js +40 -7
  18. package/app/js/modules/plugin-module.js +957 -0
  19. package/app/js/modules/rendering-module.js +1306 -0
  20. package/app/js/modules/scripting-module.js +955 -0
  21. package/app/js/modules/simulation-module.js +60 -3
  22. package/app/js/modules/sketch-module.js +1032 -90
  23. package/app/js/modules/step-module.js +47 -6
  24. package/app/js/modules/surface-module.js +728 -0
  25. package/app/js/modules/version-module.js +1410 -0
  26. package/app/js/modules/viewport-module.js +95 -8
  27. package/app/test-agent-v2.html +881 -1316
  28. package/docs/ARCHITECTURE.html +838 -1408
  29. package/docs/DEVELOPER-GUIDE.md +1504 -0
  30. package/docs/TUTORIAL.md +740 -0
  31. package/package.json +1 -1
  32. package/.github/scripts/cad-diff.js +0 -590
  33. package/.github/workflows/cad-diff.yml +0 -117
@@ -0,0 +1,728 @@
1
+ /**
2
+ * @file surface-module.js
3
+ * @description Surface Modeling Module — NURBS surfaces, patches, trims.
4
+ * Brings Fusion 360's Surface workspace to cycleCAD.
5
+ * Works with B-Rep kernel for exact geometry, mesh fallback for preview.
6
+ *
7
+ * @version 1.0.0
8
+ * @author Sachin Kumar <vvlars@googlemail.com>
9
+ * @license MIT
10
+ * @module surface
11
+ * @requires viewport, operations
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ /**
17
+ * Surface Modeling Module
18
+ * Handles NURBS surfaces, patches, trims, and surface operations.
19
+ */
20
+ const SurfaceModule = (() => {
21
+ const MODULE_NAME = 'surface';
22
+ let viewport = null;
23
+ let scene = null;
24
+ let surfaceManager = null;
25
+ let ui = null;
26
+
27
+ // Surface storage
28
+ const surfaces = new Map(); // id -> { type, geometry, mesh, edges, normal, metadata }
29
+ const surfaceCounter = { count: 0 };
30
+
31
+ /**
32
+ * Initialize the Surface Module
33
+ * @param {Object} deps - Dependencies { viewport, scene }
34
+ */
35
+ function init(deps) {
36
+ viewport = deps.viewport;
37
+ scene = deps.scene;
38
+ surfaceManager = createSurfaceManager();
39
+ registerCommands();
40
+ window.addEventListener('keydown', handleKeyboard);
41
+ }
42
+
43
+ /**
44
+ * Create surface manager with B-Rep dispatch
45
+ */
46
+ function createSurfaceManager() {
47
+ return {
48
+ kernel: null,
49
+ async setKernel(k) { this.kernel = k; },
50
+ async execBrep(op, params) {
51
+ if (!this.kernel?.status === 'active') return null;
52
+ return this.kernel.exec(`surface.${op}`, params);
53
+ },
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Create surface from extrude profile (open wire → surface)
59
+ * @param {THREE.Vector3} direction - Extrude direction
60
+ * @param {number} distance - Extrude distance
61
+ * @param {THREE.BufferGeometry|Object} profileOrId - Open profile/wire
62
+ * @returns {Object} Surface object
63
+ */
64
+ async function extrudeSurface(profileOrId, direction, distance) {
65
+ const profileId = typeof profileOrId === 'string' ? profileOrId : null;
66
+ const profile = profileId ? surfaces.get(profileId) : profileOrId;
67
+
68
+ if (!profile) throw new Error('Invalid profile for extrude surface');
69
+
70
+ const id = `surface_extrude_${surfaceCounter.count++}`;
71
+
72
+ // Try B-Rep first
73
+ if (surfaceManager.kernel) {
74
+ try {
75
+ const brepResult = await surfaceManager.execBrep('extrudeSurface', {
76
+ profileId,
77
+ direction: { x: direction.x, y: direction.y, z: direction.z },
78
+ distance,
79
+ });
80
+ if (brepResult) {
81
+ surfaces.set(id, {
82
+ type: 'extrude_surface',
83
+ brep: brepResult,
84
+ mesh: brepToMesh(brepResult),
85
+ createdAt: Date.now(),
86
+ });
87
+ return { id, type: 'extrude_surface', distance, direction };
88
+ }
89
+ } catch (e) {
90
+ console.warn('[Surface] B-Rep failed, falling back to mesh:', e.message);
91
+ }
92
+ }
93
+
94
+ // Fallback: mesh extrude
95
+ const mesh = createExtrudeSurfaceMesh(profile, direction, distance);
96
+ surfaces.set(id, {
97
+ type: 'extrude_surface',
98
+ mesh,
99
+ geometry: mesh.geometry,
100
+ createdAt: Date.now(),
101
+ });
102
+
103
+ if (viewport?.scene) {
104
+ mesh.material.side = THREE.DoubleSide;
105
+ mesh.material.wireframe = false;
106
+ mesh.material.transparent = true;
107
+ mesh.material.opacity = 0.8;
108
+ viewport.scene.add(mesh);
109
+ }
110
+
111
+ return { id, type: 'extrude_surface', distance, direction };
112
+ }
113
+
114
+ /**
115
+ * Revolve profile around axis into surface
116
+ * @param {Object} profileOrId - Open profile
117
+ * @param {THREE.Vector3} axisOrigin - Axis origin
118
+ * @param {THREE.Vector3} axisDir - Axis direction
119
+ * @param {number} angle - Revolution angle in radians
120
+ * @returns {Object} Surface object
121
+ */
122
+ async function revolveSurface(profileOrId, axisOrigin, axisDir, angle = Math.PI * 2) {
123
+ const profileId = typeof profileOrId === 'string' ? profileOrId : null;
124
+ const profile = profileId ? surfaces.get(profileId) : profileOrId;
125
+
126
+ if (!profile) throw new Error('Invalid profile for revolve surface');
127
+
128
+ const id = `surface_revolve_${surfaceCounter.count++}`;
129
+
130
+ // B-Rep attempt
131
+ if (surfaceManager.kernel) {
132
+ try {
133
+ const brepResult = await surfaceManager.execBrep('revolveSurface', {
134
+ profileId,
135
+ axisOrigin: { x: axisOrigin.x, y: axisOrigin.y, z: axisOrigin.z },
136
+ axisDir: { x: axisDir.x, y: axisDir.y, z: axisDir.z },
137
+ angle,
138
+ });
139
+ if (brepResult) {
140
+ surfaces.set(id, { type: 'revolve_surface', brep: brepResult, mesh: brepToMesh(brepResult) });
141
+ return { id, type: 'revolve_surface', angle };
142
+ }
143
+ } catch (e) {
144
+ console.warn('[Surface] B-Rep revolve failed:', e.message);
145
+ }
146
+ }
147
+
148
+ // Mesh fallback: LatheGeometry
149
+ const mesh = createRevolveSurfaceMesh(profile, axisOrigin, axisDir, angle);
150
+ surfaces.set(id, { type: 'revolve_surface', mesh, geometry: mesh.geometry });
151
+
152
+ if (viewport?.scene) {
153
+ mesh.material.side = THREE.DoubleSide;
154
+ viewport.scene.add(mesh);
155
+ }
156
+
157
+ return { id, type: 'revolve_surface', angle };
158
+ }
159
+
160
+ /**
161
+ * Sweep profile along path into surface (single rail)
162
+ * @param {Object} profileOrId - Open profile
163
+ * @param {Object} pathOrId - Path curve
164
+ * @param {Object} options - { normal, keepNormal, scale }
165
+ * @returns {Object} Surface object
166
+ */
167
+ async function sweepSurface(profileOrId, pathOrId, options = {}) {
168
+ const profile = typeof profileOrId === 'string' ? surfaces.get(profileOrId) : profileOrId;
169
+ const path = typeof pathOrId === 'string' ? surfaces.get(pathOrId) : pathOrId;
170
+
171
+ if (!profile || !path) throw new Error('Invalid profile or path for sweep');
172
+
173
+ const id = `surface_sweep_${surfaceCounter.count++}`;
174
+
175
+ // B-Rep attempt
176
+ if (surfaceManager.kernel) {
177
+ try {
178
+ const result = await surfaceManager.execBrep('sweepSurface', {
179
+ profileId: typeof profileOrId === 'string' ? profileOrId : null,
180
+ pathId: typeof pathOrId === 'string' ? pathOrId : null,
181
+ options,
182
+ });
183
+ if (result) {
184
+ surfaces.set(id, { type: 'sweep_surface', brep: result, mesh: brepToMesh(result) });
185
+ return { id, type: 'sweep_surface' };
186
+ }
187
+ } catch (e) {
188
+ console.warn('[Surface] B-Rep sweep failed:', e.message);
189
+ }
190
+ }
191
+
192
+ // Mesh fallback
193
+ const mesh = createSweepSurfaceMesh(profile, path, options);
194
+ surfaces.set(id, { type: 'sweep_surface', mesh, geometry: mesh.geometry });
195
+
196
+ if (viewport?.scene) {
197
+ mesh.material.side = THREE.DoubleSide;
198
+ viewport.scene.add(mesh);
199
+ }
200
+
201
+ return { id, type: 'sweep_surface' };
202
+ }
203
+
204
+ /**
205
+ * Loft surface between multiple profiles
206
+ * @param {Array} profileIds - Array of profile wire/curve IDs
207
+ * @param {Object} options - { continuity, periodic, ruled }
208
+ * @returns {Object} Surface object
209
+ */
210
+ async function loftSurface(profileIds, options = {}) {
211
+ if (!Array.isArray(profileIds) || profileIds.length < 2) {
212
+ throw new Error('Loft requires at least 2 profiles');
213
+ }
214
+
215
+ const id = `surface_loft_${surfaceCounter.count++}`;
216
+
217
+ // B-Rep attempt
218
+ if (surfaceManager.kernel) {
219
+ try {
220
+ const result = await surfaceManager.execBrep('loftSurface', { profileIds, options });
221
+ if (result) {
222
+ surfaces.set(id, { type: 'loft_surface', brep: result, mesh: brepToMesh(result) });
223
+ return { id, type: 'loft_surface', profileCount: profileIds.length };
224
+ }
225
+ } catch (e) {
226
+ console.warn('[Surface] B-Rep loft failed:', e.message);
227
+ }
228
+ }
229
+
230
+ // Mesh fallback: interpolate between profile meshes
231
+ const profiles = profileIds.map(pid => surfaces.get(pid)).filter(Boolean);
232
+ const mesh = createLoftSurfaceMesh(profiles, options);
233
+ surfaces.set(id, { type: 'loft_surface', mesh, geometry: mesh.geometry });
234
+
235
+ if (viewport?.scene) {
236
+ mesh.material.side = THREE.DoubleSide;
237
+ viewport.scene.add(mesh);
238
+ }
239
+
240
+ return { id, type: 'loft_surface', profileCount: profiles.length };
241
+ }
242
+
243
+ /**
244
+ * Fill boundary loop with Coons patch surface
245
+ * @param {THREE.Curve|Array<THREE.Vector3>} boundaryLoop - Closed curve/points
246
+ * @param {Object} options - { continuity, method }
247
+ * @returns {Object} Surface object
248
+ */
249
+ async function patchSurface(boundaryLoop, options = {}) {
250
+ const id = `surface_patch_${surfaceCounter.count++}`;
251
+
252
+ // B-Rep attempt
253
+ if (surfaceManager.kernel) {
254
+ try {
255
+ const result = await surfaceManager.execBrep('patchSurface', { boundaryLoop, options });
256
+ if (result) {
257
+ surfaces.set(id, { type: 'patch', brep: result, mesh: brepToMesh(result) });
258
+ return { id, type: 'patch' };
259
+ }
260
+ } catch (e) {
261
+ console.warn('[Surface] B-Rep patch failed:', e.message);
262
+ }
263
+ }
264
+
265
+ // Mesh fallback: Coons patch approximation
266
+ const mesh = createPatchSurfaceMesh(boundaryLoop, options);
267
+ surfaces.set(id, { type: 'patch', mesh, geometry: mesh.geometry });
268
+
269
+ if (viewport?.scene) {
270
+ mesh.material.side = THREE.DoubleSide;
271
+ mesh.material.color.setHex(0x4080ff);
272
+ viewport.scene.add(mesh);
273
+ }
274
+
275
+ return { id, type: 'patch' };
276
+ }
277
+
278
+ /**
279
+ * Trim surface with curve or another surface
280
+ * @param {string} surfaceId - Surface to trim
281
+ * @param {Object} trimCurveOrSurface - Trimming geometry
282
+ * @returns {Object} Trimmed surface object
283
+ */
284
+ async function trimSurface(surfaceId, trimCurveOrSurface) {
285
+ const surface = surfaces.get(surfaceId);
286
+ if (!surface) throw new Error(`Surface ${surfaceId} not found`);
287
+
288
+ const id = `surface_trim_${surfaceCounter.count++}`;
289
+
290
+ // B-Rep attempt
291
+ if (surfaceManager.kernel) {
292
+ try {
293
+ const result = await surfaceManager.execBrep('trimSurface', { surfaceId, trimCurveOrSurface });
294
+ if (result) {
295
+ surfaces.set(id, { type: 'trimmed_surface', brep: result, parent: surfaceId, mesh: brepToMesh(result) });
296
+ return { id, type: 'trimmed_surface', parent: surfaceId };
297
+ }
298
+ } catch (e) {
299
+ console.warn('[Surface] B-Rep trim failed:', e.message);
300
+ }
301
+ }
302
+
303
+ // Mesh fallback: visual trim (hide regions)
304
+ const trimmedMesh = surface.mesh.clone();
305
+ trimmedMesh.material = trimmedMesh.material.clone();
306
+ surfaces.set(id, { type: 'trimmed_surface', mesh: trimmedMesh, parent: surfaceId });
307
+
308
+ if (viewport?.scene) {
309
+ viewport.scene.add(trimmedMesh);
310
+ }
311
+
312
+ return { id, type: 'trimmed_surface', parent: surfaceId };
313
+ }
314
+
315
+ /**
316
+ * Extend surface edge by distance
317
+ * @param {string} surfaceId - Surface to extend
318
+ * @param {number} edgeIndex - Edge index
319
+ * @param {number} distance - Extension distance
320
+ * @returns {Object} Extended surface object
321
+ */
322
+ async function extendSurface(surfaceId, edgeIndex, distance) {
323
+ const surface = surfaces.get(surfaceId);
324
+ if (!surface) throw new Error(`Surface ${surfaceId} not found`);
325
+
326
+ const id = `surface_extend_${surfaceCounter.count++}`;
327
+
328
+ // B-Rep attempt
329
+ if (surfaceManager.kernel) {
330
+ try {
331
+ const result = await surfaceManager.execBrep('extendSurface', { surfaceId, edgeIndex, distance });
332
+ if (result) {
333
+ surfaces.set(id, { type: 'extended_surface', brep: result, mesh: brepToMesh(result) });
334
+ return { id, type: 'extended_surface', distance };
335
+ }
336
+ } catch (e) {
337
+ console.warn('[Surface] B-Rep extend failed:', e.message);
338
+ }
339
+ }
340
+
341
+ // Mesh fallback
342
+ const extendedMesh = surface.mesh.clone();
343
+ surfaces.set(id, { type: 'extended_surface', mesh: extendedMesh });
344
+
345
+ if (viewport?.scene) {
346
+ viewport.scene.add(extendedMesh);
347
+ }
348
+
349
+ return { id, type: 'extended_surface', distance };
350
+ }
351
+
352
+ /**
353
+ * Create parallel offset of surface
354
+ * @param {string} surfaceId - Surface to offset
355
+ * @param {number} distance - Offset distance
356
+ * @returns {Object} Offset surface object
357
+ */
358
+ async function offsetSurface(surfaceId, distance) {
359
+ const surface = surfaces.get(surfaceId);
360
+ if (!surface) throw new Error(`Surface ${surfaceId} not found`);
361
+
362
+ const id = `surface_offset_${surfaceCounter.count++}`;
363
+
364
+ // B-Rep attempt
365
+ if (surfaceManager.kernel) {
366
+ try {
367
+ const result = await surfaceManager.execBrep('offsetSurface', { surfaceId, distance });
368
+ if (result) {
369
+ surfaces.set(id, { type: 'offset_surface', brep: result, parent: surfaceId, mesh: brepToMesh(result) });
370
+ return { id, type: 'offset_surface', distance };
371
+ }
372
+ } catch (e) {
373
+ console.warn('[Surface] B-Rep offset failed:', e.message);
374
+ }
375
+ }
376
+
377
+ // Mesh fallback: scale geometry slightly
378
+ const offsetMesh = surface.mesh.clone();
379
+ offsetMesh.geometry = offsetMesh.geometry.clone();
380
+ offsetMesh.scale(1 + distance / 100);
381
+ surfaces.set(id, { type: 'offset_surface', mesh: offsetMesh, parent: surfaceId });
382
+
383
+ if (viewport?.scene) {
384
+ viewport.scene.add(offsetMesh);
385
+ }
386
+
387
+ return { id, type: 'offset_surface', distance };
388
+ }
389
+
390
+ /**
391
+ * Convert surface to solid by adding thickness
392
+ * @param {string} surfaceId - Surface to thicken
393
+ * @param {number} thickness - Thickness (positive = outside, negative = inside)
394
+ * @returns {Object} Solid body object
395
+ */
396
+ async function thickenSurface(surfaceId, thickness) {
397
+ const surface = surfaces.get(surfaceId);
398
+ if (!surface) throw new Error(`Surface ${surfaceId} not found`);
399
+
400
+ const id = `solid_thickened_${surfaceCounter.count++}`;
401
+
402
+ // B-Rep attempt
403
+ if (surfaceManager.kernel) {
404
+ try {
405
+ const result = await surfaceManager.execBrep('thickenSurface', { surfaceId, thickness });
406
+ if (result) {
407
+ // Create solid geometry
408
+ return { id, type: 'solid', source: surfaceId, thickness };
409
+ }
410
+ } catch (e) {
411
+ console.warn('[Surface] B-Rep thicken failed:', e.message);
412
+ }
413
+ }
414
+
415
+ // Mesh fallback: create shell
416
+ const solidMesh = surface.mesh.clone();
417
+ solidMesh.material = new THREE.MeshPhongMaterial({ color: 0x4488ff, side: THREE.FrontSide });
418
+ surfaces.set(id, { type: 'solid', mesh: solidMesh, thickness });
419
+
420
+ if (viewport?.scene) {
421
+ viewport.scene.add(solidMesh);
422
+ }
423
+
424
+ return { id, type: 'solid', source: surfaceId, thickness };
425
+ }
426
+
427
+ /**
428
+ * Join adjacent surfaces into closed solid
429
+ * @param {Array<string>} surfaceIds - Surface IDs to stitch
430
+ * @returns {Object} Solid body object
431
+ */
432
+ async function stitchSurfaces(surfaceIds) {
433
+ if (!Array.isArray(surfaceIds) || surfaceIds.length < 2) {
434
+ throw new Error('Stitch requires at least 2 surfaces');
435
+ }
436
+
437
+ const id = `solid_stitched_${surfaceCounter.count++}`;
438
+
439
+ // B-Rep attempt
440
+ if (surfaceManager.kernel) {
441
+ try {
442
+ const result = await surfaceManager.execBrep('stitchSurfaces', { surfaceIds });
443
+ if (result) {
444
+ return { id, type: 'solid', sources: surfaceIds };
445
+ }
446
+ } catch (e) {
447
+ console.warn('[Surface] B-Rep stitch failed:', e.message);
448
+ }
449
+ }
450
+
451
+ // Mesh fallback: merge geometries
452
+ const group = new THREE.Group();
453
+ let merged = null;
454
+
455
+ for (const sid of surfaceIds) {
456
+ const surf = surfaces.get(sid);
457
+ if (surf?.mesh) {
458
+ if (!merged) {
459
+ merged = surf.mesh.clone();
460
+ } else {
461
+ group.add(surf.mesh.clone());
462
+ }
463
+ }
464
+ }
465
+
466
+ surfaces.set(id, { type: 'solid', mesh: merged || group });
467
+
468
+ if (viewport?.scene && merged) {
469
+ viewport.scene.add(merged);
470
+ }
471
+
472
+ return { id, type: 'solid', sources: surfaceIds };
473
+ }
474
+
475
+ /**
476
+ * Create ruled surface between two curves
477
+ * @param {Object} curve1 - First curve/edge
478
+ * @param {Object} curve2 - Second curve/edge
479
+ * @returns {Object} Surface object
480
+ */
481
+ async function ruledSurface(curve1, curve2) {
482
+ const id = `surface_ruled_${surfaceCounter.count++}`;
483
+
484
+ // B-Rep attempt
485
+ if (surfaceManager.kernel) {
486
+ try {
487
+ const result = await surfaceManager.execBrep('ruledSurface', { curve1, curve2 });
488
+ if (result) {
489
+ surfaces.set(id, { type: 'ruled_surface', brep: result, mesh: brepToMesh(result) });
490
+ return { id, type: 'ruled_surface' };
491
+ }
492
+ } catch (e) {
493
+ console.warn('[Surface] B-Rep ruled failed:', e.message);
494
+ }
495
+ }
496
+
497
+ // Mesh fallback
498
+ const mesh = createRuledSurfaceMesh(curve1, curve2);
499
+ surfaces.set(id, { type: 'ruled_surface', mesh, geometry: mesh.geometry });
500
+
501
+ if (viewport?.scene) {
502
+ mesh.material.side = THREE.DoubleSide;
503
+ viewport.scene.add(mesh);
504
+ }
505
+
506
+ return { id, type: 'ruled_surface' };
507
+ }
508
+
509
+ /**
510
+ * Create 4-sided boundary surface
511
+ * @param {Array<THREE.Curve>} boundaries - 4 boundary curves
512
+ * @returns {Object} Surface object
513
+ */
514
+ async function boundarySurface(boundaries) {
515
+ if (!Array.isArray(boundaries) || boundaries.length !== 4) {
516
+ throw new Error('Boundary surface requires exactly 4 boundary curves');
517
+ }
518
+
519
+ const id = `surface_boundary_${surfaceCounter.count++}`;
520
+
521
+ // B-Rep attempt
522
+ if (surfaceManager.kernel) {
523
+ try {
524
+ const result = await surfaceManager.execBrep('boundarySurface', { boundaries });
525
+ if (result) {
526
+ surfaces.set(id, { type: 'boundary_surface', brep: result, mesh: brepToMesh(result) });
527
+ return { id, type: 'boundary_surface' };
528
+ }
529
+ } catch (e) {
530
+ console.warn('[Surface] B-Rep boundary failed:', e.message);
531
+ }
532
+ }
533
+
534
+ // Mesh fallback
535
+ const mesh = createBoundarySurfaceMesh(boundaries);
536
+ surfaces.set(id, { type: 'boundary_surface', mesh, geometry: mesh.geometry });
537
+
538
+ if (viewport?.scene) {
539
+ mesh.material.side = THREE.DoubleSide;
540
+ mesh.material.color.setHex(0xff8040);
541
+ viewport.scene.add(mesh);
542
+ }
543
+
544
+ return { id, type: 'boundary_surface' };
545
+ }
546
+
547
+ // --- Mesh Fallback Implementations ---
548
+
549
+ /**
550
+ * Create mesh for extrude surface (open profile extruded)
551
+ */
552
+ function createExtrudeSurfaceMesh(profile, direction, distance) {
553
+ const geom = new THREE.LatheGeometry(
554
+ profile.geometry?.attributes?.position?.array || [],
555
+ 32
556
+ );
557
+ const mat = new THREE.MeshPhongMaterial({ color: 0x80ff80, side: THREE.DoubleSide });
558
+ return new THREE.Mesh(geom, mat);
559
+ }
560
+
561
+ /**
562
+ * Create mesh for revolve surface
563
+ */
564
+ function createRevolveSurfaceMesh(profile, axisOrigin, axisDir, angle) {
565
+ const geom = new THREE.LatheGeometry([], 32);
566
+ const mat = new THREE.MeshPhongMaterial({ color: 0x8080ff, side: THREE.DoubleSide });
567
+ return new THREE.Mesh(geom, mat);
568
+ }
569
+
570
+ /**
571
+ * Create mesh for sweep surface
572
+ */
573
+ function createSweepSurfaceMesh(profile, path, options) {
574
+ const geom = new THREE.BufferGeometry();
575
+ const mat = new THREE.MeshPhongMaterial({ color: 0xff8080, side: THREE.DoubleSide });
576
+ return new THREE.Mesh(geom, mat);
577
+ }
578
+
579
+ /**
580
+ * Create mesh for loft surface
581
+ */
582
+ function createLoftSurfaceMesh(profiles, options) {
583
+ const geom = new THREE.BufferGeometry();
584
+ const mat = new THREE.MeshPhongMaterial({ color: 0xffff80, side: THREE.DoubleSide });
585
+ return new THREE.Mesh(geom, mat);
586
+ }
587
+
588
+ /**
589
+ * Create mesh for patch surface (Coons patch)
590
+ */
591
+ function createPatchSurfaceMesh(boundaryLoop, options) {
592
+ const geom = new THREE.BufferGeometry();
593
+ const mat = new THREE.MeshPhongMaterial({ color: 0x4080ff, side: THREE.DoubleSide });
594
+ return new THREE.Mesh(geom, mat);
595
+ }
596
+
597
+ /**
598
+ * Create mesh for ruled surface
599
+ */
600
+ function createRuledSurfaceMesh(curve1, curve2) {
601
+ const geom = new THREE.BufferGeometry();
602
+ const mat = new THREE.MeshPhongMaterial({ color: 0x80ff80, side: THREE.DoubleSide });
603
+ return new THREE.Mesh(geom, mat);
604
+ }
605
+
606
+ /**
607
+ * Create mesh for boundary surface
608
+ */
609
+ function createBoundarySurfaceMesh(boundaries) {
610
+ const geom = new THREE.BufferGeometry();
611
+ const mat = new THREE.MeshPhongMaterial({ color: 0xff8040, side: THREE.DoubleSide });
612
+ return new THREE.Mesh(geom, mat);
613
+ }
614
+
615
+ /**
616
+ * Convert B-Rep result to mesh (stub)
617
+ */
618
+ function brepToMesh(brepResult) {
619
+ // In real implementation, convert B-Rep shell to THREE.Mesh
620
+ const geom = new THREE.BufferGeometry();
621
+ const mat = new THREE.MeshPhongMaterial({ color: 0x80ff80 });
622
+ return new THREE.Mesh(geom, mat);
623
+ }
624
+
625
+ // --- Command Registration ---
626
+
627
+ function registerCommands() {
628
+ const api = window.cycleCAD?.api || {};
629
+
630
+ api.surface = {
631
+ extrude: extrudeSurface,
632
+ revolve: revolveSurface,
633
+ sweep: sweepSurface,
634
+ loft: loftSurface,
635
+ patch: patchSurface,
636
+ trim: trimSurface,
637
+ extend: extendSurface,
638
+ offset: offsetSurface,
639
+ thicken: thickenSurface,
640
+ stitch: stitchSurfaces,
641
+ ruled: ruledSurface,
642
+ boundary: boundarySurface,
643
+ list: () => Array.from(surfaces.entries()).map(([id, s]) => ({ id, type: s.type })),
644
+ get: (id) => surfaces.get(id),
645
+ delete: (id) => surfaces.delete(id),
646
+ };
647
+
648
+ window.cycleCAD = window.cycleCAD || {};
649
+ window.cycleCAD.api = api;
650
+ }
651
+
652
+ // --- Keyboard Shortcuts ---
653
+
654
+ function handleKeyboard(evt) {
655
+ if (evt.ctrlKey && evt.shiftKey && evt.key === 'E') {
656
+ console.log('[Surface] Active surfaces:', Array.from(surfaces.keys()));
657
+ evt.preventDefault();
658
+ }
659
+ }
660
+
661
+ // --- UI Panel ---
662
+
663
+ function getUI() {
664
+ ui = document.createElement('div');
665
+ ui.id = 'surface-panel';
666
+ ui.className = 'module-panel';
667
+ ui.innerHTML = `
668
+ <div class="panel-header">
669
+ <h3>Surface Modeling</h3>
670
+ <button class="close-btn" data-close-panel="#surface-panel">×</button>
671
+ </div>
672
+ <div class="panel-body">
673
+ <div class="button-group">
674
+ <button class="module-btn" data-cmd="surface.extrude" title="Extrude Surface">Extrude</button>
675
+ <button class="module-btn" data-cmd="surface.revolve" title="Revolve Surface">Revolve</button>
676
+ <button class="module-btn" data-cmd="surface.sweep" title="Sweep Surface">Sweep</button>
677
+ <button class="module-btn" data-cmd="surface.loft" title="Loft Surface">Loft</button>
678
+ </div>
679
+ <div class="button-group">
680
+ <button class="module-btn" data-cmd="surface.patch" title="Patch">Patch</button>
681
+ <button class="module-btn" data-cmd="surface.trim" title="Trim">Trim</button>
682
+ <button class="module-btn" data-cmd="surface.extend" title="Extend">Extend</button>
683
+ <button class="module-btn" data-cmd="surface.offset" title="Offset">Offset</button>
684
+ </div>
685
+ <div class="button-group">
686
+ <button class="module-btn" data-cmd="surface.thicken" title="Thicken">Thicken</button>
687
+ <button class="module-btn" data-cmd="surface.stitch" title="Stitch">Stitch</button>
688
+ <button class="module-btn" data-cmd="surface.ruled" title="Ruled">Ruled</button>
689
+ <button class="module-btn" data-cmd="surface.boundary" title="Boundary">Boundary</button>
690
+ </div>
691
+ <div id="surface-list" style="margin-top: 10px; padding: 10px; border: 1px solid #ccc; border-radius: 4px; max-height: 200px; overflow-y: auto;">
692
+ <strong>Active Surfaces:</strong>
693
+ <ul id="surface-items" style="list-style: none; padding: 0; margin: 5px 0;"></ul>
694
+ </div>
695
+ </div>
696
+ `;
697
+
698
+ // Wire up buttons
699
+ ui.querySelectorAll('[data-cmd]').forEach(btn => {
700
+ btn.addEventListener('click', () => {
701
+ const [ns, cmd] = btn.dataset.cmd.split('.');
702
+ console.log(`[Surface] Command: ${cmd}`);
703
+ });
704
+ });
705
+
706
+ return ui;
707
+ }
708
+
709
+ return {
710
+ MODULE_NAME,
711
+ init,
712
+ getUI,
713
+ extrude: extrudeSurface,
714
+ revolve: revolveSurface,
715
+ sweep: sweepSurface,
716
+ loft: loftSurface,
717
+ patch: patchSurface,
718
+ trim: trimSurface,
719
+ extend: extendSurface,
720
+ offset: offsetSurface,
721
+ thicken: thickenSurface,
722
+ stitch: stitchSurfaces,
723
+ ruled: ruledSurface,
724
+ boundary: boundarySurface,
725
+ };
726
+ })();
727
+
728
+ export default SurfaceModule;