cyclecad 1.3.2 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DRAWING_MODULE_INTEGRATION.md +633 -0
- package/README.md +138 -317
- package/app/index.html +2 -0
- package/app/js/brep-kernel.js +853 -0
- package/app/js/kernel.js +684 -0
- package/app/js/modules/assembly-module.js +582 -0
- package/app/js/modules/brep-module.js +583 -0
- package/app/js/modules/drawing-module.js +883 -0
- package/app/js/modules/operations-module.js +660 -0
- package/app/js/modules/simulation-module.js +834 -0
- package/app/js/modules/sketch-module.js +720 -0
- package/app/js/modules/step-module.js +510 -0
- package/app/js/modules/viewport-module.js +530 -0
- package/fusion360-gap-analysis.html +636 -0
- package/package.json +1 -1
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assembly Module for cycleCAD
|
|
3
|
+
* Manages multi-body assemblies with joints, constraints, and motion studies
|
|
4
|
+
* Version 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const AssemblyModule = {
|
|
8
|
+
id: 'assembly',
|
|
9
|
+
name: 'Assembly',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
category: 'engine',
|
|
12
|
+
dependencies: ['viewport', 'operations'],
|
|
13
|
+
memoryEstimate: 25,
|
|
14
|
+
|
|
15
|
+
// Module state
|
|
16
|
+
state: {
|
|
17
|
+
components: new Map(), // id -> {partId, name, position, rotation, visible}
|
|
18
|
+
joints: new Map(), // id -> {type, comp1, comp2, axis, origin, min, max, value}
|
|
19
|
+
bomEntries: [], // [{partId, quantity, name}]
|
|
20
|
+
explodeFactor: 0,
|
|
21
|
+
motionStudy: null, // {jointId, start, end, steps, positions[]}
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// Joint type definitions
|
|
25
|
+
JOINT_TYPES: {
|
|
26
|
+
RIGID: 'rigid', // 0 DOF
|
|
27
|
+
REVOLUTE: 'revolute', // 1 DOF (rotation)
|
|
28
|
+
SLIDER: 'slider', // 1 DOF (translation)
|
|
29
|
+
CYLINDRICAL: 'cylindrical', // 2 DOF (rotation + translation)
|
|
30
|
+
PIN_SLOT: 'pin-slot', // 2 DOF (rotation + perpendicular translation)
|
|
31
|
+
PLANAR: 'planar', // 3 DOF (translation in plane + normal rotation)
|
|
32
|
+
BALL: 'ball', // 3 DOF (rotation around point)
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialize assembly module
|
|
37
|
+
*/
|
|
38
|
+
init() {
|
|
39
|
+
if (window._debug) console.log('[Assembly] Initializing...');
|
|
40
|
+
this.state.components.clear();
|
|
41
|
+
this.state.joints.clear();
|
|
42
|
+
this.state.bomEntries = [];
|
|
43
|
+
this.state.explodeFactor = 0;
|
|
44
|
+
this._initEventListeners();
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize event listeners
|
|
49
|
+
*/
|
|
50
|
+
_initEventListeners() {
|
|
51
|
+
// Placeholder for event delegation
|
|
52
|
+
window.addEventListener('assembly:action', (e) => {
|
|
53
|
+
if (window._debug) console.log('[Assembly] Event:', e.detail);
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Insert a component (part) into assembly
|
|
59
|
+
* @param {string} partId - UUID or index of part
|
|
60
|
+
* @param {THREE.Vector3} position - placement position
|
|
61
|
+
* @param {THREE.Quaternion} rotation - placement rotation
|
|
62
|
+
* @returns {string} componentId
|
|
63
|
+
*/
|
|
64
|
+
insertComponent(partId, position = new THREE.Vector3(0, 0, 0), rotation = new THREE.Quaternion()) {
|
|
65
|
+
const componentId = `comp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
66
|
+
|
|
67
|
+
const component = {
|
|
68
|
+
id: componentId,
|
|
69
|
+
partId,
|
|
70
|
+
name: `Part ${this.state.components.size + 1}`,
|
|
71
|
+
position: position.clone(),
|
|
72
|
+
rotation: rotation.clone(),
|
|
73
|
+
visible: true,
|
|
74
|
+
group: new THREE.Group(),
|
|
75
|
+
matrix: new THREE.Matrix4(),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Set initial transform
|
|
79
|
+
component.group.position.copy(position);
|
|
80
|
+
component.group.quaternion.copy(rotation);
|
|
81
|
+
|
|
82
|
+
this.state.components.set(componentId, component);
|
|
83
|
+
|
|
84
|
+
if (window._debug) console.log(`[Assembly] Inserted component ${componentId} for part ${partId}`);
|
|
85
|
+
window.dispatchEvent(new CustomEvent('assembly:componentInserted', { detail: { componentId, partId } }));
|
|
86
|
+
|
|
87
|
+
return componentId;
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Create a joint between two components
|
|
92
|
+
* @param {string} type - joint type (RIGID, REVOLUTE, SLIDER, etc.)
|
|
93
|
+
* @param {string} comp1Id - first component ID
|
|
94
|
+
* @param {string} comp2Id - second component ID
|
|
95
|
+
* @param {Object} params - {axis: Vector3, origin: Vector3, offset1: Vector3, offset2: Vector3}
|
|
96
|
+
* @returns {string} jointId
|
|
97
|
+
*/
|
|
98
|
+
createJoint(type, comp1Id, comp2Id, params = {}) {
|
|
99
|
+
if (!this.JOINT_TYPES[type.toUpperCase()]) {
|
|
100
|
+
console.error(`[Assembly] Unknown joint type: ${type}`);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const jointId = `joint-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
105
|
+
const comp1 = this.state.components.get(comp1Id);
|
|
106
|
+
const comp2 = this.state.components.get(comp2Id);
|
|
107
|
+
|
|
108
|
+
if (!comp1 || !comp2) {
|
|
109
|
+
console.error(`[Assembly] Component not found: ${comp1Id} or ${comp2Id}`);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Default axis and origin (world Z-axis at midpoint)
|
|
114
|
+
const axis = params.axis || new THREE.Vector3(0, 0, 1);
|
|
115
|
+
const origin = params.origin || new THREE.Vector3().addVectors(comp1.position, comp2.position).multiplyScalar(0.5);
|
|
116
|
+
|
|
117
|
+
const joint = {
|
|
118
|
+
id: jointId,
|
|
119
|
+
type: type.toLowerCase(),
|
|
120
|
+
comp1Id,
|
|
121
|
+
comp2Id,
|
|
122
|
+
axis: axis.normalize(),
|
|
123
|
+
origin: origin.clone(),
|
|
124
|
+
min: params.min !== undefined ? params.min : 0,
|
|
125
|
+
max: params.max !== undefined ? params.max : 0,
|
|
126
|
+
value: 0, // Current joint value (angle in rad for revolute, distance for slider)
|
|
127
|
+
offset1: params.offset1 ? params.offset1.clone() : new THREE.Vector3(),
|
|
128
|
+
offset2: params.offset2 ? params.offset2.clone() : new THREE.Vector3(),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
this.state.joints.set(jointId, joint);
|
|
132
|
+
|
|
133
|
+
if (window._debug) console.log(`[Assembly] Created ${type} joint ${jointId} between ${comp1Id} and ${comp2Id}`);
|
|
134
|
+
window.dispatchEvent(new CustomEvent('assembly:jointCreated', { detail: { jointId, type, comp1Id, comp2Id } }));
|
|
135
|
+
|
|
136
|
+
return jointId;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Set joint limits (min and max values)
|
|
141
|
+
* @param {string} jointId - joint ID
|
|
142
|
+
* @param {number} min - minimum value (radians for revolute, mm for slider)
|
|
143
|
+
* @param {number} max - maximum value
|
|
144
|
+
*/
|
|
145
|
+
setJointLimits(jointId, min, max) {
|
|
146
|
+
const joint = this.state.joints.get(jointId);
|
|
147
|
+
if (!joint) {
|
|
148
|
+
console.error(`[Assembly] Joint not found: ${jointId}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
joint.min = min;
|
|
153
|
+
joint.max = max;
|
|
154
|
+
joint.value = Math.max(min, Math.min(joint.value, max)); // Clamp current value
|
|
155
|
+
|
|
156
|
+
if (window._debug) console.log(`[Assembly] Set limits for ${jointId}: [${min}, ${max}]`);
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Animate a joint to a specific value
|
|
161
|
+
* @param {string} jointId - joint ID
|
|
162
|
+
* @param {number} value - target value (radians for revolute, mm for slider)
|
|
163
|
+
* @param {number} duration - animation duration in ms (optional, default 500)
|
|
164
|
+
*/
|
|
165
|
+
animateJoint(jointId, value, duration = 500) {
|
|
166
|
+
const joint = this.state.joints.get(jointId);
|
|
167
|
+
if (!joint) {
|
|
168
|
+
console.error(`[Assembly] Joint not found: ${jointId}`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Clamp to limits
|
|
173
|
+
const targetValue = Math.max(joint.min, Math.min(value, joint.max));
|
|
174
|
+
const startValue = joint.value;
|
|
175
|
+
const startTime = Date.now();
|
|
176
|
+
|
|
177
|
+
const animate = () => {
|
|
178
|
+
const elapsed = Date.now() - startTime;
|
|
179
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
180
|
+
|
|
181
|
+
// Ease-in-out cubic
|
|
182
|
+
const easeProgress = progress < 0.5
|
|
183
|
+
? 4 * progress * progress * progress
|
|
184
|
+
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
|
|
185
|
+
|
|
186
|
+
joint.value = startValue + (targetValue - startValue) * easeProgress;
|
|
187
|
+
this._updateComponentTransforms();
|
|
188
|
+
|
|
189
|
+
if (progress < 1) {
|
|
190
|
+
requestAnimationFrame(animate);
|
|
191
|
+
} else {
|
|
192
|
+
joint.value = targetValue;
|
|
193
|
+
this._updateComponentTransforms();
|
|
194
|
+
window.dispatchEvent(new CustomEvent('assembly:jointAnimationComplete', { detail: { jointId } }));
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
animate();
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Update component transforms based on joint values
|
|
203
|
+
* @private
|
|
204
|
+
*/
|
|
205
|
+
_updateComponentTransforms() {
|
|
206
|
+
for (const joint of this.state.joints.values()) {
|
|
207
|
+
const comp1 = this.state.components.get(joint.comp1Id);
|
|
208
|
+
const comp2 = this.state.components.get(joint.comp2Id);
|
|
209
|
+
|
|
210
|
+
if (!comp1 || !comp2) continue;
|
|
211
|
+
|
|
212
|
+
switch (joint.type) {
|
|
213
|
+
case 'rigid':
|
|
214
|
+
// No relative motion
|
|
215
|
+
break;
|
|
216
|
+
|
|
217
|
+
case 'revolute':
|
|
218
|
+
// Rotate comp2 around joint axis
|
|
219
|
+
{
|
|
220
|
+
const q = new THREE.Quaternion();
|
|
221
|
+
q.setFromAxisAngle(joint.axis, joint.value);
|
|
222
|
+
|
|
223
|
+
// Transform relative to joint origin
|
|
224
|
+
const relPos = comp2.position.clone().sub(joint.origin);
|
|
225
|
+
relPos.applyQuaternion(q);
|
|
226
|
+
comp2.position.copy(joint.origin).add(relPos);
|
|
227
|
+
|
|
228
|
+
const newRot = new THREE.Quaternion();
|
|
229
|
+
newRot.multiplyQuaternions(q, comp2.rotation);
|
|
230
|
+
comp2.rotation.copy(newRot);
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
|
|
234
|
+
case 'slider':
|
|
235
|
+
// Translate comp2 along joint axis
|
|
236
|
+
{
|
|
237
|
+
const translation = joint.axis.clone().multiplyScalar(joint.value);
|
|
238
|
+
comp2.position.copy(comp1.position).add(translation).add(joint.offset2);
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
|
|
242
|
+
case 'cylindrical':
|
|
243
|
+
// Combination of revolute and slider
|
|
244
|
+
{
|
|
245
|
+
// Translation component (only apply 50% of value for cylindrical)
|
|
246
|
+
const translation = joint.axis.clone().multiplyScalar(joint.value * 0.5);
|
|
247
|
+
const q = new THREE.Quaternion();
|
|
248
|
+
q.setFromAxisAngle(joint.axis, joint.value);
|
|
249
|
+
|
|
250
|
+
const relPos = comp2.position.clone().sub(joint.origin);
|
|
251
|
+
relPos.applyQuaternion(q);
|
|
252
|
+
relPos.add(translation);
|
|
253
|
+
|
|
254
|
+
comp2.position.copy(joint.origin).add(relPos);
|
|
255
|
+
const newRot = new THREE.Quaternion();
|
|
256
|
+
newRot.multiplyQuaternions(q, comp2.rotation);
|
|
257
|
+
comp2.rotation.copy(newRot);
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
|
|
261
|
+
case 'planar':
|
|
262
|
+
// Translation in plane (2 DOF) — simplified to just translation
|
|
263
|
+
{
|
|
264
|
+
const translation = joint.axis.clone().multiplyScalar(joint.value);
|
|
265
|
+
comp2.position.copy(comp1.position).add(translation);
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
|
|
269
|
+
case 'ball':
|
|
270
|
+
// Free rotation around origin
|
|
271
|
+
{
|
|
272
|
+
const q = new THREE.Quaternion();
|
|
273
|
+
q.setFromAxisAngle(joint.axis, joint.value);
|
|
274
|
+
const newRot = new THREE.Quaternion();
|
|
275
|
+
newRot.multiplyQuaternions(q, comp2.rotation);
|
|
276
|
+
comp2.rotation.copy(newRot);
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Update Three.js group
|
|
282
|
+
comp2.group.position.copy(comp2.position);
|
|
283
|
+
comp2.group.quaternion.copy(comp2.rotation);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Check for interference (overlap) between components
|
|
289
|
+
* @param {string[]} componentIds - list of component IDs to check (or all if empty)
|
|
290
|
+
* @returns {Array} [{comp1Id, comp2Id, distance, isInterfering}]
|
|
291
|
+
*/
|
|
292
|
+
checkInterference(componentIds = []) {
|
|
293
|
+
const toCheck = componentIds.length > 0 ? componentIds : Array.from(this.state.components.keys());
|
|
294
|
+
const results = [];
|
|
295
|
+
|
|
296
|
+
for (let i = 0; i < toCheck.length; i++) {
|
|
297
|
+
for (let j = i + 1; j < toCheck.length; j++) {
|
|
298
|
+
const comp1 = this.state.components.get(toCheck[i]);
|
|
299
|
+
const comp2 = this.state.components.get(toCheck[j]);
|
|
300
|
+
|
|
301
|
+
if (!comp1 || !comp2) continue;
|
|
302
|
+
|
|
303
|
+
// Simple BBox-based interference check
|
|
304
|
+
// In production, would use mesh-to-mesh intersection
|
|
305
|
+
const dist = comp1.position.distanceTo(comp2.position);
|
|
306
|
+
const minDist = 5; // Minimum safe distance in mm
|
|
307
|
+
const isInterfering = dist < minDist;
|
|
308
|
+
|
|
309
|
+
results.push({
|
|
310
|
+
comp1Id: toCheck[i],
|
|
311
|
+
comp2Id: toCheck[j],
|
|
312
|
+
distance: dist,
|
|
313
|
+
isInterfering,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (isInterfering) {
|
|
317
|
+
window.dispatchEvent(new CustomEvent('assembly:interferenceFound', {
|
|
318
|
+
detail: { comp1Id: toCheck[i], comp2Id: toCheck[j], distance: dist }
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return results;
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Generate exploded view
|
|
329
|
+
* @param {number} factor - explosion distance multiplier (0-1 for collapse, 1+ for explode)
|
|
330
|
+
*/
|
|
331
|
+
explode(factor = 1.5) {
|
|
332
|
+
this.state.explodeFactor = factor;
|
|
333
|
+
|
|
334
|
+
for (const component of this.state.components.values()) {
|
|
335
|
+
// Move each component away from assembly origin
|
|
336
|
+
const direction = component.position.clone().normalize();
|
|
337
|
+
const baseDistance = component.position.length();
|
|
338
|
+
const newDistance = baseDistance * factor;
|
|
339
|
+
|
|
340
|
+
component.position.copy(direction.multiplyScalar(newDistance));
|
|
341
|
+
component.group.position.copy(component.position);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (window._debug) console.log(`[Assembly] Exploded view with factor ${factor}`);
|
|
345
|
+
window.dispatchEvent(new CustomEvent('assembly:exploded', { detail: { factor } }));
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Generate bill of materials
|
|
350
|
+
* @returns {Array} [{partId, name, quantity}]
|
|
351
|
+
*/
|
|
352
|
+
generateBOM() {
|
|
353
|
+
const bom = new Map(); // partId -> {name, quantity}
|
|
354
|
+
|
|
355
|
+
for (const component of this.state.components.values()) {
|
|
356
|
+
if (bom.has(component.partId)) {
|
|
357
|
+
bom.get(component.partId).quantity++;
|
|
358
|
+
} else {
|
|
359
|
+
bom.set(component.partId, {
|
|
360
|
+
partId: component.partId,
|
|
361
|
+
name: component.name,
|
|
362
|
+
quantity: 1,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this.state.bomEntries = Array.from(bom.values());
|
|
368
|
+
|
|
369
|
+
if (window._debug) console.log(`[Assembly] Generated BOM with ${this.state.bomEntries.length} entries`);
|
|
370
|
+
window.dispatchEvent(new CustomEvent('assembly:bomGenerated', { detail: { entries: this.state.bomEntries } }));
|
|
371
|
+
|
|
372
|
+
return this.state.bomEntries;
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Create a component pattern (linear or circular)
|
|
377
|
+
* @param {string} componentId - component to pattern
|
|
378
|
+
* @param {string} type - 'linear' or 'circular'
|
|
379
|
+
* @param {Object} params - {count, spacing, axis, center}
|
|
380
|
+
*/
|
|
381
|
+
pattern(componentId, type, params = {}) {
|
|
382
|
+
const baseComp = this.state.components.get(componentId);
|
|
383
|
+
if (!baseComp) {
|
|
384
|
+
console.error(`[Assembly] Component not found: ${componentId}`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const count = params.count || 3;
|
|
389
|
+
const spacing = params.spacing || 10;
|
|
390
|
+
const axis = params.axis || new THREE.Vector3(1, 0, 0);
|
|
391
|
+
const center = params.center || baseComp.position.clone();
|
|
392
|
+
|
|
393
|
+
const newComponentIds = [];
|
|
394
|
+
|
|
395
|
+
for (let i = 1; i < count; i++) {
|
|
396
|
+
let position = baseComp.position.clone();
|
|
397
|
+
|
|
398
|
+
if (type === 'linear') {
|
|
399
|
+
position.add(axis.clone().normalize().multiplyScalar(spacing * i));
|
|
400
|
+
} else if (type === 'circular') {
|
|
401
|
+
// Circular array around center
|
|
402
|
+
const angle = (Math.PI * 2 / count) * i;
|
|
403
|
+
const radius = baseComp.position.distanceTo(center);
|
|
404
|
+
const x = center.x + radius * Math.cos(angle);
|
|
405
|
+
const y = center.y + radius * Math.sin(angle);
|
|
406
|
+
position.set(x, y, center.z);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const newCompId = this.insertComponent(baseComp.partId, position, baseComp.rotation.clone());
|
|
410
|
+
newComponentIds.push(newCompId);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (window._debug) console.log(`[Assembly] Created ${type} pattern with ${count} instances`);
|
|
414
|
+
window.dispatchEvent(new CustomEvent('assembly:patternCreated', {
|
|
415
|
+
detail: { type, count, newComponentIds }
|
|
416
|
+
}));
|
|
417
|
+
|
|
418
|
+
return newComponentIds;
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Create a motion study (animate through joint range)
|
|
423
|
+
* @param {string} jointId - joint ID to animate
|
|
424
|
+
* @param {number} start - start value
|
|
425
|
+
* @param {number} end - end value
|
|
426
|
+
* @param {number} steps - number of steps in study
|
|
427
|
+
* @returns {Object} motion study object
|
|
428
|
+
*/
|
|
429
|
+
motionStudy(jointId, start, end, steps = 20) {
|
|
430
|
+
const joint = this.state.joints.get(jointId);
|
|
431
|
+
if (!joint) {
|
|
432
|
+
console.error(`[Assembly] Joint not found: ${jointId}`);
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const positions = [];
|
|
437
|
+
for (let i = 0; i <= steps; i++) {
|
|
438
|
+
const t = i / steps;
|
|
439
|
+
const value = start + (end - start) * t;
|
|
440
|
+
positions.push(value);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
this.state.motionStudy = {
|
|
444
|
+
jointId,
|
|
445
|
+
start,
|
|
446
|
+
end,
|
|
447
|
+
steps,
|
|
448
|
+
positions,
|
|
449
|
+
currentStep: 0,
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
if (window._debug) console.log(`[Assembly] Created motion study: ${steps} steps, ${start} → ${end}`);
|
|
453
|
+
window.dispatchEvent(new CustomEvent('assembly:motionStudyCreated', { detail: this.state.motionStudy }));
|
|
454
|
+
|
|
455
|
+
return this.state.motionStudy;
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Step through motion study
|
|
460
|
+
* @param {number} step - step index (0 to steps)
|
|
461
|
+
*/
|
|
462
|
+
motionStudyStep(step) {
|
|
463
|
+
if (!this.state.motionStudy) {
|
|
464
|
+
console.error('[Assembly] No active motion study');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const study = this.state.motionStudy;
|
|
469
|
+
step = Math.max(0, Math.min(step, study.positions.length - 1));
|
|
470
|
+
const value = study.positions[step];
|
|
471
|
+
|
|
472
|
+
this.animateJoint(study.jointId, value, 100);
|
|
473
|
+
study.currentStep = step;
|
|
474
|
+
|
|
475
|
+
window.dispatchEvent(new CustomEvent('assembly:motionStudyStep', { detail: { step, value } }));
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Get UI panel for assembly editor
|
|
480
|
+
* @returns {HTMLElement}
|
|
481
|
+
*/
|
|
482
|
+
getUI() {
|
|
483
|
+
const panel = document.createElement('div');
|
|
484
|
+
panel.className = 'assembly-panel';
|
|
485
|
+
panel.style.cssText = `
|
|
486
|
+
width: 300px;
|
|
487
|
+
height: 100%;
|
|
488
|
+
background: #1e1e1e;
|
|
489
|
+
color: #e0e0e0;
|
|
490
|
+
font-family: Calibri, sans-serif;
|
|
491
|
+
font-size: 13px;
|
|
492
|
+
border-left: 1px solid #333;
|
|
493
|
+
overflow-y: auto;
|
|
494
|
+
padding: 12px;
|
|
495
|
+
`;
|
|
496
|
+
|
|
497
|
+
panel.innerHTML = `
|
|
498
|
+
<div style="margin-bottom: 16px;">
|
|
499
|
+
<h3 style="margin: 0 0 8px 0; color: #0284c7;">Assembly Editor</h3>
|
|
500
|
+
|
|
501
|
+
<div style="margin-bottom: 12px;">
|
|
502
|
+
<label style="display: block; margin-bottom: 4px;">Components (${this.state.components.size})</label>
|
|
503
|
+
<div id="component-list" style="max-height: 150px; overflow-y: auto; background: #252525; border-radius: 4px; padding: 8px;">
|
|
504
|
+
${Array.from(this.state.components.values()).map(c => `
|
|
505
|
+
<div style="padding: 4px; margin-bottom: 4px; background: #333; border-radius: 2px; font-size: 11px;">
|
|
506
|
+
${c.name}
|
|
507
|
+
<button data-comp-id="${c.id}" class="comp-delete" style="float: right; padding: 1px 6px; font-size: 10px;">X</button>
|
|
508
|
+
</div>
|
|
509
|
+
`).join('')}
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
<div style="margin-bottom: 12px;">
|
|
514
|
+
<label style="display: block; margin-bottom: 4px;">Joints (${this.state.joints.size})</label>
|
|
515
|
+
<div id="joint-list" style="max-height: 150px; overflow-y: auto; background: #252525; border-radius: 4px; padding: 8px;">
|
|
516
|
+
${Array.from(this.state.joints.values()).map(j => `
|
|
517
|
+
<div style="padding: 4px; margin-bottom: 4px; background: #333; border-radius: 2px; font-size: 11px;">
|
|
518
|
+
${j.type.toUpperCase()}
|
|
519
|
+
<input type="range" min="${j.min}" max="${j.max}" value="${j.value}"
|
|
520
|
+
data-joint-id="${j.id}" class="joint-slider"
|
|
521
|
+
style="width: 100%; margin-top: 4px; cursor: pointer;">
|
|
522
|
+
</div>
|
|
523
|
+
`).join('')}
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
<button id="btn-explode" style="width: 100%; padding: 6px; margin-bottom: 6px; background: #0284c7; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
|
528
|
+
Explode (${(this.state.explodeFactor * 100).toFixed(0)}%)
|
|
529
|
+
</button>
|
|
530
|
+
|
|
531
|
+
<button id="btn-bom" style="width: 100%; padding: 6px; margin-bottom: 6px; background: #0284c7; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
|
532
|
+
Generate BOM
|
|
533
|
+
</button>
|
|
534
|
+
|
|
535
|
+
<button id="btn-motion-study" style="width: 100%; padding: 6px; background: #0284c7; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
|
536
|
+
Motion Study
|
|
537
|
+
</button>
|
|
538
|
+
</div>
|
|
539
|
+
`;
|
|
540
|
+
|
|
541
|
+
// Event handlers
|
|
542
|
+
const self = this;
|
|
543
|
+
panel.querySelectorAll('.joint-slider').forEach(slider => {
|
|
544
|
+
slider.addEventListener('input', (e) => {
|
|
545
|
+
const jointId = e.target.dataset.jointId;
|
|
546
|
+
const value = parseFloat(e.target.value);
|
|
547
|
+
self.animateJoint(jointId, value, 200);
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
panel.querySelector('#btn-explode').addEventListener('click', () => {
|
|
552
|
+
const factor = this.state.explodeFactor > 1 ? 1 : 1.5;
|
|
553
|
+
this.explode(factor);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
panel.querySelector('#btn-bom').addEventListener('click', () => {
|
|
557
|
+
const bom = this.generateBOM();
|
|
558
|
+
console.table(bom);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
panel.querySelector('#btn-motion-study').addEventListener('click', () => {
|
|
562
|
+
if (this.state.joints.size > 0) {
|
|
563
|
+
const joint = Array.from(this.state.joints.values())[0];
|
|
564
|
+
const study = this.motionStudy(joint.id, joint.min, joint.max, 30);
|
|
565
|
+
if (study) {
|
|
566
|
+
let step = 0;
|
|
567
|
+
const interval = setInterval(() => {
|
|
568
|
+
if (step >= study.positions.length) {
|
|
569
|
+
clearInterval(interval);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
this.motionStudyStep(step++);
|
|
573
|
+
}, 100);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
return panel;
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
export default AssemblyModule;
|