cyclecad 2.0.1 → 3.0.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 (48) hide show
  1. package/DELIVERABLES.txt +296 -445
  2. package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
  3. package/ENHANCEMENT_SUMMARY.txt +308 -0
  4. package/FEATURE_INVENTORY.md +235 -0
  5. package/FUSION360_FEATURES_SUMMARY.md +452 -0
  6. package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
  7. package/FUSION360_PARITY_SUMMARY.md +520 -0
  8. package/FUSION360_QUICK_REFERENCE.md +351 -0
  9. package/IMPLEMENTATION_GUIDE.md +502 -0
  10. package/INTEGRATION-GUIDE.md +377 -0
  11. package/MODULES_PHASES_6_7.md +780 -0
  12. package/MODULE_API_REFERENCE.md +712 -0
  13. package/MODULE_INVENTORY.txt +264 -0
  14. package/app/index.html +1345 -4930
  15. package/app/js/app.js +1312 -514
  16. package/app/js/brep-kernel.js +1353 -455
  17. package/app/js/help-module.js +1437 -0
  18. package/app/js/kernel.js +364 -40
  19. package/app/js/modules/animation-module.js +1461 -0
  20. package/app/js/modules/assembly-module.js +47 -3
  21. package/app/js/modules/cam-module.js +1572 -0
  22. package/app/js/modules/collaboration-module.js +1615 -0
  23. package/app/js/modules/constraint-module.js +1266 -0
  24. package/app/js/modules/data-module.js +1054 -0
  25. package/app/js/modules/drawing-module.js +54 -8
  26. package/app/js/modules/formats-module.js +873 -0
  27. package/app/js/modules/inspection-module.js +1330 -0
  28. package/app/js/modules/mesh-module-enhanced.js +880 -0
  29. package/app/js/modules/mesh-module.js +968 -0
  30. package/app/js/modules/operations-module.js +40 -7
  31. package/app/js/modules/plugin-module.js +1554 -0
  32. package/app/js/modules/rendering-module.js +1766 -0
  33. package/app/js/modules/scripting-module.js +1073 -0
  34. package/app/js/modules/simulation-module.js +60 -3
  35. package/app/js/modules/sketch-module.js +2029 -91
  36. package/app/js/modules/step-module.js +47 -6
  37. package/app/js/modules/surface-module.js +1040 -0
  38. package/app/js/modules/version-module.js +1830 -0
  39. package/app/js/modules/viewport-module.js +95 -8
  40. package/app/test-agent-v2.html +881 -1316
  41. package/cycleCAD-Architecture-v2.pptx +0 -0
  42. package/docs/ARCHITECTURE.html +838 -1408
  43. package/docs/DEVELOPER-GUIDE.md +1504 -0
  44. package/docs/TUTORIAL.md +740 -0
  45. package/package.json +1 -1
  46. package/~$cycleCAD-Architecture-v2.pptx +0 -0
  47. package/.github/scripts/cad-diff.js +0 -590
  48. package/.github/workflows/cad-diff.yml +0 -117
@@ -0,0 +1,1040 @@
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
+ /**
710
+ * Freeform T-spline sculpting - push/pull vertices in real-time
711
+ */
712
+ async function sculptTSpline(surfaceId, options = {}) {
713
+ const { mode = 'push', radius = 10, strength = 1.0 } = options;
714
+ const surface = surfaces.get(surfaceId);
715
+ if (!surface) throw new Error(`Surface ${surfaceId} not found`);
716
+
717
+ const id = `surface_sculpt_${surfaceCounter.count++}`;
718
+ surfaces.set(id, { type: 'sculpted_surface', parent: surfaceId, mesh: surface.mesh?.clone(), mode, createdAt: Date.now() });
719
+ return { id, type: 'sculpted_surface', mode, radius, strength };
720
+ }
721
+
722
+ /**
723
+ * Surface extension - extend edge naturally, linearly, or circularly
724
+ */
725
+ async function extendSurfaceAdvanced(surfaceId, edgeIndex, distance, extensionType = 'natural') {
726
+ const surface = surfaces.get(surfaceId);
727
+ if (!surface) throw new Error(`Surface ${surfaceId} not found`);
728
+
729
+ const id = `surface_extend_${extensionType}_${surfaceCounter.count++}`;
730
+
731
+ if (surfaceManager.kernel) {
732
+ try {
733
+ const result = await surfaceManager.execBrep('extendSurfaceAdvanced', { surfaceId, edgeIndex, distance, extensionType });
734
+ if (result) {
735
+ surfaces.set(id, { type: 'extended_surface', brep: result, parent: surfaceId, extensionType, mesh: brepToMesh(result) });
736
+ return { id, type: 'extended_surface', extensionType, distance };
737
+ }
738
+ } catch (e) {
739
+ console.warn('[Surface] B-Rep extend advanced failed:', e.message);
740
+ }
741
+ }
742
+
743
+ const extendedMesh = surface.mesh?.clone();
744
+ surfaces.set(id, { type: 'extended_surface', mesh: extendedMesh, extensionType });
745
+ return { id, type: 'extended_surface', extensionType, distance };
746
+ }
747
+
748
+ /**
749
+ * Curvature analysis with color mapping - Gaussian, mean, or principal
750
+ */
751
+ async function analyzeCurvature(surfaceId, options = {}) {
752
+ const { type = 'mean', colorMap = 'heatmap', apply = true } = options;
753
+ const surface = surfaces.get(surfaceId);
754
+ if (!surface) throw new Error(`Surface ${surfaceId} not found`);
755
+
756
+ const mesh = surface.mesh;
757
+ if (!mesh || !mesh.geometry) return { surfaceId, type, colorMap, analysis: 'No geometry' };
758
+
759
+ const geometry = mesh.geometry;
760
+ const normals = geometry.attributes.normal;
761
+ const positions = geometry.attributes.position;
762
+
763
+ if (!normals || !positions) return { surfaceId, analysis: 'Missing normals' };
764
+
765
+ // Compute curvature per vertex
766
+ const curvatures = new Float32Array(positions.count);
767
+ const colors = new Uint8Array(positions.count * 3);
768
+
769
+ for (let i = 0; i < positions.count; i++) {
770
+ const n = new THREE.Vector3().fromBufferAttribute(normals, i);
771
+ let curvature = Math.abs(n.x + n.y + n.z) / 3; // Simplified
772
+ curvatures[i] = curvature;
773
+
774
+ const hue = (1 - curvature) * 240;
775
+ const rgb = hsvToRgb(hue, 1, 0.8);
776
+ colors[i * 3] = rgb[0];
777
+ colors[i * 3 + 1] = rgb[1];
778
+ colors[i * 3 + 2] = rgb[2];
779
+ }
780
+
781
+ if (apply) {
782
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3, true));
783
+ mesh.material.vertexColors = true;
784
+ }
785
+
786
+ return { surfaceId, type, colorMap, curvatures, analysis: 'Curvature computed' };
787
+ }
788
+
789
+ /**
790
+ * Zebra stripes - continuity analysis visualization
791
+ */
792
+ async function zebraStripes(surfaceId, options = {}) {
793
+ const { stripeWidth = 0.5, direction = 'u', apply = true } = options;
794
+ const surface = surfaces.get(surfaceId);
795
+ if (!surface) throw new Error(`Surface ${surfaceId} not found`);
796
+
797
+ const mesh = surface.mesh;
798
+ if (!mesh || !mesh.geometry) return null;
799
+
800
+ const geometry = mesh.geometry;
801
+ const positions = geometry.attributes.position;
802
+ const colors = new Uint8Array(positions.count * 3);
803
+
804
+ for (let i = 0; i < positions.count; i++) {
805
+ const pos = new THREE.Vector3().fromBufferAttribute(positions, i);
806
+ const coord = direction === 'u' ? pos.x : pos.y;
807
+ const stripe = Math.floor(coord / stripeWidth) % 2;
808
+ const color = stripe === 0 ? 255 : 200;
809
+ colors[i * 3] = color;
810
+ colors[i * 3 + 1] = color;
811
+ colors[i * 3 + 2] = color;
812
+ }
813
+
814
+ if (apply) {
815
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3, true));
816
+ mesh.material.vertexColors = true;
817
+ }
818
+
819
+ return { surfaceId, stripeWidth, direction, applied: true };
820
+ }
821
+
822
+ /**
823
+ * Draft analysis - check if surface can be pulled from mold
824
+ */
825
+ async function draftAnalysis(surfaceId, options = {}) {
826
+ const { pullDirection = new THREE.Vector3(0, 0, 1), minAngle = 2 } = options;
827
+ const surface = surfaces.get(surfaceId);
828
+ if (!surface) throw new Error(`Surface ${surfaceId} not found`);
829
+
830
+ const mesh = surface.mesh;
831
+ if (!mesh || !mesh.geometry) return null;
832
+
833
+ const geometry = mesh.geometry;
834
+ const normals = geometry.attributes.normal;
835
+ const minAngleRad = (minAngle * Math.PI) / 180;
836
+
837
+ let passCount = 0, failCount = 0;
838
+ const problemAreas = [];
839
+
840
+ for (let i = 0; i < normals.count; i++) {
841
+ const normal = new THREE.Vector3().fromBufferAttribute(normals, i);
842
+ const angle = Math.acos(Math.abs(normal.dot(pullDirection.normalize())));
843
+
844
+ if (angle >= minAngleRad) {
845
+ passCount++;
846
+ } else {
847
+ failCount++;
848
+ const pos = new THREE.Vector3().fromBufferAttribute(geometry.attributes.position, i);
849
+ problemAreas.push({ position: pos, angle: (angle * 180) / Math.PI });
850
+ }
851
+ }
852
+
853
+ return {
854
+ surfaceId,
855
+ pullDirection: { x: pullDirection.x, y: pullDirection.y, z: pullDirection.z },
856
+ minAngle,
857
+ passPercentage: (passCount / (passCount + failCount)) * 100,
858
+ problemAreas,
859
+ passed: failCount === 0
860
+ };
861
+ }
862
+
863
+ /**
864
+ * Isocurve display - show parametric curves on surface
865
+ */
866
+ async function showIsocurves(surfaceId, options = {}) {
867
+ const { uCount = 10, vCount = 10, color = 0x00ff00 } = options;
868
+ const surface = surfaces.get(surfaceId);
869
+ if (!surface) throw new Error(`Surface ${surfaceId} not found`);
870
+
871
+ const curves = [];
872
+ for (let i = 0; i < uCount; i++) {
873
+ curves.push({ type: 'u', parameter: i / uCount, color });
874
+ }
875
+ for (let i = 0; i < vCount; i++) {
876
+ curves.push({ type: 'v', parameter: i / vCount, color });
877
+ }
878
+
879
+ return { surfaceId, isocurves: curves, uCount, vCount, visible: true };
880
+ }
881
+
882
+ /**
883
+ * Unstitch surfaces - break joined surfaces apart
884
+ */
885
+ async function unstitchSurfaces(solidId) {
886
+ const surfaces_list = [];
887
+ // In real implementation, extract individual surface faces from solid
888
+ return { solidId, surfaces: surfaces_list, count: surfaces_list.length };
889
+ }
890
+
891
+ /**
892
+ * Replace face - swap solid face with surface
893
+ */
894
+ async function replaceFace(solidId, faceIndex, replacementSurfaceId) {
895
+ const id = `solid_replaced_face_${surfaceCounter.count++}`;
896
+
897
+ if (surfaceManager.kernel) {
898
+ try {
899
+ const result = await surfaceManager.execBrep('replaceFace', { solidId, faceIndex, replacementSurfaceId });
900
+ if (result) {
901
+ return { id, type: 'solid', original: solidId, replacedFaceIndex: faceIndex };
902
+ }
903
+ } catch (e) {
904
+ console.warn('[Surface] B-Rep replace face failed:', e.message);
905
+ }
906
+ }
907
+
908
+ return { id, type: 'solid', original: solidId, replacedFaceIndex: faceIndex };
909
+ }
910
+
911
+ /**
912
+ * Pipe along path - create tube surface along curve
913
+ */
914
+ async function pipeAlongPath(profileOrId, pathOrId, options = {}) {
915
+ const { radius = 5, align = 'normal' } = options;
916
+ const id = `surface_pipe_${surfaceCounter.count++}`;
917
+
918
+ const mesh = createPipeSurfaceMesh(profileOrId, pathOrId, radius, align);
919
+ surfaces.set(id, { type: 'pipe_surface', mesh, radius, align });
920
+
921
+ if (viewport?.scene) {
922
+ mesh.material.side = THREE.DoubleSide;
923
+ viewport.scene.add(mesh);
924
+ }
925
+
926
+ return { id, type: 'pipe_surface', radius, align };
927
+ }
928
+
929
+ /**
930
+ * Circular surface cap - fill boundary with circular surface
931
+ */
932
+ async function circularCap(boundaryLoop) {
933
+ const id = `surface_circular_cap_${surfaceCounter.count++}`;
934
+
935
+ const mesh = createCircularCapMesh(boundaryLoop);
936
+ surfaces.set(id, { type: 'circular_cap', mesh });
937
+
938
+ if (viewport?.scene) {
939
+ mesh.material.side = THREE.DoubleSide;
940
+ mesh.material.color.setHex(0xffaa44);
941
+ viewport.scene.add(mesh);
942
+ }
943
+
944
+ return { id, type: 'circular_cap' };
945
+ }
946
+
947
+ /**
948
+ * Helper: Create pipe surface mesh
949
+ */
950
+ function createPipeSurfaceMesh(profile, path, radius, align) {
951
+ const geom = new THREE.BufferGeometry();
952
+ const mat = new THREE.MeshPhongMaterial({ color: 0xcc88ff, side: THREE.DoubleSide });
953
+ return new THREE.Mesh(geom, mat);
954
+ }
955
+
956
+ /**
957
+ * Helper: Create circular cap mesh
958
+ */
959
+ function createCircularCapMesh(boundaryLoop) {
960
+ const geom = new THREE.BufferGeometry();
961
+ const mat = new THREE.MeshPhongMaterial({ color: 0xffaa44, side: THREE.DoubleSide });
962
+ return new THREE.Mesh(geom, mat);
963
+ }
964
+
965
+ /**
966
+ * Helper: HSV to RGB conversion
967
+ */
968
+ function hsvToRgb(h, s, v) {
969
+ h = h % 360;
970
+ const c = v * s;
971
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
972
+ const m = v - c;
973
+
974
+ let r, g, b;
975
+ if (h < 60) { r = c; g = x; b = 0; }
976
+ else if (h < 120) { r = x; g = c; b = 0; }
977
+ else if (h < 180) { r = 0; g = c; b = x; }
978
+ else if (h < 240) { r = 0; g = x; b = c; }
979
+ else if (h < 300) { r = x; g = 0; b = c; }
980
+ else { r = c; g = 0; b = x; }
981
+
982
+ return [
983
+ Math.round((r + m) * 255),
984
+ Math.round((g + m) * 255),
985
+ Math.round((b + m) * 255)
986
+ ];
987
+ }
988
+
989
+ return {
990
+ MODULE_NAME,
991
+ init,
992
+ getUI,
993
+ extrude: extrudeSurface,
994
+ revolve: revolveSurface,
995
+ sweep: sweepSurface,
996
+ loft: loftSurface,
997
+ patch: patchSurface,
998
+ trim: trimSurface,
999
+ extend: extendSurface,
1000
+ extendAdvanced: extendSurfaceAdvanced,
1001
+ offset: offsetSurface,
1002
+ thicken: thickenSurface,
1003
+ stitch: stitchSurfaces,
1004
+ ruled: ruledSurface,
1005
+ boundary: boundarySurface,
1006
+ sculpt: sculptTSpline,
1007
+ curvature: analyzeCurvature,
1008
+ zebra: zebraStripes,
1009
+ draft: draftAnalysis,
1010
+ isocurves: showIsocurves,
1011
+ unstitch: unstitchSurfaces,
1012
+ replaceFace: replaceFace,
1013
+ pipe: pipeAlongPath,
1014
+ circularCap: circularCap,
1015
+ };
1016
+ })();
1017
+
1018
+ /**
1019
+ * Help entries for surface module
1020
+ */
1021
+ const HELP_ENTRIES_SURFACE = [
1022
+ { id: 'surf-extrude', title: 'Extrude Surface', category: 'Surface', description: 'Extrude open profile into surface' },
1023
+ { id: 'surf-revolve', title: 'Revolve Surface', category: 'Surface', description: 'Revolve profile around axis' },
1024
+ { id: 'surf-sweep', title: 'Sweep Surface', category: 'Surface', description: 'Sweep profile along path' },
1025
+ { id: 'surf-loft', title: 'Loft Surface', category: 'Surface', description: 'Blend between multiple profiles' },
1026
+ { id: 'surf-patch', title: 'Patch Surface', category: 'Surface', description: 'Fill boundary with Coons patch' },
1027
+ { id: 'surf-ruled', title: 'Ruled Surface', category: 'Surface', description: 'Create ruled surface between curves' },
1028
+ { id: 'surf-boundary', title: 'Boundary Surface', category: 'Surface', description: 'Fill 4-sided boundary' },
1029
+ { id: 'surf-offset', title: 'Offset Surface', category: 'Surface', description: 'Create parallel offset' },
1030
+ { id: 'surf-extend', title: 'Extend Surface', category: 'Surface', description: 'Extend surface edge' },
1031
+ { id: 'surf-curvature', title: 'Curvature Analysis', category: 'Surface', description: 'Analyze and visualize curvature' },
1032
+ { id: 'surf-zebra', title: 'Zebra Stripes', category: 'Surface', description: 'Continuity visualization' },
1033
+ { id: 'surf-draft', title: 'Draft Analysis', category: 'Surface', description: 'Check molding draft angles' },
1034
+ { id: 'surf-isocurves', title: 'Isocurves', category: 'Surface', description: 'Display parametric curves' },
1035
+ { id: 'surf-thicken', title: 'Thicken', category: 'Surface', description: 'Convert surface to solid' },
1036
+ { id: 'surf-stitch', title: 'Stitch', category: 'Surface', description: 'Join surfaces into solid' },
1037
+ { id: 'surf-pipe', title: 'Pipe Along Path', category: 'Surface', description: 'Create tube along curve' },
1038
+ ];
1039
+
1040
+ export default SurfaceModule;