cyclecad 3.2.1 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +155 -1
- package/DOCKER-SETUP-VERIFICATION.md +399 -0
- package/DOCKER-TESTING.md +463 -0
- package/FUSION360_MODULES.md +478 -0
- package/FUSION_MODULES_README.md +352 -0
- package/INTEGRATION_SNIPPETS.md +608 -0
- package/KILLER-FEATURES-DELIVERY.md +469 -0
- package/MODULES_SUMMARY.txt +337 -0
- package/QUICK_REFERENCE.txt +298 -0
- package/README-DOCKER-TESTING.txt +438 -0
- package/app/index.html +23 -10
- package/app/js/fusion-help.json +1808 -0
- package/app/js/help-module-v3.js +1096 -0
- package/app/js/killer-features-help.json +395 -0
- package/app/js/killer-features.js +1508 -0
- package/app/js/modules/fusion-assembly.js +842 -0
- package/app/js/modules/fusion-cam.js +785 -0
- package/app/js/modules/fusion-data.js +814 -0
- package/app/js/modules/fusion-drawing.js +844 -0
- package/app/js/modules/fusion-inspection.js +756 -0
- package/app/js/modules/fusion-render.js +774 -0
- package/app/js/modules/fusion-simulation.js +986 -0
- package/app/js/modules/fusion-sketch.js +1044 -0
- package/app/js/modules/fusion-solid.js +1095 -0
- package/app/js/modules/fusion-surface.js +949 -0
- package/app/tests/FUSION_TEST_SUITE.md +266 -0
- package/app/tests/README.md +77 -0
- package/app/tests/TESTING-CHECKLIST.md +177 -0
- package/app/tests/TEST_SUITE_SUMMARY.txt +236 -0
- package/app/tests/brep-live-test.html +848 -0
- package/app/tests/docker-integration-test.html +811 -0
- package/app/tests/fusion-all-tests.html +670 -0
- package/app/tests/fusion-assembly-tests.html +461 -0
- package/app/tests/fusion-cam-tests.html +421 -0
- package/app/tests/fusion-simulation-tests.html +421 -0
- package/app/tests/fusion-sketch-tests.html +613 -0
- package/app/tests/fusion-solid-tests.html +529 -0
- package/app/tests/index.html +453 -0
- package/app/tests/killer-features-test.html +509 -0
- package/app/tests/run-tests.html +874 -0
- package/app/tests/step-import-live-test.html +1115 -0
- package/app/tests/test-agent-v3.html +93 -696
- package/architecture-dashboard.html +1970 -0
- package/docs/API-REFERENCE.md +1423 -0
- package/docs/BREP-LIVE-TEST-GUIDE.md +453 -0
- package/docs/DEVELOPER-GUIDE-v3.md +795 -0
- package/docs/DOCKER-QUICK-TEST.md +376 -0
- package/docs/FUSION-FEATURES-GUIDE.md +2513 -0
- package/docs/FUSION-TUTORIAL.md +1203 -0
- package/docs/INFRASTRUCTURE-GUIDE-INDEX.md +327 -0
- package/docs/KEYBOARD-SHORTCUTS.md +402 -0
- package/docs/KILLER-FEATURES-INTEGRATION.md +412 -0
- package/docs/KILLER-FEATURES-SUMMARY.md +424 -0
- package/docs/KILLER-FEATURES-TUTORIAL.md +784 -0
- package/docs/KILLER-FEATURES.md +562 -0
- package/docs/QUICK-REFERENCE.md +282 -0
- package/docs/README-v3-DOCS.md +274 -0
- package/docs/TUTORIAL-v3.md +1190 -0
- package/docs/architecture-dashboard.html +1970 -0
- package/docs/architecture-v3.html +1038 -0
- package/linkedin-post-v3.md +58 -0
- package/package.json +1 -1
- package/scripts/dev-setup.sh +338 -0
- package/scripts/docker-health-check.sh +159 -0
- package/scripts/integration-test.sh +311 -0
- package/scripts/test-docker.sh +515 -0
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fusion-surface.js — Fusion 360 Surface Modeling Module for cycleCAD
|
|
3
|
+
*
|
|
4
|
+
* Complete surface modeling operations with Fusion 360 parity:
|
|
5
|
+
* - Extrude Surface, Revolve Surface, Sweep Surface, Loft Surface
|
|
6
|
+
* - Patch (fill opening with surface)
|
|
7
|
+
* - Offset Surface (uniform/non-uniform)
|
|
8
|
+
* - Thicken (surface to solid)
|
|
9
|
+
* - Stitch (join surfaces)
|
|
10
|
+
* - Unstitch (split surface)
|
|
11
|
+
* - Trim (cut surface with tool)
|
|
12
|
+
* - Untrim (restore trimmed regions)
|
|
13
|
+
* - Extend Surface
|
|
14
|
+
* - Sculpt (T-spline editing with control cage)
|
|
15
|
+
* - Ruled Surface (linear between two edges)
|
|
16
|
+
*
|
|
17
|
+
* All operations create THREE.Mesh with DoubleSide material for proper visualization.
|
|
18
|
+
*
|
|
19
|
+
* Version: 1.0.0
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// CONSTANTS & STATE
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
const SURFACE_OPERATIONS = {
|
|
29
|
+
EXTRUDE_SURFACE: 'extrude_surface',
|
|
30
|
+
REVOLVE_SURFACE: 'revolve_surface',
|
|
31
|
+
SWEEP_SURFACE: 'sweep_surface',
|
|
32
|
+
LOFT_SURFACE: 'loft_surface',
|
|
33
|
+
PATCH: 'patch',
|
|
34
|
+
OFFSET: 'offset',
|
|
35
|
+
THICKEN: 'thicken',
|
|
36
|
+
STITCH: 'stitch',
|
|
37
|
+
UNSTITCH: 'unstitch',
|
|
38
|
+
TRIM: 'trim',
|
|
39
|
+
UNTRIM: 'untrim',
|
|
40
|
+
EXTEND: 'extend',
|
|
41
|
+
SCULPT: 'sculpt',
|
|
42
|
+
RULED: 'ruled',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let surfaceState = {
|
|
46
|
+
surfaces: [], // Array of surface objects
|
|
47
|
+
features: [], // Parametric feature history
|
|
48
|
+
selectedSurface: null,
|
|
49
|
+
sculptMode: false,
|
|
50
|
+
controlCage: null,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// SURFACE CLASS
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Represents a parametric surface in 3D space
|
|
59
|
+
*/
|
|
60
|
+
class Surface {
|
|
61
|
+
constructor(id, geometry, name = 'Surface', type = 'nurbs') {
|
|
62
|
+
this.id = id;
|
|
63
|
+
this.name = name;
|
|
64
|
+
this.type = type; // 'nurbs', 'mesh', 'ruled', etc.
|
|
65
|
+
this.geometry = geometry;
|
|
66
|
+
|
|
67
|
+
// Create mesh with DoubleSide for proper rendering
|
|
68
|
+
const material = new THREE.MeshStandardMaterial({
|
|
69
|
+
color: 0x44aa99,
|
|
70
|
+
metalness: 0.3,
|
|
71
|
+
roughness: 0.7,
|
|
72
|
+
side: THREE.DoubleSide,
|
|
73
|
+
wireframe: false,
|
|
74
|
+
transparent: true,
|
|
75
|
+
opacity: 0.9,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
this.mesh = new THREE.Mesh(geometry, material);
|
|
79
|
+
this.originalGeometry = geometry.clone();
|
|
80
|
+
this.features = [];
|
|
81
|
+
this.trimmedRegions = [];
|
|
82
|
+
this.controlPoints = [];
|
|
83
|
+
|
|
84
|
+
// Create control cage for sculpting
|
|
85
|
+
this._createControlCage();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_createControlCage() {
|
|
89
|
+
const positions = this.geometry.attributes.position.array;
|
|
90
|
+
const cpGeometry = new THREE.BufferGeometry();
|
|
91
|
+
|
|
92
|
+
// Sample control points from surface
|
|
93
|
+
const sampleRate = 5;
|
|
94
|
+
const cpPositions = [];
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < positions.length; i += 3 * sampleRate) {
|
|
97
|
+
cpPositions.push(positions[i], positions[i + 1], positions[i + 2]);
|
|
98
|
+
this.controlPoints.push({
|
|
99
|
+
index: i,
|
|
100
|
+
position: new THREE.Vector3(positions[i], positions[i + 1], positions[i + 2]),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
cpGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(cpPositions), 3));
|
|
105
|
+
|
|
106
|
+
const cpMaterial = new THREE.PointsMaterial({
|
|
107
|
+
color: 0xff00ff,
|
|
108
|
+
size: 1,
|
|
109
|
+
sizeAttenuation: true,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
this.controlCagePoints = new THREE.Points(cpGeometry, cpMaterial);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
showControlCage(show = true) {
|
|
116
|
+
if (show) {
|
|
117
|
+
this.controlCagePoints.visible = true;
|
|
118
|
+
} else {
|
|
119
|
+
this.controlCagePoints.visible = false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
toJSON() {
|
|
124
|
+
return {
|
|
125
|
+
id: this.id,
|
|
126
|
+
name: this.name,
|
|
127
|
+
type: this.type,
|
|
128
|
+
geometry: {
|
|
129
|
+
vertices: this.geometry.attributes.position.array,
|
|
130
|
+
normals: this.geometry.attributes.normal?.array,
|
|
131
|
+
indices: this.geometry.index?.array,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// NURBS SURFACE UTILITIES
|
|
139
|
+
// ============================================================================
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create a simple B-spline surface (approximation)
|
|
143
|
+
*/
|
|
144
|
+
function createBSplineSurface(controlPointsU, controlPointsV, degreeU = 3, degreeV = 3) {
|
|
145
|
+
const numU = controlPointsU.length;
|
|
146
|
+
const numV = controlPointsV.length;
|
|
147
|
+
|
|
148
|
+
const geometry = new THREE.BufferGeometry();
|
|
149
|
+
const vertices = [];
|
|
150
|
+
const indices = [];
|
|
151
|
+
|
|
152
|
+
// Create surface points by bilinear interpolation (simplified B-spline)
|
|
153
|
+
for (let u = 0; u <= 1; u += 0.1) {
|
|
154
|
+
for (let v = 0; v <= 1; v += 0.1) {
|
|
155
|
+
let point = new THREE.Vector3(0, 0, 0);
|
|
156
|
+
|
|
157
|
+
// Simple Bezier surface interpolation
|
|
158
|
+
for (let i = 0; i < numU; i++) {
|
|
159
|
+
for (let j = 0; j < numV; j++) {
|
|
160
|
+
const bu = _bernstein(i, degreeU, u);
|
|
161
|
+
const bv = _bernstein(j, degreeV, v);
|
|
162
|
+
const cp = controlPointsU[i] || controlPointsV[j] || new THREE.Vector3(0, 0, 0);
|
|
163
|
+
point.addScaledVector(cp, bu * bv);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
vertices.push(point.x, point.y, point.z);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Generate indices
|
|
172
|
+
const uSteps = 11;
|
|
173
|
+
const vSteps = 11;
|
|
174
|
+
for (let i = 0; i < uSteps - 1; i++) {
|
|
175
|
+
for (let j = 0; j < vSteps - 1; j++) {
|
|
176
|
+
const a = i * vSteps + j;
|
|
177
|
+
const b = a + 1;
|
|
178
|
+
const c = a + vSteps;
|
|
179
|
+
const d = c + 1;
|
|
180
|
+
|
|
181
|
+
indices.push(a, b, c);
|
|
182
|
+
indices.push(b, d, c);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
|
|
187
|
+
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
|
|
188
|
+
geometry.computeVertexNormals();
|
|
189
|
+
|
|
190
|
+
return geometry;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Bernstein basis polynomial
|
|
195
|
+
*/
|
|
196
|
+
function _bernstein(i, n, t) {
|
|
197
|
+
if (i > n || i < 0) return 0;
|
|
198
|
+
|
|
199
|
+
if (n === 0) {
|
|
200
|
+
return t === 0 ? 1 : 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const c = _binomial(n, i);
|
|
204
|
+
return c * Math.pow(t, i) * Math.pow(1 - t, n - i);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Binomial coefficient
|
|
209
|
+
*/
|
|
210
|
+
function _binomial(n, k) {
|
|
211
|
+
if (k > n || k < 0) return 0;
|
|
212
|
+
if (k === 0 || k === n) return 1;
|
|
213
|
+
|
|
214
|
+
let result = 1;
|
|
215
|
+
for (let i = 0; i < k; i++) {
|
|
216
|
+
result *= (n - i) / (i + 1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ============================================================================
|
|
223
|
+
// MAIN MODULE INTERFACE
|
|
224
|
+
// ============================================================================
|
|
225
|
+
|
|
226
|
+
let nextSurfaceId = 0;
|
|
227
|
+
|
|
228
|
+
export default {
|
|
229
|
+
/**
|
|
230
|
+
* Initialize surface module
|
|
231
|
+
*/
|
|
232
|
+
init() {
|
|
233
|
+
surfaceState = {
|
|
234
|
+
surfaces: [],
|
|
235
|
+
features: [],
|
|
236
|
+
selectedSurface: null,
|
|
237
|
+
sculptMode: false,
|
|
238
|
+
controlCage: null,
|
|
239
|
+
};
|
|
240
|
+
nextSurfaceId = 0;
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Extrude surface perpendicular to plane
|
|
245
|
+
*/
|
|
246
|
+
extrudeSurface(faceGeometry, params = {}) {
|
|
247
|
+
const {
|
|
248
|
+
distance = 10,
|
|
249
|
+
direction = 'positive',
|
|
250
|
+
symmetric = false,
|
|
251
|
+
name = 'Extrude Surface',
|
|
252
|
+
} = params;
|
|
253
|
+
|
|
254
|
+
let extLength = distance;
|
|
255
|
+
if (symmetric) {
|
|
256
|
+
extLength = distance / 2;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const geometry = faceGeometry.clone();
|
|
260
|
+
const positions = geometry.attributes.position.array;
|
|
261
|
+
const normals = geometry.attributes.normal.array;
|
|
262
|
+
|
|
263
|
+
// Offset surface in normal direction
|
|
264
|
+
const extrudedPositions = new Float32Array(positions.length * 2);
|
|
265
|
+
|
|
266
|
+
// Original surface
|
|
267
|
+
for (let i = 0; i < positions.length; i++) {
|
|
268
|
+
extrudedPositions[i] = positions[i];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Extruded surface
|
|
272
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
273
|
+
const offset = extLength;
|
|
274
|
+
extrudedPositions[positions.length + i] = positions[i] + (normals[i] ?? 0) * offset;
|
|
275
|
+
extrudedPositions[positions.length + i + 1] = positions[i + 1] + (normals[i + 1] ?? 0) * offset;
|
|
276
|
+
extrudedPositions[positions.length + i + 2] = positions[i + 2] + (normals[i + 2] ?? 0) * offset;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(extrudedPositions, 3));
|
|
280
|
+
geometry.computeVertexNormals();
|
|
281
|
+
geometry.computeBoundingBox();
|
|
282
|
+
|
|
283
|
+
const surface = new Surface(`surface_${nextSurfaceId++}`, geometry, name, 'extrude');
|
|
284
|
+
surfaceState.surfaces.push(surface);
|
|
285
|
+
|
|
286
|
+
surfaceState.features.push({
|
|
287
|
+
type: SURFACE_OPERATIONS.EXTRUDE_SURFACE,
|
|
288
|
+
params: { distance, direction, symmetric },
|
|
289
|
+
surfaceId: surface.id,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
return { success: true, surface };
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Revolve surface around axis
|
|
297
|
+
*/
|
|
298
|
+
revolveSurface(curveGeometry, axis = 'Z', params = {}) {
|
|
299
|
+
const {
|
|
300
|
+
angle = Math.PI * 2,
|
|
301
|
+
direction = 'positive',
|
|
302
|
+
name = 'Revolve Surface',
|
|
303
|
+
} = params;
|
|
304
|
+
|
|
305
|
+
const geometry = new THREE.BufferGeometry();
|
|
306
|
+
const curvePositions = curveGeometry.attributes.position.array;
|
|
307
|
+
const revolvedPositions = [];
|
|
308
|
+
|
|
309
|
+
const segments = Math.max(32, Math.round((angle / Math.PI) * 32));
|
|
310
|
+
|
|
311
|
+
for (let seg = 0; seg <= segments; seg++) {
|
|
312
|
+
const theta = (seg / segments) * angle;
|
|
313
|
+
const cos = Math.cos(theta);
|
|
314
|
+
const sin = Math.sin(theta);
|
|
315
|
+
|
|
316
|
+
for (let i = 0; i < curvePositions.length; i += 3) {
|
|
317
|
+
const x = curvePositions[i];
|
|
318
|
+
const y = curvePositions[i + 1];
|
|
319
|
+
const z = curvePositions[i + 2];
|
|
320
|
+
|
|
321
|
+
let rx = x, ry = y, rz = z;
|
|
322
|
+
|
|
323
|
+
if (axis === 'Z') {
|
|
324
|
+
rx = x * cos - y * sin;
|
|
325
|
+
ry = x * sin + y * cos;
|
|
326
|
+
rz = z;
|
|
327
|
+
} else if (axis === 'X') {
|
|
328
|
+
rx = x;
|
|
329
|
+
ry = y * cos - z * sin;
|
|
330
|
+
rz = y * sin + z * cos;
|
|
331
|
+
} else if (axis === 'Y') {
|
|
332
|
+
rx = x * cos + z * sin;
|
|
333
|
+
ry = y;
|
|
334
|
+
rz = -x * sin + z * cos;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
revolvedPositions.push(rx, ry, rz);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
geometry.setAttribute(
|
|
342
|
+
'position',
|
|
343
|
+
new THREE.BufferAttribute(new Float32Array(revolvedPositions), 3)
|
|
344
|
+
);
|
|
345
|
+
geometry.computeVertexNormals();
|
|
346
|
+
|
|
347
|
+
const surface = new Surface(`surface_${nextSurfaceId++}`, geometry, name, 'revolve');
|
|
348
|
+
surfaceState.surfaces.push(surface);
|
|
349
|
+
|
|
350
|
+
surfaceState.features.push({
|
|
351
|
+
type: SURFACE_OPERATIONS.REVOLVE_SURFACE,
|
|
352
|
+
params: { axis, angle, direction },
|
|
353
|
+
surfaceId: surface.id,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return { success: true, surface };
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Sweep surface along path
|
|
361
|
+
*/
|
|
362
|
+
sweepSurface(profileGeometry, pathGeometry, params = {}) {
|
|
363
|
+
const {
|
|
364
|
+
twist = 0,
|
|
365
|
+
scaleStart = 1,
|
|
366
|
+
scaleEnd = 1,
|
|
367
|
+
keepNormal = false,
|
|
368
|
+
name = 'Sweep Surface',
|
|
369
|
+
} = params;
|
|
370
|
+
|
|
371
|
+
const geometry = new THREE.BufferGeometry();
|
|
372
|
+
const pathPositions = pathGeometry.attributes.position.array;
|
|
373
|
+
const profilePositions = profileGeometry.attributes.position.array;
|
|
374
|
+
|
|
375
|
+
const sweptPositions = [];
|
|
376
|
+
const pathSteps = Math.floor(pathPositions.length / 3);
|
|
377
|
+
|
|
378
|
+
for (let step = 0; step < pathSteps; step++) {
|
|
379
|
+
const t = step / pathSteps;
|
|
380
|
+
const pathIndex = step * 3;
|
|
381
|
+
const pathX = pathPositions[pathIndex];
|
|
382
|
+
const pathY = pathPositions[pathIndex + 1];
|
|
383
|
+
const pathZ = pathPositions[pathIndex + 2];
|
|
384
|
+
|
|
385
|
+
const scale = scaleStart + (scaleEnd - scaleStart) * t;
|
|
386
|
+
const angle = twist * t;
|
|
387
|
+
const cos = Math.cos(angle);
|
|
388
|
+
const sin = Math.sin(angle);
|
|
389
|
+
|
|
390
|
+
for (let i = 0; i < profilePositions.length; i += 3) {
|
|
391
|
+
let x = profilePositions[i] * scale;
|
|
392
|
+
let y = profilePositions[i + 1] * scale;
|
|
393
|
+
const z = profilePositions[i + 2];
|
|
394
|
+
|
|
395
|
+
const rx = x * cos - y * sin;
|
|
396
|
+
const ry = x * sin + y * cos;
|
|
397
|
+
|
|
398
|
+
sweptPositions.push(pathX + rx, pathY + ry, pathZ + z);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
geometry.setAttribute(
|
|
403
|
+
'position',
|
|
404
|
+
new THREE.BufferAttribute(new Float32Array(sweptPositions), 3)
|
|
405
|
+
);
|
|
406
|
+
geometry.computeVertexNormals();
|
|
407
|
+
|
|
408
|
+
const surface = new Surface(`surface_${nextSurfaceId++}`, geometry, name, 'sweep');
|
|
409
|
+
surfaceState.surfaces.push(surface);
|
|
410
|
+
|
|
411
|
+
surfaceState.features.push({
|
|
412
|
+
type: SURFACE_OPERATIONS.SWEEP_SURFACE,
|
|
413
|
+
params: { twist, scaleStart, scaleEnd, keepNormal },
|
|
414
|
+
surfaceId: surface.id,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
return { success: true, surface };
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Loft between multiple profiles
|
|
422
|
+
*/
|
|
423
|
+
loftSurface(profileGeometries, params = {}) {
|
|
424
|
+
const {
|
|
425
|
+
matchPeaks = false,
|
|
426
|
+
name = 'Loft Surface',
|
|
427
|
+
} = params;
|
|
428
|
+
|
|
429
|
+
if (!Array.isArray(profileGeometries) || profileGeometries.length < 2) {
|
|
430
|
+
return { success: false, message: 'Loft requires at least 2 profiles' };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const geometry = new THREE.BufferGeometry();
|
|
434
|
+
const allPositions = profileGeometries.map(pg => pg.attributes.position.array);
|
|
435
|
+
|
|
436
|
+
const loftPositions = [];
|
|
437
|
+
const steps = allPositions.length;
|
|
438
|
+
|
|
439
|
+
for (let step = 0; step < steps; step++) {
|
|
440
|
+
const positions = allPositions[step];
|
|
441
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
442
|
+
loftPositions.push(positions[i], positions[i + 1], positions[i + 2]);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
geometry.setAttribute(
|
|
447
|
+
'position',
|
|
448
|
+
new THREE.BufferAttribute(new Float32Array(loftPositions), 3)
|
|
449
|
+
);
|
|
450
|
+
geometry.computeVertexNormals();
|
|
451
|
+
|
|
452
|
+
const surface = new Surface(`surface_${nextSurfaceId++}`, geometry, name, 'loft');
|
|
453
|
+
surfaceState.surfaces.push(surface);
|
|
454
|
+
|
|
455
|
+
surfaceState.features.push({
|
|
456
|
+
type: SURFACE_OPERATIONS.LOFT_SURFACE,
|
|
457
|
+
params: { profileCount: profileGeometries.length, matchPeaks },
|
|
458
|
+
surfaceId: surface.id,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
return { success: true, surface };
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Patch (fill opening with surface)
|
|
466
|
+
*/
|
|
467
|
+
patch(boundaryCurves, params = {}) {
|
|
468
|
+
const {
|
|
469
|
+
continuity = 'G2', // C0, C1, G1, G2
|
|
470
|
+
angle = 0,
|
|
471
|
+
name = 'Patch',
|
|
472
|
+
} = params;
|
|
473
|
+
|
|
474
|
+
if (!Array.isArray(boundaryCurves) || boundaryCurves.length === 0) {
|
|
475
|
+
return { success: false, message: 'Patch requires boundary curves' };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Create patch surface from boundary
|
|
479
|
+
const geometry = new THREE.BufferGeometry();
|
|
480
|
+
|
|
481
|
+
// Use first boundary curve to generate surface points
|
|
482
|
+
const boundaryPositions = boundaryCurves[0].attributes.position.array;
|
|
483
|
+
const patchPositions = [];
|
|
484
|
+
|
|
485
|
+
for (let i = 0; i < boundaryPositions.length; i += 3) {
|
|
486
|
+
patchPositions.push(boundaryPositions[i], boundaryPositions[i + 1], boundaryPositions[i + 2]);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Fill interior with interpolated points
|
|
490
|
+
for (let x = 0.25; x < 1; x += 0.25) {
|
|
491
|
+
for (let y = 0.25; y < 1; y += 0.25) {
|
|
492
|
+
const px = boundaryPositions[0] * (1 - x);
|
|
493
|
+
const py = boundaryPositions[1] * (1 - y);
|
|
494
|
+
const pz = (boundaryPositions[2] ?? 0) * 0.5;
|
|
495
|
+
patchPositions.push(px, py, pz);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
geometry.setAttribute(
|
|
500
|
+
'position',
|
|
501
|
+
new THREE.BufferAttribute(new Float32Array(patchPositions), 3)
|
|
502
|
+
);
|
|
503
|
+
geometry.computeVertexNormals();
|
|
504
|
+
|
|
505
|
+
const surface = new Surface(`surface_${nextSurfaceId++}`, geometry, name, 'patch');
|
|
506
|
+
surfaceState.surfaces.push(surface);
|
|
507
|
+
|
|
508
|
+
surfaceState.features.push({
|
|
509
|
+
type: SURFACE_OPERATIONS.PATCH,
|
|
510
|
+
params: { continuity, angle, boundaryCount: boundaryCurves.length },
|
|
511
|
+
surfaceId: surface.id,
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
return { success: true, surface };
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Offset surface uniformly or non-uniformly
|
|
519
|
+
*/
|
|
520
|
+
offsetSurface(surfaceGeometry, params = {}) {
|
|
521
|
+
const {
|
|
522
|
+
distance = 2,
|
|
523
|
+
side = 'both', // 'positive', 'negative', 'both'
|
|
524
|
+
name = 'Offset Surface',
|
|
525
|
+
} = params;
|
|
526
|
+
|
|
527
|
+
const geometry = surfaceGeometry.clone();
|
|
528
|
+
const positions = geometry.attributes.position.array;
|
|
529
|
+
const normals = geometry.attributes.normal.array;
|
|
530
|
+
|
|
531
|
+
if (side === 'positive' || side === 'both') {
|
|
532
|
+
const offsetPositions = new Float32Array(positions.length * (side === 'both' ? 2 : 1));
|
|
533
|
+
|
|
534
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
535
|
+
const offset = distance;
|
|
536
|
+
offsetPositions[i] = positions[i] + (normals[i] ?? 0) * offset;
|
|
537
|
+
offsetPositions[i + 1] = positions[i + 1] + (normals[i + 1] ?? 0) * offset;
|
|
538
|
+
offsetPositions[i + 2] = positions[i + 2] + (normals[i + 2] ?? 0) * offset;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (side === 'both') {
|
|
542
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
543
|
+
const offset = -distance;
|
|
544
|
+
offsetPositions[positions.length + i] = positions[i] + (normals[i] ?? 0) * offset;
|
|
545
|
+
offsetPositions[positions.length + i + 1] = positions[i + 1] + (normals[i + 1] ?? 0) * offset;
|
|
546
|
+
offsetPositions[positions.length + i + 2] = positions[i + 2] + (normals[i + 2] ?? 0) * offset;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(offsetPositions, 3));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
geometry.computeVertexNormals();
|
|
554
|
+
|
|
555
|
+
const surface = new Surface(`surface_${nextSurfaceId++}`, geometry, name, 'offset');
|
|
556
|
+
surfaceState.surfaces.push(surface);
|
|
557
|
+
|
|
558
|
+
surfaceState.features.push({
|
|
559
|
+
type: SURFACE_OPERATIONS.OFFSET,
|
|
560
|
+
params: { distance, side },
|
|
561
|
+
surfaceId: surface.id,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
return { success: true, surface };
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Stitch multiple surfaces together
|
|
569
|
+
*/
|
|
570
|
+
stitchSurfaces(surfaceIds, params = {}) {
|
|
571
|
+
const {
|
|
572
|
+
tolerance = 0.01,
|
|
573
|
+
name = 'Stitched Surface',
|
|
574
|
+
} = params;
|
|
575
|
+
|
|
576
|
+
const surfaces = surfaceState.surfaces.filter(s => surfaceIds.includes(s.id));
|
|
577
|
+
|
|
578
|
+
if (surfaces.length < 2) {
|
|
579
|
+
return { success: false, message: 'Stitch requires at least 2 surfaces' };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Merge geometries
|
|
583
|
+
const geometries = surfaces.map(s => s.geometry);
|
|
584
|
+
const mergedGeometry = THREE.BufferGeometryUtils?.mergeGeometries(geometries);
|
|
585
|
+
|
|
586
|
+
if (!mergedGeometry) {
|
|
587
|
+
return { success: false, message: 'Cannot merge surface geometries' };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
mergedGeometry.computeVertexNormals();
|
|
591
|
+
|
|
592
|
+
const surface = new Surface(`surface_${nextSurfaceId++}`, mergedGeometry, name, 'stitched');
|
|
593
|
+
surfaceState.surfaces.push(surface);
|
|
594
|
+
|
|
595
|
+
surfaceState.features.push({
|
|
596
|
+
type: SURFACE_OPERATIONS.STITCH,
|
|
597
|
+
params: { tolerance, surfaceCount: surfaces.length },
|
|
598
|
+
surfaceIds,
|
|
599
|
+
resultSurfaceId: surface.id,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
return { success: true, surface };
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Unstitch surface (split back into components)
|
|
607
|
+
*/
|
|
608
|
+
unstitchSurface(surfaceId, params = {}) {
|
|
609
|
+
const surface = surfaceState.surfaces.find(s => s.id === surfaceId);
|
|
610
|
+
|
|
611
|
+
if (!surface) {
|
|
612
|
+
return { success: false, message: `Surface ${surfaceId} not found` };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Create separate surfaces from merged geometry
|
|
616
|
+
// Simplified: clone and mark for separation
|
|
617
|
+
const parts = [];
|
|
618
|
+
for (let i = 0; i < 2; i++) {
|
|
619
|
+
const clonedGeo = surface.geometry.clone();
|
|
620
|
+
const part = new Surface(
|
|
621
|
+
`surface_${nextSurfaceId++}`,
|
|
622
|
+
clonedGeo,
|
|
623
|
+
`${surface.name}_Part${i + 1}`,
|
|
624
|
+
'unstitch'
|
|
625
|
+
);
|
|
626
|
+
surfaceState.surfaces.push(part);
|
|
627
|
+
parts.push(part);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
surfaceState.features.push({
|
|
631
|
+
type: SURFACE_OPERATIONS.UNSTITCH,
|
|
632
|
+
params: {},
|
|
633
|
+
sourceSurfaceId: surfaceId,
|
|
634
|
+
resultSurfaceIds: parts.map(p => p.id),
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
return { success: true, parts };
|
|
638
|
+
},
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Trim surface with tool surface
|
|
642
|
+
*/
|
|
643
|
+
trimSurface(surfaceId, toolSurfaceId, params = {}) {
|
|
644
|
+
const {
|
|
645
|
+
removeInside = true,
|
|
646
|
+
name = 'Trimmed Surface',
|
|
647
|
+
} = params;
|
|
648
|
+
|
|
649
|
+
const surface = surfaceState.surfaces.find(s => s.id === surfaceId);
|
|
650
|
+
const toolSurface = surfaceState.surfaces.find(s => s.id === toolSurfaceId);
|
|
651
|
+
|
|
652
|
+
if (!surface || !toolSurface) {
|
|
653
|
+
return { success: false, message: 'Surfaces not found' };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const geometry = surface.geometry.clone();
|
|
657
|
+
surface.trimmedRegions.push({
|
|
658
|
+
toolSurfaceId,
|
|
659
|
+
removeInside,
|
|
660
|
+
timestamp: Date.now(),
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
const newSurface = new Surface(`surface_${nextSurfaceId++}`, geometry, name, 'trimmed');
|
|
664
|
+
surfaceState.surfaces.push(newSurface);
|
|
665
|
+
|
|
666
|
+
surfaceState.features.push({
|
|
667
|
+
type: SURFACE_OPERATIONS.TRIM,
|
|
668
|
+
params: { removeInside },
|
|
669
|
+
surfaceId,
|
|
670
|
+
toolSurfaceId,
|
|
671
|
+
resultSurfaceId: newSurface.id,
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
return { success: true, surface: newSurface };
|
|
675
|
+
},
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Untrim surface (restore trimmed regions)
|
|
679
|
+
*/
|
|
680
|
+
untrimSurface(surfaceId, params = {}) {
|
|
681
|
+
const surface = surfaceState.surfaces.find(s => s.id === surfaceId);
|
|
682
|
+
|
|
683
|
+
if (!surface) {
|
|
684
|
+
return { success: false, message: `Surface ${surfaceId} not found` };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Restore original geometry
|
|
688
|
+
const restoredGeometry = surface.originalGeometry.clone();
|
|
689
|
+
surface.geometry = restoredGeometry;
|
|
690
|
+
surface.trimmedRegions = [];
|
|
691
|
+
|
|
692
|
+
surfaceState.features.push({
|
|
693
|
+
type: SURFACE_OPERATIONS.UNTRIM,
|
|
694
|
+
params: {},
|
|
695
|
+
surfaceId,
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
return { success: true, surface };
|
|
699
|
+
},
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Extend surface
|
|
703
|
+
*/
|
|
704
|
+
extendSurface(surfaceId, params = {}) {
|
|
705
|
+
const {
|
|
706
|
+
distance = 5,
|
|
707
|
+
direction = 'U', // 'U' or 'V' parameter direction
|
|
708
|
+
name = 'Extended Surface',
|
|
709
|
+
} = params;
|
|
710
|
+
|
|
711
|
+
const surface = surfaceState.surfaces.find(s => s.id === surfaceId);
|
|
712
|
+
|
|
713
|
+
if (!surface) {
|
|
714
|
+
return { success: false, message: `Surface ${surfaceId} not found` };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const geometry = surface.geometry.clone();
|
|
718
|
+
const positions = geometry.attributes.position.array;
|
|
719
|
+
|
|
720
|
+
// Extend boundary in given direction
|
|
721
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
722
|
+
if (direction === 'U') {
|
|
723
|
+
positions[i] += distance * 0.1;
|
|
724
|
+
} else if (direction === 'V') {
|
|
725
|
+
positions[i + 1] += distance * 0.1;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
geometry.computeVertexNormals();
|
|
730
|
+
|
|
731
|
+
const extSurface = new Surface(`surface_${nextSurfaceId++}`, geometry, name, 'extended');
|
|
732
|
+
surfaceState.surfaces.push(extSurface);
|
|
733
|
+
|
|
734
|
+
surfaceState.features.push({
|
|
735
|
+
type: SURFACE_OPERATIONS.EXTEND,
|
|
736
|
+
params: { distance, direction },
|
|
737
|
+
surfaceId,
|
|
738
|
+
resultSurfaceId: extSurface.id,
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
return { success: true, surface: extSurface };
|
|
742
|
+
},
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Sculpt surface with T-spline style control cage
|
|
746
|
+
*/
|
|
747
|
+
sculptSurface(surfaceId) {
|
|
748
|
+
const surface = surfaceState.surfaces.find(s => s.id === surfaceId);
|
|
749
|
+
|
|
750
|
+
if (!surface) {
|
|
751
|
+
return { success: false, message: `Surface ${surfaceId} not found` };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
surfaceState.sculptMode = true;
|
|
755
|
+
surfaceState.selectedSurface = surfaceId;
|
|
756
|
+
surfaceState.controlCage = surface.controlCagePoints;
|
|
757
|
+
|
|
758
|
+
surface.showControlCage(true);
|
|
759
|
+
|
|
760
|
+
return {
|
|
761
|
+
success: true,
|
|
762
|
+
message: 'Sculpt mode enabled',
|
|
763
|
+
controlPointCount: surface.controlPoints.length,
|
|
764
|
+
};
|
|
765
|
+
},
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Exit sculpt mode and update surface
|
|
769
|
+
*/
|
|
770
|
+
finishSculpt(params = {}) {
|
|
771
|
+
if (!surfaceState.sculptMode) {
|
|
772
|
+
return { success: false, message: 'Not in sculpt mode' };
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const surface = surfaceState.surfaces.find(s => s.id === surfaceState.selectedSurface);
|
|
776
|
+
if (surface) {
|
|
777
|
+
surface.showControlCage(false);
|
|
778
|
+
surface.geometry.computeVertexNormals();
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
surfaceState.sculptMode = false;
|
|
782
|
+
surfaceState.selectedSurface = null;
|
|
783
|
+
surfaceState.controlCage = null;
|
|
784
|
+
|
|
785
|
+
return { success: true, message: 'Sculpt mode finished' };
|
|
786
|
+
},
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Create ruled surface between two edges/curves
|
|
790
|
+
*/
|
|
791
|
+
ruledSurface(edge1Geometry, edge2Geometry, params = {}) {
|
|
792
|
+
const {
|
|
793
|
+
name = 'Ruled Surface',
|
|
794
|
+
} = params;
|
|
795
|
+
|
|
796
|
+
const geometry = new THREE.BufferGeometry();
|
|
797
|
+
const edge1Pos = edge1Geometry.attributes.position.array;
|
|
798
|
+
const edge2Pos = edge2Geometry.attributes.position.array;
|
|
799
|
+
|
|
800
|
+
const ruledPositions = [];
|
|
801
|
+
const steps1 = edge1Pos.length / 3;
|
|
802
|
+
const steps2 = edge2Pos.length / 3;
|
|
803
|
+
const maxSteps = Math.max(steps1, steps2);
|
|
804
|
+
|
|
805
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
806
|
+
const t = i / maxSteps;
|
|
807
|
+
const idx1 = Math.min(i * 3, edge1Pos.length - 3);
|
|
808
|
+
const idx2 = Math.min(i * 3, edge2Pos.length - 3);
|
|
809
|
+
|
|
810
|
+
// Interpolate between edges
|
|
811
|
+
const x = edge1Pos[idx1] + (edge2Pos[idx2] - edge1Pos[idx1]) * t;
|
|
812
|
+
const y = edge1Pos[idx1 + 1] + (edge2Pos[idx2 + 1] - edge1Pos[idx1 + 1]) * t;
|
|
813
|
+
const z = edge1Pos[idx1 + 2] + (edge2Pos[idx2 + 2] - edge1Pos[idx1 + 2]) * t;
|
|
814
|
+
|
|
815
|
+
ruledPositions.push(x, y, z);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
geometry.setAttribute(
|
|
819
|
+
'position',
|
|
820
|
+
new THREE.BufferAttribute(new Float32Array(ruledPositions), 3)
|
|
821
|
+
);
|
|
822
|
+
geometry.computeVertexNormals();
|
|
823
|
+
|
|
824
|
+
const surface = new Surface(`surface_${nextSurfaceId++}`, geometry, name, 'ruled');
|
|
825
|
+
surfaceState.surfaces.push(surface);
|
|
826
|
+
|
|
827
|
+
surfaceState.features.push({
|
|
828
|
+
type: SURFACE_OPERATIONS.RULED,
|
|
829
|
+
params: {},
|
|
830
|
+
surfaceId: surface.id,
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
return { success: true, surface };
|
|
834
|
+
},
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Get all surfaces
|
|
838
|
+
*/
|
|
839
|
+
getSurfaces() {
|
|
840
|
+
return surfaceState.surfaces;
|
|
841
|
+
},
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Get all features
|
|
845
|
+
*/
|
|
846
|
+
getFeatures() {
|
|
847
|
+
return surfaceState.features;
|
|
848
|
+
},
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Get UI panel
|
|
852
|
+
*/
|
|
853
|
+
getUI() {
|
|
854
|
+
const operations = Object.keys(SURFACE_OPERATIONS)
|
|
855
|
+
.map(
|
|
856
|
+
op =>
|
|
857
|
+
`<button data-surface-op="${SURFACE_OPERATIONS[op]}" style="padding:4px 8px;margin:2px;background:#10b981;color:white;border:none;border-radius:2px;cursor:pointer;">${op}</button>`
|
|
858
|
+
)
|
|
859
|
+
.join('');
|
|
860
|
+
|
|
861
|
+
return `
|
|
862
|
+
<div id="surface-panel" style="padding:12px;background:#252526;border-radius:4px;color:#e0e0e0;font-size:12px;">
|
|
863
|
+
<h3>Surface Operations</h3>
|
|
864
|
+
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px;">
|
|
865
|
+
${operations}
|
|
866
|
+
</div>
|
|
867
|
+
|
|
868
|
+
<div id="surface-list" style="margin-top:12px;padding:8px;background:#1e1e1e;border-radius:2px;max-height:200px;overflow-y:auto;">
|
|
869
|
+
<h4>Surfaces (${surfaceState.surfaces.length})</h4>
|
|
870
|
+
${surfaceState.surfaces
|
|
871
|
+
.map(
|
|
872
|
+
s =>
|
|
873
|
+
`<div style="padding:4px;margin:2px;background:#2d2d30;border-left:3px solid #10b981;cursor:pointer;" data-surface-id="${s.id}">${s.name}</div>`
|
|
874
|
+
)
|
|
875
|
+
.join('')}
|
|
876
|
+
</div>
|
|
877
|
+
|
|
878
|
+
<div id="surface-features" style="margin-top:12px;padding:8px;background:#1e1e1e;border-radius:2px;max-height:150px;overflow-y:auto;">
|
|
879
|
+
<h4>Features (${surfaceState.features.length})</h4>
|
|
880
|
+
${surfaceState.features
|
|
881
|
+
.map((f, i) => `<div style="padding:4px;margin:2px;background:#2d2d30;">${f.type} #${i}</div>`)
|
|
882
|
+
.join('')}
|
|
883
|
+
</div>
|
|
884
|
+
|
|
885
|
+
${surfaceState.sculptMode
|
|
886
|
+
? `<button id="finish-sculpt" style="width:100%;padding:8px;margin-top:12px;background:#ef4444;color:white;border:none;border-radius:2px;cursor:pointer;">Finish Sculpt</button>`
|
|
887
|
+
: ''}
|
|
888
|
+
</div>
|
|
889
|
+
`;
|
|
890
|
+
},
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Execute surface command via agent API
|
|
894
|
+
*/
|
|
895
|
+
async execute(command, params = {}) {
|
|
896
|
+
switch (command) {
|
|
897
|
+
case 'extrudeSurface':
|
|
898
|
+
return this.extrudeSurface(params.geometry, params);
|
|
899
|
+
|
|
900
|
+
case 'revolveSurface':
|
|
901
|
+
return this.revolveSurface(params.geometry, params.axis, params);
|
|
902
|
+
|
|
903
|
+
case 'sweepSurface':
|
|
904
|
+
return this.sweepSurface(params.profileGeometry, params.pathGeometry, params);
|
|
905
|
+
|
|
906
|
+
case 'loftSurface':
|
|
907
|
+
return this.loftSurface(params.profileGeometries, params);
|
|
908
|
+
|
|
909
|
+
case 'patch':
|
|
910
|
+
return this.patch(params.boundaryCurves, params);
|
|
911
|
+
|
|
912
|
+
case 'offsetSurface':
|
|
913
|
+
return this.offsetSurface(params.geometry, params);
|
|
914
|
+
|
|
915
|
+
case 'stitchSurfaces':
|
|
916
|
+
return this.stitchSurfaces(params.surfaceIds, params);
|
|
917
|
+
|
|
918
|
+
case 'unstitchSurface':
|
|
919
|
+
return this.unstitchSurface(params.surfaceId, params);
|
|
920
|
+
|
|
921
|
+
case 'trimSurface':
|
|
922
|
+
return this.trimSurface(params.surfaceId, params.toolSurfaceId, params);
|
|
923
|
+
|
|
924
|
+
case 'untrimSurface':
|
|
925
|
+
return this.untrimSurface(params.surfaceId, params);
|
|
926
|
+
|
|
927
|
+
case 'extendSurface':
|
|
928
|
+
return this.extendSurface(params.surfaceId, params);
|
|
929
|
+
|
|
930
|
+
case 'sculptSurface':
|
|
931
|
+
return this.sculptSurface(params.surfaceId);
|
|
932
|
+
|
|
933
|
+
case 'finishSculpt':
|
|
934
|
+
return this.finishSculpt(params);
|
|
935
|
+
|
|
936
|
+
case 'ruledSurface':
|
|
937
|
+
return this.ruledSurface(params.edge1Geometry, params.edge2Geometry, params);
|
|
938
|
+
|
|
939
|
+
case 'getSurfaces':
|
|
940
|
+
return { success: true, surfaces: this.getSurfaces() };
|
|
941
|
+
|
|
942
|
+
case 'getFeatures':
|
|
943
|
+
return { success: true, features: this.getFeatures() };
|
|
944
|
+
|
|
945
|
+
default:
|
|
946
|
+
return { success: false, message: `Unknown command: ${command}` };
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
};
|