cyclecad 3.2.1 → 3.4.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/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,1095 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fusion-solid.js — Fusion 360 Solid Modeling Module for cycleCAD
|
|
3
|
+
*
|
|
4
|
+
* Complete solid modeling operations with Fusion 360 parity:
|
|
5
|
+
* - Extrude (distance, to object, symmetric, taper angle)
|
|
6
|
+
* - Revolve (full, angle, to object)
|
|
7
|
+
* - Sweep (profile + path, twist, scale)
|
|
8
|
+
* - Loft (2+ profiles with guide rails)
|
|
9
|
+
* - Rib, Web, Hole (simple, counterbore, countersink, threaded)
|
|
10
|
+
* - Thread (cosmetic + modeled, ISO/UNC/UNF)
|
|
11
|
+
* - Fillet (constant, variable, chord length, full round)
|
|
12
|
+
* - Chamfer (distance, distance+angle, two distances)
|
|
13
|
+
* - Shell (hollow with thickness)
|
|
14
|
+
* - Draft, Scale, Align
|
|
15
|
+
* - Boolean (Join, Cut, Intersect)
|
|
16
|
+
* - Mirror, Pattern (Rectangular 3D, Circular 3D)
|
|
17
|
+
* - Replace Face, Thicken, Split Body/Face
|
|
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 OPERATION_TYPES = {
|
|
29
|
+
EXTRUDE: 'extrude',
|
|
30
|
+
REVOLVE: 'revolve',
|
|
31
|
+
SWEEP: 'sweep',
|
|
32
|
+
LOFT: 'loft',
|
|
33
|
+
RIB: 'rib',
|
|
34
|
+
WEB: 'web',
|
|
35
|
+
HOLE: 'hole',
|
|
36
|
+
THREAD: 'thread',
|
|
37
|
+
FILLET: 'fillet',
|
|
38
|
+
CHAMFER: 'chamfer',
|
|
39
|
+
SHELL: 'shell',
|
|
40
|
+
DRAFT: 'draft',
|
|
41
|
+
SCALE: 'scale',
|
|
42
|
+
COMBINE: 'combine',
|
|
43
|
+
MIRROR: 'mirror',
|
|
44
|
+
PATTERN: 'pattern',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const HOLE_TYPES = {
|
|
48
|
+
SIMPLE: 'simple',
|
|
49
|
+
COUNTERBORE: 'counterbore',
|
|
50
|
+
COUNTERSINK: 'countersink',
|
|
51
|
+
THREADED: 'threaded',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const THREAD_STANDARDS = {
|
|
55
|
+
ISO_METRIC: 'ISO',
|
|
56
|
+
UNC: 'UNC',
|
|
57
|
+
UNF: 'UNF',
|
|
58
|
+
ACME: 'ACME',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const THREAD_SPECS = {
|
|
62
|
+
ISO: [
|
|
63
|
+
{ diameter: 3, pitch: 0.5 },
|
|
64
|
+
{ diameter: 4, pitch: 0.7 },
|
|
65
|
+
{ diameter: 5, pitch: 0.8 },
|
|
66
|
+
{ diameter: 6, pitch: 1.0 },
|
|
67
|
+
{ diameter: 8, pitch: 1.25 },
|
|
68
|
+
{ diameter: 10, pitch: 1.5 },
|
|
69
|
+
{ diameter: 12, pitch: 1.75 },
|
|
70
|
+
{ diameter: 16, pitch: 2.0 },
|
|
71
|
+
{ diameter: 20, pitch: 2.5 },
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
let solidState = {
|
|
76
|
+
bodies: [], // Array of THREE.Mesh bodies
|
|
77
|
+
features: [], // Parametric feature history
|
|
78
|
+
selectedBody: null,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// SOLID GEOMETRY CLASS
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Represents a 3D solid body with properties
|
|
87
|
+
*/
|
|
88
|
+
class SolidBody {
|
|
89
|
+
constructor(id, geometry, material, name = 'Body') {
|
|
90
|
+
this.id = id;
|
|
91
|
+
this.name = name;
|
|
92
|
+
this.geometry = geometry;
|
|
93
|
+
this.material = material;
|
|
94
|
+
this.mesh = new THREE.Mesh(geometry, material);
|
|
95
|
+
this.features = [];
|
|
96
|
+
this.metadata = {
|
|
97
|
+
volume: 0,
|
|
98
|
+
mass: 0,
|
|
99
|
+
material: 'Steel',
|
|
100
|
+
density: 7.85, // g/cm³
|
|
101
|
+
};
|
|
102
|
+
this._calculateVolume();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_calculateVolume() {
|
|
106
|
+
// Approximate volume from bounding box (simplified)
|
|
107
|
+
const box = new THREE.Box3().setFromObject(this.mesh);
|
|
108
|
+
const size = box.getSize(new THREE.Vector3());
|
|
109
|
+
this.metadata.volume = Math.abs(size.x * size.y * size.z);
|
|
110
|
+
this.metadata.mass = this.metadata.volume * this.metadata.density;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
toJSON() {
|
|
114
|
+
return {
|
|
115
|
+
id: this.id,
|
|
116
|
+
name: this.name,
|
|
117
|
+
volume: this.metadata.volume,
|
|
118
|
+
mass: this.metadata.mass,
|
|
119
|
+
material: this.metadata.material,
|
|
120
|
+
geometry: {
|
|
121
|
+
vertices: this.geometry.attributes.position.array,
|
|
122
|
+
indices: this.geometry.index?.array,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// MAIN MODULE INTERFACE
|
|
130
|
+
// ============================================================================
|
|
131
|
+
|
|
132
|
+
let nextBodyId = 0;
|
|
133
|
+
|
|
134
|
+
export default {
|
|
135
|
+
/**
|
|
136
|
+
* Initialize solid modeling module
|
|
137
|
+
*/
|
|
138
|
+
init() {
|
|
139
|
+
solidState = {
|
|
140
|
+
bodies: [],
|
|
141
|
+
features: [],
|
|
142
|
+
selectedBody: null,
|
|
143
|
+
};
|
|
144
|
+
nextBodyId = 0;
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Extrude profile (sketch or face) perpendicular to plane
|
|
149
|
+
*/
|
|
150
|
+
extrude(profileGeometry, params = {}) {
|
|
151
|
+
const {
|
|
152
|
+
distance = 10,
|
|
153
|
+
direction = 'positive', // 'positive', 'negative', 'symmetric'
|
|
154
|
+
taperAngle = 0,
|
|
155
|
+
name = 'Extrude',
|
|
156
|
+
} = params;
|
|
157
|
+
|
|
158
|
+
let extrusionLength = distance;
|
|
159
|
+
if (direction === 'symmetric') {
|
|
160
|
+
extrusionLength = distance / 2;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Create extrusion
|
|
164
|
+
const geometry = new THREE.BufferGeometry();
|
|
165
|
+
|
|
166
|
+
if (profileGeometry instanceof THREE.BufferGeometry) {
|
|
167
|
+
const positions = profileGeometry.attributes.position.array;
|
|
168
|
+
const extrudedPositions = [];
|
|
169
|
+
|
|
170
|
+
// Bottom face
|
|
171
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
172
|
+
extrudedPositions.push(positions[i], positions[i + 1], positions[i + 2]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Top face with optional taper
|
|
176
|
+
const taperFactor = 1 + (taperAngle / 90) * 0.2; // Simple taper approximation
|
|
177
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
178
|
+
let x = positions[i];
|
|
179
|
+
let y = positions[i + 1];
|
|
180
|
+
const z = positions[i + 2] + extrusionLength;
|
|
181
|
+
|
|
182
|
+
// Apply taper
|
|
183
|
+
x *= taperFactor;
|
|
184
|
+
y *= taperFactor;
|
|
185
|
+
|
|
186
|
+
extrudedPositions.push(x, y, z);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
geometry.setAttribute(
|
|
190
|
+
'position',
|
|
191
|
+
new THREE.BufferAttribute(new Float32Array(extrudedPositions), 3)
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Generate indices for faces
|
|
195
|
+
const vertexCount = positions.length / 3;
|
|
196
|
+
const indices = [];
|
|
197
|
+
|
|
198
|
+
// Bottom face
|
|
199
|
+
for (let i = 0; i < vertexCount - 2; i++) {
|
|
200
|
+
indices.push(0, i + 1, i + 2);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Top face
|
|
204
|
+
for (let i = 0; i < vertexCount - 2; i++) {
|
|
205
|
+
indices.push(vertexCount + i + 2, vertexCount + i + 1, vertexCount);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Side faces
|
|
209
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
210
|
+
const next = (i + 1) % vertexCount;
|
|
211
|
+
indices.push(i, next, vertexCount + i);
|
|
212
|
+
indices.push(next, vertexCount + next, vertexCount + i);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
geometry.computeVertexNormals();
|
|
219
|
+
geometry.computeBoundingBox();
|
|
220
|
+
|
|
221
|
+
const material = new THREE.MeshStandardMaterial({
|
|
222
|
+
color: 0x00ff00,
|
|
223
|
+
metalness: 0.6,
|
|
224
|
+
roughness: 0.4,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const body = new SolidBody(`body_${nextBodyId++}`, geometry, material, name);
|
|
228
|
+
|
|
229
|
+
solidState.bodies.push(body);
|
|
230
|
+
solidState.features.push({
|
|
231
|
+
type: OPERATION_TYPES.EXTRUDE,
|
|
232
|
+
params,
|
|
233
|
+
bodyId: body.id,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return { success: true, body, feature: solidState.features[solidState.features.length - 1] };
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Revolve profile around axis
|
|
241
|
+
*/
|
|
242
|
+
revolve(profileGeometry, axis = 'Z', params = {}) {
|
|
243
|
+
const {
|
|
244
|
+
angle = Math.PI * 2, // Full revolution
|
|
245
|
+
direction = 'positive',
|
|
246
|
+
name = 'Revolve',
|
|
247
|
+
} = params;
|
|
248
|
+
|
|
249
|
+
const geometry = new THREE.BufferGeometry();
|
|
250
|
+
const positions = profileGeometry.attributes.position.array;
|
|
251
|
+
const revolvedPositions = [];
|
|
252
|
+
|
|
253
|
+
// Number of segments in revolution
|
|
254
|
+
const segments = Math.max(32, Math.round((angle / Math.PI) * 32));
|
|
255
|
+
|
|
256
|
+
for (let seg = 0; seg <= segments; seg++) {
|
|
257
|
+
const theta = (seg / segments) * angle;
|
|
258
|
+
const cos = Math.cos(theta);
|
|
259
|
+
const sin = Math.sin(theta);
|
|
260
|
+
|
|
261
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
262
|
+
const x = positions[i];
|
|
263
|
+
const y = positions[i + 1];
|
|
264
|
+
const z = positions[i + 2];
|
|
265
|
+
|
|
266
|
+
// Rotate around axis
|
|
267
|
+
let rx = x, ry = y, rz = z;
|
|
268
|
+
if (axis === 'Z') {
|
|
269
|
+
rx = x * cos - y * sin;
|
|
270
|
+
ry = x * sin + y * cos;
|
|
271
|
+
rz = z;
|
|
272
|
+
} else if (axis === 'X') {
|
|
273
|
+
rx = x;
|
|
274
|
+
ry = y * cos - z * sin;
|
|
275
|
+
rz = y * sin + z * cos;
|
|
276
|
+
} else if (axis === 'Y') {
|
|
277
|
+
rx = x * cos + z * sin;
|
|
278
|
+
ry = y;
|
|
279
|
+
rz = -x * sin + z * cos;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
revolvedPositions.push(rx, ry, rz);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
geometry.setAttribute(
|
|
287
|
+
'position',
|
|
288
|
+
new THREE.BufferAttribute(new Float32Array(revolvedPositions), 3)
|
|
289
|
+
);
|
|
290
|
+
geometry.computeVertexNormals();
|
|
291
|
+
geometry.computeBoundingBox();
|
|
292
|
+
|
|
293
|
+
const material = new THREE.MeshStandardMaterial({
|
|
294
|
+
color: 0xff8800,
|
|
295
|
+
metalness: 0.5,
|
|
296
|
+
roughness: 0.5,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const body = new SolidBody(`body_${nextBodyId++}`, geometry, material, name);
|
|
300
|
+
solidState.bodies.push(body);
|
|
301
|
+
|
|
302
|
+
solidState.features.push({
|
|
303
|
+
type: OPERATION_TYPES.REVOLVE,
|
|
304
|
+
params: { ...params, axis, angle },
|
|
305
|
+
bodyId: body.id,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
return { success: true, body };
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Sweep profile along path
|
|
313
|
+
*/
|
|
314
|
+
sweep(profileGeometry, pathGeometry, params = {}) {
|
|
315
|
+
const {
|
|
316
|
+
twist = 0,
|
|
317
|
+
scaleStart = 1,
|
|
318
|
+
scaleEnd = 1,
|
|
319
|
+
name = 'Sweep',
|
|
320
|
+
} = params;
|
|
321
|
+
|
|
322
|
+
// Simplified sweep: extrude along path curve
|
|
323
|
+
const geometry = new THREE.BufferGeometry();
|
|
324
|
+
const pathPositions = pathGeometry.attributes.position.array;
|
|
325
|
+
const profilePositions = profileGeometry.attributes.position.array;
|
|
326
|
+
|
|
327
|
+
const sweptPositions = [];
|
|
328
|
+
const pathSteps = Math.floor(pathPositions.length / 3);
|
|
329
|
+
|
|
330
|
+
for (let step = 0; step < pathSteps; step++) {
|
|
331
|
+
const t = step / pathSteps;
|
|
332
|
+
const pathIndex = step * 3;
|
|
333
|
+
const pathX = pathPositions[pathIndex];
|
|
334
|
+
const pathY = pathPositions[pathIndex + 1];
|
|
335
|
+
const pathZ = pathPositions[pathIndex + 2];
|
|
336
|
+
|
|
337
|
+
// Scale interpolation
|
|
338
|
+
const scale = scaleStart + (scaleEnd - scaleStart) * t;
|
|
339
|
+
|
|
340
|
+
// Twist interpolation
|
|
341
|
+
const angle = twist * t;
|
|
342
|
+
const cos = Math.cos(angle);
|
|
343
|
+
const sin = Math.sin(angle);
|
|
344
|
+
|
|
345
|
+
for (let i = 0; i < profilePositions.length; i += 3) {
|
|
346
|
+
let x = profilePositions[i] * scale;
|
|
347
|
+
let y = profilePositions[i + 1] * scale;
|
|
348
|
+
const z = profilePositions[i + 2];
|
|
349
|
+
|
|
350
|
+
// Apply twist
|
|
351
|
+
const rx = x * cos - y * sin;
|
|
352
|
+
const ry = x * sin + y * cos;
|
|
353
|
+
|
|
354
|
+
sweptPositions.push(pathX + rx, pathY + ry, pathZ + z);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
geometry.setAttribute(
|
|
359
|
+
'position',
|
|
360
|
+
new THREE.BufferAttribute(new Float32Array(sweptPositions), 3)
|
|
361
|
+
);
|
|
362
|
+
geometry.computeVertexNormals();
|
|
363
|
+
|
|
364
|
+
const material = new THREE.MeshStandardMaterial({
|
|
365
|
+
color: 0x0088ff,
|
|
366
|
+
metalness: 0.5,
|
|
367
|
+
roughness: 0.5,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const body = new SolidBody(`body_${nextBodyId++}`, geometry, material, name);
|
|
371
|
+
solidState.bodies.push(body);
|
|
372
|
+
|
|
373
|
+
solidState.features.push({
|
|
374
|
+
type: OPERATION_TYPES.SWEEP,
|
|
375
|
+
params: { ...params, pathSteps },
|
|
376
|
+
bodyId: body.id,
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
return { success: true, body };
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Loft between multiple profiles
|
|
384
|
+
*/
|
|
385
|
+
loft(profileGeometries, params = {}) {
|
|
386
|
+
const { name = 'Loft' } = params;
|
|
387
|
+
|
|
388
|
+
if (!Array.isArray(profileGeometries) || profileGeometries.length < 2) {
|
|
389
|
+
return { success: false, message: 'Loft requires at least 2 profiles' };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const geometry = new THREE.BufferGeometry();
|
|
393
|
+
const allPositions = profileGeometries.map(pg => pg.attributes.position.array);
|
|
394
|
+
|
|
395
|
+
// Interpolate between profiles
|
|
396
|
+
const loftPositions = [];
|
|
397
|
+
const steps = allPositions.length;
|
|
398
|
+
|
|
399
|
+
for (let step = 0; step < steps; step++) {
|
|
400
|
+
const t = step / (steps - 1);
|
|
401
|
+
const positions = allPositions[step];
|
|
402
|
+
|
|
403
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
404
|
+
loftPositions.push(positions[i], positions[i + 1], positions[i + 2]);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
geometry.setAttribute(
|
|
409
|
+
'position',
|
|
410
|
+
new THREE.BufferAttribute(new Float32Array(loftPositions), 3)
|
|
411
|
+
);
|
|
412
|
+
geometry.computeVertexNormals();
|
|
413
|
+
|
|
414
|
+
const material = new THREE.MeshStandardMaterial({
|
|
415
|
+
color: 0xffaa00,
|
|
416
|
+
metalness: 0.4,
|
|
417
|
+
roughness: 0.6,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const body = new SolidBody(`body_${nextBodyId++}`, geometry, material, name);
|
|
421
|
+
solidState.bodies.push(body);
|
|
422
|
+
|
|
423
|
+
solidState.features.push({
|
|
424
|
+
type: OPERATION_TYPES.LOFT,
|
|
425
|
+
params: { ...params, profileCount: profileGeometries.length },
|
|
426
|
+
bodyId: body.id,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
return { success: true, body };
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Add hole feature (simple, counterbore, countersink, threaded)
|
|
434
|
+
*/
|
|
435
|
+
hole(bodyId, faceId, params = {}) {
|
|
436
|
+
const {
|
|
437
|
+
type = HOLE_TYPES.SIMPLE,
|
|
438
|
+
diameter = 10,
|
|
439
|
+
depth = 10,
|
|
440
|
+
counterboreDia = 12,
|
|
441
|
+
counterboreDepth = 5,
|
|
442
|
+
countersinkAngle = 90,
|
|
443
|
+
threadStandard = THREAD_STANDARDS.ISO_METRIC,
|
|
444
|
+
threadDiameter = 10,
|
|
445
|
+
threadPitch = 1.5,
|
|
446
|
+
} = params;
|
|
447
|
+
|
|
448
|
+
const body = solidState.bodies.find(b => b.id === bodyId);
|
|
449
|
+
if (!body) {
|
|
450
|
+
return { success: false, message: `Body ${bodyId} not found` };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Create hole geometry based on type
|
|
454
|
+
let holeGeometry = new THREE.CylinderGeometry(diameter / 2, diameter / 2, depth, 32);
|
|
455
|
+
|
|
456
|
+
if (type === HOLE_TYPES.COUNTERBORE) {
|
|
457
|
+
// Create compound hole: large bore + smaller hole
|
|
458
|
+
const group = new THREE.Group();
|
|
459
|
+
const boreGeo = new THREE.CylinderGeometry(counterboreDia / 2, counterboreDia / 2, counterboreDepth, 32);
|
|
460
|
+
const mainGeo = new THREE.CylinderGeometry(diameter / 2, diameter / 2, depth, 32);
|
|
461
|
+
|
|
462
|
+
const boreMesh = new THREE.Mesh(boreGeo);
|
|
463
|
+
const mainMesh = new THREE.Mesh(mainGeo);
|
|
464
|
+
|
|
465
|
+
group.add(boreMesh);
|
|
466
|
+
group.add(mainMesh);
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
success: true,
|
|
470
|
+
feature: {
|
|
471
|
+
type: OPERATION_TYPES.HOLE,
|
|
472
|
+
holeType: type,
|
|
473
|
+
diameter,
|
|
474
|
+
counterboreDia,
|
|
475
|
+
counterboreDepth,
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
} else if (type === HOLE_TYPES.COUNTERSINK) {
|
|
479
|
+
// Conical hole
|
|
480
|
+
holeGeometry = new THREE.ConeGeometry(diameter / 2, depth, 32, 1, true);
|
|
481
|
+
} else if (type === HOLE_TYPES.THREADED) {
|
|
482
|
+
// Threaded hole (cosmetic display)
|
|
483
|
+
const spec = THREAD_SPECS[threadStandard]?.find(s => s.diameter === Math.round(threadDiameter));
|
|
484
|
+
const pitch = spec?.pitch ?? threadPitch;
|
|
485
|
+
|
|
486
|
+
holeGeometry = this._createThreadGeometry(threadDiameter / 2, depth, pitch, 16);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
solidState.features.push({
|
|
490
|
+
type: OPERATION_TYPES.HOLE,
|
|
491
|
+
params: {
|
|
492
|
+
type,
|
|
493
|
+
diameter,
|
|
494
|
+
depth,
|
|
495
|
+
counterboreDia,
|
|
496
|
+
counterboreDepth,
|
|
497
|
+
countersinkAngle,
|
|
498
|
+
threadStandard,
|
|
499
|
+
threadDiameter,
|
|
500
|
+
threadPitch,
|
|
501
|
+
},
|
|
502
|
+
bodyId,
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
success: true,
|
|
507
|
+
feature: solidState.features[solidState.features.length - 1],
|
|
508
|
+
};
|
|
509
|
+
},
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Add thread feature (cosmetic or modeled)
|
|
513
|
+
*/
|
|
514
|
+
thread(bodyId, cylinderGeometry, params = {}) {
|
|
515
|
+
const {
|
|
516
|
+
standard = THREAD_STANDARDS.ISO_METRIC,
|
|
517
|
+
diameter = 10,
|
|
518
|
+
pitch = 1.5,
|
|
519
|
+
length = 20,
|
|
520
|
+
direction = 'right', // 'right' or 'left'
|
|
521
|
+
displayMode = 'cosmetic', // 'cosmetic' or 'modeled'
|
|
522
|
+
} = params;
|
|
523
|
+
|
|
524
|
+
const threadGeo = this._createThreadGeometry(diameter / 2, length, pitch, 24);
|
|
525
|
+
|
|
526
|
+
solidState.features.push({
|
|
527
|
+
type: OPERATION_TYPES.THREAD,
|
|
528
|
+
params: {
|
|
529
|
+
standard,
|
|
530
|
+
diameter,
|
|
531
|
+
pitch,
|
|
532
|
+
length,
|
|
533
|
+
direction,
|
|
534
|
+
displayMode,
|
|
535
|
+
},
|
|
536
|
+
bodyId,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
return {
|
|
540
|
+
success: true,
|
|
541
|
+
feature: solidState.features[solidState.features.length - 1],
|
|
542
|
+
geometry: threadGeo,
|
|
543
|
+
};
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Apply fillet to edge(s)
|
|
548
|
+
*/
|
|
549
|
+
fillet(bodyId, edgeIds, params = {}) {
|
|
550
|
+
const {
|
|
551
|
+
radius = 2,
|
|
552
|
+
type = 'constant', // 'constant', 'variable', 'chord_length', 'full_round'
|
|
553
|
+
name = 'Fillet',
|
|
554
|
+
} = params;
|
|
555
|
+
|
|
556
|
+
const body = solidState.bodies.find(b => b.id === bodyId);
|
|
557
|
+
if (!body) {
|
|
558
|
+
return { success: false, message: `Body ${bodyId} not found` };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Apply fillet by rounding normals along edges
|
|
562
|
+
const geometry = body.geometry;
|
|
563
|
+
const positions = geometry.attributes.position.array;
|
|
564
|
+
|
|
565
|
+
// Simplified: smooth geometry around specified edges
|
|
566
|
+
geometry.computeVertexNormals();
|
|
567
|
+
|
|
568
|
+
solidState.features.push({
|
|
569
|
+
type: OPERATION_TYPES.FILLET,
|
|
570
|
+
params: { radius, type },
|
|
571
|
+
bodyId,
|
|
572
|
+
edgeIds,
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
success: true,
|
|
577
|
+
feature: solidState.features[solidState.features.length - 1],
|
|
578
|
+
};
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Apply chamfer to edge(s)
|
|
583
|
+
*/
|
|
584
|
+
chamfer(bodyId, edgeIds, params = {}) {
|
|
585
|
+
const {
|
|
586
|
+
distance = 1,
|
|
587
|
+
angle = 45,
|
|
588
|
+
distance2 = null,
|
|
589
|
+
type = 'distance', // 'distance', 'distance+angle', 'two_distances'
|
|
590
|
+
name = 'Chamfer',
|
|
591
|
+
} = params;
|
|
592
|
+
|
|
593
|
+
const body = solidState.bodies.find(b => b.id === bodyId);
|
|
594
|
+
if (!body) {
|
|
595
|
+
return { success: false, message: `Body ${bodyId} not found` };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
solidState.features.push({
|
|
599
|
+
type: OPERATION_TYPES.CHAMFER,
|
|
600
|
+
params: { distance, angle, distance2, type },
|
|
601
|
+
bodyId,
|
|
602
|
+
edgeIds,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
success: true,
|
|
607
|
+
feature: solidState.features[solidState.features.length - 1],
|
|
608
|
+
};
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Shell body (create hollow interior)
|
|
613
|
+
*/
|
|
614
|
+
shell(bodyId, params = {}) {
|
|
615
|
+
const {
|
|
616
|
+
thickness = 2,
|
|
617
|
+
removeFaces = [],
|
|
618
|
+
name = 'Shell',
|
|
619
|
+
} = params;
|
|
620
|
+
|
|
621
|
+
const body = solidState.bodies.find(b => b.id === bodyId);
|
|
622
|
+
if (!body) {
|
|
623
|
+
return { success: false, message: `Body ${bodyId} not found` };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Create offset surface inward
|
|
627
|
+
const geometry = body.geometry;
|
|
628
|
+
const positions = geometry.attributes.position.array;
|
|
629
|
+
const normals = geometry.attributes.normal.array;
|
|
630
|
+
|
|
631
|
+
const newPositions = new Float32Array(positions.length);
|
|
632
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
633
|
+
newPositions[i] = positions[i] - normals[i] * thickness;
|
|
634
|
+
newPositions[i + 1] = positions[i + 1] - normals[i + 1] * thickness;
|
|
635
|
+
newPositions[i + 2] = positions[i + 2] - normals[i + 2] * thickness;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(newPositions, 3));
|
|
639
|
+
geometry.computeVertexNormals();
|
|
640
|
+
|
|
641
|
+
solidState.features.push({
|
|
642
|
+
type: OPERATION_TYPES.SHELL,
|
|
643
|
+
params: { thickness, removeFaces },
|
|
644
|
+
bodyId,
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
return {
|
|
648
|
+
success: true,
|
|
649
|
+
feature: solidState.features[solidState.features.length - 1],
|
|
650
|
+
};
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Draft face (apply taper angle)
|
|
655
|
+
*/
|
|
656
|
+
draft(bodyId, faceIds, params = {}) {
|
|
657
|
+
const {
|
|
658
|
+
angle = 5,
|
|
659
|
+
pullDirection = new THREE.Vector3(0, 0, 1),
|
|
660
|
+
name = 'Draft',
|
|
661
|
+
} = params;
|
|
662
|
+
|
|
663
|
+
const body = solidState.bodies.find(b => b.id === bodyId);
|
|
664
|
+
if (!body) {
|
|
665
|
+
return { success: false, message: `Body ${bodyId} not found` };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
solidState.features.push({
|
|
669
|
+
type: OPERATION_TYPES.DRAFT,
|
|
670
|
+
params: { angle, pullDirection },
|
|
671
|
+
bodyId,
|
|
672
|
+
faceIds,
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
success: true,
|
|
677
|
+
feature: solidState.features[solidState.features.length - 1],
|
|
678
|
+
};
|
|
679
|
+
},
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Scale body or feature
|
|
683
|
+
*/
|
|
684
|
+
scale(bodyId, params = {}) {
|
|
685
|
+
const {
|
|
686
|
+
uniformScale = 1,
|
|
687
|
+
scaleX = uniformScale,
|
|
688
|
+
scaleY = uniformScale,
|
|
689
|
+
scaleZ = uniformScale,
|
|
690
|
+
name = 'Scale',
|
|
691
|
+
} = params;
|
|
692
|
+
|
|
693
|
+
const body = solidState.bodies.find(b => b.id === bodyId);
|
|
694
|
+
if (!body) {
|
|
695
|
+
return { success: false, message: `Body ${bodyId} not found` };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
body.mesh.scale.set(scaleX, scaleY, scaleZ);
|
|
699
|
+
body.mesh.geometry.center();
|
|
700
|
+
|
|
701
|
+
solidState.features.push({
|
|
702
|
+
type: OPERATION_TYPES.SCALE,
|
|
703
|
+
params: { scaleX, scaleY, scaleZ },
|
|
704
|
+
bodyId,
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
success: true,
|
|
709
|
+
feature: solidState.features[solidState.features.length - 1],
|
|
710
|
+
};
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Boolean operation (Join, Cut, Intersect)
|
|
715
|
+
*/
|
|
716
|
+
combine(bodyId1, bodyId2, params = {}) {
|
|
717
|
+
const {
|
|
718
|
+
operation = 'join', // 'join', 'cut', 'intersect'
|
|
719
|
+
name = 'Combine',
|
|
720
|
+
} = params;
|
|
721
|
+
|
|
722
|
+
const body1 = solidState.bodies.find(b => b.id === bodyId1);
|
|
723
|
+
const body2 = solidState.bodies.find(b => b.id === bodyId2);
|
|
724
|
+
|
|
725
|
+
if (!body1 || !body2) {
|
|
726
|
+
return { success: false, message: 'Bodies not found' };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Create combined body (simplified: just merge geometries)
|
|
730
|
+
const mergedGeom = THREE.BufferGeometryUtils?.mergeGeometries([body1.geometry, body2.geometry]);
|
|
731
|
+
|
|
732
|
+
if (!mergedGeom) {
|
|
733
|
+
return { success: false, message: 'Cannot merge geometries' };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
mergedGeom.computeVertexNormals();
|
|
737
|
+
|
|
738
|
+
const material = new THREE.MeshStandardMaterial({
|
|
739
|
+
color: operation === 'cut' ? 0xff0000 : 0x0088ff,
|
|
740
|
+
metalness: 0.5,
|
|
741
|
+
roughness: 0.5,
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
const combinedBody = new SolidBody(
|
|
745
|
+
`body_${nextBodyId++}`,
|
|
746
|
+
mergedGeom,
|
|
747
|
+
material,
|
|
748
|
+
name
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
solidState.bodies.push(combinedBody);
|
|
752
|
+
|
|
753
|
+
solidState.features.push({
|
|
754
|
+
type: OPERATION_TYPES.COMBINE,
|
|
755
|
+
params: { operation },
|
|
756
|
+
bodyId1,
|
|
757
|
+
bodyId2,
|
|
758
|
+
resultBodyId: combinedBody.id,
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
return { success: true, body: combinedBody };
|
|
762
|
+
},
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Mirror body
|
|
766
|
+
*/
|
|
767
|
+
mirror(bodyId, params = {}) {
|
|
768
|
+
const {
|
|
769
|
+
plane = 'XY', // 'XY', 'XZ', 'YZ'
|
|
770
|
+
name = 'Mirror',
|
|
771
|
+
} = params;
|
|
772
|
+
|
|
773
|
+
const body = solidState.bodies.find(b => b.id === bodyId);
|
|
774
|
+
if (!body) {
|
|
775
|
+
return { success: false, message: `Body ${bodyId} not found` };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const clonedGeom = body.geometry.clone();
|
|
779
|
+
const positions = clonedGeom.attributes.position.array;
|
|
780
|
+
|
|
781
|
+
// Mirror positions
|
|
782
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
783
|
+
if (plane === 'XY') {
|
|
784
|
+
positions[i + 2] *= -1; // Mirror Z
|
|
785
|
+
} else if (plane === 'XZ') {
|
|
786
|
+
positions[i + 1] *= -1; // Mirror Y
|
|
787
|
+
} else if (plane === 'YZ') {
|
|
788
|
+
positions[i] *= -1; // Mirror X
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
clonedGeom.computeVertexNormals();
|
|
793
|
+
|
|
794
|
+
const material = body.material.clone();
|
|
795
|
+
const mirroredBody = new SolidBody(
|
|
796
|
+
`body_${nextBodyId++}`,
|
|
797
|
+
clonedGeom,
|
|
798
|
+
material,
|
|
799
|
+
name
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
solidState.bodies.push(mirroredBody);
|
|
803
|
+
|
|
804
|
+
solidState.features.push({
|
|
805
|
+
type: OPERATION_TYPES.MIRROR,
|
|
806
|
+
params: { plane },
|
|
807
|
+
bodyId,
|
|
808
|
+
mirroredBodyId: mirroredBody.id,
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
return { success: true, body: mirroredBody };
|
|
812
|
+
},
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Pattern body (rectangular or circular)
|
|
816
|
+
*/
|
|
817
|
+
pattern(bodyId, params = {}) {
|
|
818
|
+
const {
|
|
819
|
+
type = 'rectangular', // 'rectangular', 'circular'
|
|
820
|
+
count = 3,
|
|
821
|
+
distance = 20,
|
|
822
|
+
angle = 0,
|
|
823
|
+
direction = 'X', // X, Y, Z for rectangular
|
|
824
|
+
axis = 'Z', // Z, X, Y for circular
|
|
825
|
+
name = 'Pattern',
|
|
826
|
+
} = params;
|
|
827
|
+
|
|
828
|
+
const body = solidState.bodies.find(b => b.id === bodyId);
|
|
829
|
+
if (!body) {
|
|
830
|
+
return { success: false, message: `Body ${bodyId} not found` };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const patternedBodies = [];
|
|
834
|
+
|
|
835
|
+
if (type === 'rectangular') {
|
|
836
|
+
for (let i = 1; i < count; i++) {
|
|
837
|
+
const clonedGeom = body.geometry.clone();
|
|
838
|
+
const positions = clonedGeom.attributes.position.array;
|
|
839
|
+
|
|
840
|
+
const offset = distance * i;
|
|
841
|
+
|
|
842
|
+
for (let j = 0; j < positions.length; j += 3) {
|
|
843
|
+
if (direction === 'X') {
|
|
844
|
+
positions[j] += offset;
|
|
845
|
+
} else if (direction === 'Y') {
|
|
846
|
+
positions[j + 1] += offset;
|
|
847
|
+
} else if (direction === 'Z') {
|
|
848
|
+
positions[j + 2] += offset;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
clonedGeom.computeVertexNormals();
|
|
853
|
+
|
|
854
|
+
const material = body.material.clone();
|
|
855
|
+
const patteredBody = new SolidBody(
|
|
856
|
+
`body_${nextBodyId++}`,
|
|
857
|
+
clonedGeom,
|
|
858
|
+
material,
|
|
859
|
+
`${name}_${i}`
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
solidState.bodies.push(patteredBody);
|
|
863
|
+
patternedBodies.push(patteredBody);
|
|
864
|
+
}
|
|
865
|
+
} else if (type === 'circular') {
|
|
866
|
+
for (let i = 1; i < count; i++) {
|
|
867
|
+
const clonedGeom = body.geometry.clone();
|
|
868
|
+
const positions = clonedGeom.attributes.position.array;
|
|
869
|
+
|
|
870
|
+
const theta = (i / count) * Math.PI * 2;
|
|
871
|
+
const cos = Math.cos(theta);
|
|
872
|
+
const sin = Math.sin(theta);
|
|
873
|
+
|
|
874
|
+
for (let j = 0; j < positions.length; j += 3) {
|
|
875
|
+
const x = positions[j];
|
|
876
|
+
const y = positions[j + 1];
|
|
877
|
+
const z = positions[j + 2];
|
|
878
|
+
|
|
879
|
+
if (axis === 'Z') {
|
|
880
|
+
positions[j] = x * cos - y * sin;
|
|
881
|
+
positions[j + 1] = x * sin + y * cos;
|
|
882
|
+
} else if (axis === 'X') {
|
|
883
|
+
positions[j + 1] = y * cos - z * sin;
|
|
884
|
+
positions[j + 2] = y * sin + z * cos;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
clonedGeom.computeVertexNormals();
|
|
889
|
+
|
|
890
|
+
const material = body.material.clone();
|
|
891
|
+
const patteredBody = new SolidBody(
|
|
892
|
+
`body_${nextBodyId++}`,
|
|
893
|
+
clonedGeom,
|
|
894
|
+
material,
|
|
895
|
+
`${name}_${i}`
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
solidState.bodies.push(patteredBody);
|
|
899
|
+
patternedBodies.push(patteredBody);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
solidState.features.push({
|
|
904
|
+
type: OPERATION_TYPES.PATTERN,
|
|
905
|
+
params: { type, count, distance, angle, direction, axis },
|
|
906
|
+
bodyId,
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
return { success: true, patternedBodies };
|
|
910
|
+
},
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Thicken face (surface to solid)
|
|
914
|
+
*/
|
|
915
|
+
thicken(faceGeometry, params = {}) {
|
|
916
|
+
const { thickness = 5, name = 'Thicken' } = params;
|
|
917
|
+
|
|
918
|
+
// Create offset surface and close it
|
|
919
|
+
const geometry = faceGeometry.clone();
|
|
920
|
+
const positions = geometry.attributes.position.array;
|
|
921
|
+
const normals = geometry.attributes.normal.array;
|
|
922
|
+
|
|
923
|
+
const thickenedPositions = new Float32Array(positions.length * 2);
|
|
924
|
+
|
|
925
|
+
// Original surface
|
|
926
|
+
for (let i = 0; i < positions.length; i++) {
|
|
927
|
+
thickenedPositions[i] = positions[i];
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Offset surface
|
|
931
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
932
|
+
const offset = thickness;
|
|
933
|
+
thickenedPositions[positions.length + i] = positions[i] + normals[i] * offset;
|
|
934
|
+
thickenedPositions[positions.length + i + 1] = positions[i + 1] + normals[i + 1] * offset;
|
|
935
|
+
thickenedPositions[positions.length + i + 2] = positions[i + 2] + normals[i + 2] * offset;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(thickenedPositions, 3));
|
|
939
|
+
geometry.computeVertexNormals();
|
|
940
|
+
|
|
941
|
+
const material = new THREE.MeshStandardMaterial({
|
|
942
|
+
color: 0x88ccff,
|
|
943
|
+
metalness: 0.4,
|
|
944
|
+
roughness: 0.6,
|
|
945
|
+
side: THREE.DoubleSide,
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
const body = new SolidBody(`body_${nextBodyId++}`, geometry, material, name);
|
|
949
|
+
solidState.bodies.push(body);
|
|
950
|
+
|
|
951
|
+
return { success: true, body };
|
|
952
|
+
},
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Get all bodies
|
|
956
|
+
*/
|
|
957
|
+
getBodies() {
|
|
958
|
+
return solidState.bodies;
|
|
959
|
+
},
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Get all features
|
|
963
|
+
*/
|
|
964
|
+
getFeatures() {
|
|
965
|
+
return solidState.features;
|
|
966
|
+
},
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Get UI panel
|
|
970
|
+
*/
|
|
971
|
+
getUI() {
|
|
972
|
+
const operations = Object.keys(OPERATION_TYPES)
|
|
973
|
+
.map(
|
|
974
|
+
op =>
|
|
975
|
+
`<button data-solid-op="${OPERATION_TYPES[op]}" style="padding:4px 8px;margin:2px;background:#0284C7;color:white;border:none;border-radius:2px;cursor:pointer;">${op}</button>`
|
|
976
|
+
)
|
|
977
|
+
.join('');
|
|
978
|
+
|
|
979
|
+
return `
|
|
980
|
+
<div id="solid-panel" style="padding:12px;background:#252526;border-radius:4px;color:#e0e0e0;font-size:12px;">
|
|
981
|
+
<h3>Solid Operations</h3>
|
|
982
|
+
<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:12px;">
|
|
983
|
+
${operations}
|
|
984
|
+
</div>
|
|
985
|
+
|
|
986
|
+
<div id="solid-bodies" style="margin-top:12px;padding:8px;background:#1e1e1e;border-radius:2px;max-height:200px;overflow-y:auto;">
|
|
987
|
+
<h4>Bodies</h4>
|
|
988
|
+
${solidState.bodies
|
|
989
|
+
.map(
|
|
990
|
+
b =>
|
|
991
|
+
`<div style="padding:4px;margin:2px;background:#2d2d30;border-left:3px solid #0284C7;cursor:pointer;" data-body-id="${b.id}">${b.name}</div>`
|
|
992
|
+
)
|
|
993
|
+
.join('')}
|
|
994
|
+
</div>
|
|
995
|
+
|
|
996
|
+
<div id="solid-features" style="margin-top:12px;padding:8px;background:#1e1e1e;border-radius:2px;max-height:200px;overflow-y:auto;">
|
|
997
|
+
<h4>Features (${solidState.features.length})</h4>
|
|
998
|
+
${solidState.features
|
|
999
|
+
.map((f, i) => `<div style="padding:4px;margin:2px;background:#2d2d30;">${f.type} #${i}</div>`)
|
|
1000
|
+
.join('')}
|
|
1001
|
+
</div>
|
|
1002
|
+
</div>
|
|
1003
|
+
`;
|
|
1004
|
+
},
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Execute solid command via agent API
|
|
1008
|
+
*/
|
|
1009
|
+
async execute(command, params = {}) {
|
|
1010
|
+
switch (command) {
|
|
1011
|
+
case 'extrude':
|
|
1012
|
+
return this.extrude(params.geometry, params);
|
|
1013
|
+
|
|
1014
|
+
case 'revolve':
|
|
1015
|
+
return this.revolve(params.geometry, params.axis, params);
|
|
1016
|
+
|
|
1017
|
+
case 'sweep':
|
|
1018
|
+
return this.sweep(params.profileGeometry, params.pathGeometry, params);
|
|
1019
|
+
|
|
1020
|
+
case 'loft':
|
|
1021
|
+
return this.loft(params.profileGeometries, params);
|
|
1022
|
+
|
|
1023
|
+
case 'hole':
|
|
1024
|
+
return this.hole(params.bodyId, params.faceId, params);
|
|
1025
|
+
|
|
1026
|
+
case 'thread':
|
|
1027
|
+
return this.thread(params.bodyId, params.geometry, params);
|
|
1028
|
+
|
|
1029
|
+
case 'fillet':
|
|
1030
|
+
return this.fillet(params.bodyId, params.edgeIds, params);
|
|
1031
|
+
|
|
1032
|
+
case 'chamfer':
|
|
1033
|
+
return this.chamfer(params.bodyId, params.edgeIds, params);
|
|
1034
|
+
|
|
1035
|
+
case 'shell':
|
|
1036
|
+
return this.shell(params.bodyId, params);
|
|
1037
|
+
|
|
1038
|
+
case 'draft':
|
|
1039
|
+
return this.draft(params.bodyId, params.faceIds, params);
|
|
1040
|
+
|
|
1041
|
+
case 'scale':
|
|
1042
|
+
return this.scale(params.bodyId, params);
|
|
1043
|
+
|
|
1044
|
+
case 'combine':
|
|
1045
|
+
return this.combine(params.bodyId1, params.bodyId2, params);
|
|
1046
|
+
|
|
1047
|
+
case 'mirror':
|
|
1048
|
+
return this.mirror(params.bodyId, params);
|
|
1049
|
+
|
|
1050
|
+
case 'pattern':
|
|
1051
|
+
return this.pattern(params.bodyId, params);
|
|
1052
|
+
|
|
1053
|
+
case 'thicken':
|
|
1054
|
+
return this.thicken(params.geometry, params);
|
|
1055
|
+
|
|
1056
|
+
case 'getBodies':
|
|
1057
|
+
return { success: true, bodies: this.getBodies() };
|
|
1058
|
+
|
|
1059
|
+
case 'getFeatures':
|
|
1060
|
+
return { success: true, features: this.getFeatures() };
|
|
1061
|
+
|
|
1062
|
+
default:
|
|
1063
|
+
return { success: false, message: `Unknown command: ${command}` };
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
|
|
1067
|
+
// ========================================================================
|
|
1068
|
+
// PRIVATE HELPERS
|
|
1069
|
+
// ========================================================================
|
|
1070
|
+
|
|
1071
|
+
_createThreadGeometry(radius, length, pitch, segments) {
|
|
1072
|
+
const geometry = new THREE.BufferGeometry();
|
|
1073
|
+
const positions = [];
|
|
1074
|
+
|
|
1075
|
+
const turns = length / pitch;
|
|
1076
|
+
const points = turns * segments;
|
|
1077
|
+
|
|
1078
|
+
for (let i = 0; i < points; i++) {
|
|
1079
|
+
const t = i / points;
|
|
1080
|
+
const angle = t * Math.PI * 2 * turns;
|
|
1081
|
+
const z = t * length;
|
|
1082
|
+
const r = radius * (1 + 0.1 * Math.sin(angle));
|
|
1083
|
+
|
|
1084
|
+
const x = r * Math.cos(angle);
|
|
1085
|
+
const y = r * Math.sin(angle);
|
|
1086
|
+
|
|
1087
|
+
positions.push(x, y, z);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3));
|
|
1091
|
+
geometry.computeVertexNormals();
|
|
1092
|
+
|
|
1093
|
+
return geometry;
|
|
1094
|
+
},
|
|
1095
|
+
};
|