cyclecad 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CNAME +1 -0
- package/app/docs/api-reference.html +1436 -0
- package/app/docs/examples.html +803 -0
- package/app/docs/getting-started.html +1620 -0
- package/app/duo-project-browser.html +1321 -0
- package/app/duo-rebuild-guide.html +861 -0
- package/app/index.html +1635 -0
- package/app/js/ai-chat.js +992 -0
- package/app/js/app.js +724 -0
- package/app/js/export.js +658 -0
- package/app/js/inventor-parser.js +1138 -0
- package/app/js/operations.js +689 -0
- package/app/js/params.js +523 -0
- package/app/js/reverse-engineer.js +1275 -0
- package/app/js/shortcuts.js +350 -0
- package/app/js/sketch.js +899 -0
- package/app/js/tree.js +479 -0
- package/app/js/viewport.js +643 -0
- package/app/samples/Leistenbuerstenblech.ipt +0 -0
- package/app/samples/Rahmen_Seite.iam +0 -0
- package/app/samples/TraegerHoehe1.ipt +0 -0
- package/index.html +1226 -0
- package/package.json +33 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* viewport.js - Three.js 3D viewport module for cycleCAD
|
|
3
|
+
*
|
|
4
|
+
* A production-quality ES module that provides:
|
|
5
|
+
* - Three.js scene, camera, renderer initialization
|
|
6
|
+
* - OrbitControls with damping
|
|
7
|
+
* - Lighting (ambient, directional, fill)
|
|
8
|
+
* - Grid + origin reference geometry
|
|
9
|
+
* - Preset camera views with smooth transitions
|
|
10
|
+
* - Object management and viewport utilities
|
|
11
|
+
* - Proper cleanup and resize handling
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
|
|
15
|
+
import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/controls/OrbitControls.js';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Module State
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
let scene = null;
|
|
22
|
+
let camera = null;
|
|
23
|
+
let renderer = null;
|
|
24
|
+
let controls = null;
|
|
25
|
+
let animationFrameId = null;
|
|
26
|
+
let isAnimating = false;
|
|
27
|
+
|
|
28
|
+
// Grid and reference objects
|
|
29
|
+
let gridHelper = null;
|
|
30
|
+
let axisLines = null;
|
|
31
|
+
let referencePlanes = {};
|
|
32
|
+
|
|
33
|
+
// Camera animation state
|
|
34
|
+
let cameraAnimationState = {
|
|
35
|
+
isTransitioning: false,
|
|
36
|
+
startPos: new THREE.Vector3(),
|
|
37
|
+
endPos: new THREE.Vector3(),
|
|
38
|
+
startTarget: new THREE.Vector3(),
|
|
39
|
+
endTarget: new THREE.Vector3(),
|
|
40
|
+
startTime: 0,
|
|
41
|
+
duration: 800, // ms
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Preset camera views (distance varies by scene size)
|
|
45
|
+
const PRESET_VIEWS = {
|
|
46
|
+
front: { pos: { x: 0, y: 0, z: 1 }, target: { x: 0, y: 0, z: 0 } },
|
|
47
|
+
back: { pos: { x: 0, y: 0, z: -1 }, target: { x: 0, y: 0, z: 0 } },
|
|
48
|
+
top: { pos: { x: 0, y: 1, z: 0 }, target: { x: 0, y: 0, z: 0 } },
|
|
49
|
+
bottom: { pos: { x: 0, y: -1, z: 0 }, target: { x: 0, y: 0, z: 0 } },
|
|
50
|
+
left: { pos: { x: -1, y: 0, z: 0 }, target: { x: 0, y: 0, z: 0 } },
|
|
51
|
+
right: { pos: { x: 1, y: 0, z: 0 }, target: { x: 0, y: 0, z: 0 } },
|
|
52
|
+
iso: { pos: { x: 1, y: 1, z: 1 }, target: { x: 0, y: 0, z: 0 } },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const GRID_SIZE = 200;
|
|
56
|
+
const GRID_DIVISIONS = GRID_SIZE; // 1mm spacing
|
|
57
|
+
const AXIS_LENGTH = 60;
|
|
58
|
+
const COLORS = {
|
|
59
|
+
bg: 0x0a0e14,
|
|
60
|
+
ambient: 0xffffff,
|
|
61
|
+
directional: 0xffffff,
|
|
62
|
+
fill: 0xaabbff,
|
|
63
|
+
axisX: 0xff0000,
|
|
64
|
+
axisY: 0x00ff00,
|
|
65
|
+
axisZ: 0x0088ff,
|
|
66
|
+
gridMain: 0x3a4a6a,
|
|
67
|
+
gridSub: 0x1a2a4a,
|
|
68
|
+
refPlane: 0x444466,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Initialization
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Initialize the Three.js viewport with scene, camera, renderer, and controls
|
|
77
|
+
* @param {string} containerId - ID of the DOM element to attach the renderer
|
|
78
|
+
* @returns {Object} Viewport object with accessor methods
|
|
79
|
+
*/
|
|
80
|
+
export function initViewport(containerId) {
|
|
81
|
+
const container = document.getElementById(containerId);
|
|
82
|
+
if (!container) {
|
|
83
|
+
throw new Error(`Container with id "${containerId}" not found`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Get dimensions
|
|
87
|
+
const width = container.clientWidth;
|
|
88
|
+
const height = container.clientHeight;
|
|
89
|
+
|
|
90
|
+
// Create scene
|
|
91
|
+
scene = new THREE.Scene();
|
|
92
|
+
scene.background = new THREE.Color(COLORS.bg);
|
|
93
|
+
scene.fog = new THREE.Fog(COLORS.bg, 10000, 50000);
|
|
94
|
+
|
|
95
|
+
// Create camera
|
|
96
|
+
const fov = 45;
|
|
97
|
+
const aspect = width / height;
|
|
98
|
+
const near = 0.1;
|
|
99
|
+
const far = 50000;
|
|
100
|
+
camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
|
|
101
|
+
camera.position.set(150, 100, 150);
|
|
102
|
+
camera.lookAt(0, 0, 0);
|
|
103
|
+
|
|
104
|
+
// Create renderer
|
|
105
|
+
renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
|
|
106
|
+
renderer.setSize(width, height);
|
|
107
|
+
renderer.setPixelRatio(window.devicePixelRatio);
|
|
108
|
+
renderer.shadowMap.enabled = true;
|
|
109
|
+
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
110
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
111
|
+
renderer.toneMappingExposure = 1.0;
|
|
112
|
+
container.appendChild(renderer.domElement);
|
|
113
|
+
|
|
114
|
+
// Create OrbitControls
|
|
115
|
+
controls = new OrbitControls(camera, renderer.domElement);
|
|
116
|
+
controls.enableDamping = true;
|
|
117
|
+
controls.dampingFactor = 0.05;
|
|
118
|
+
controls.autoRotate = false;
|
|
119
|
+
controls.autoRotateSpeed = 5;
|
|
120
|
+
controls.enableZoom = true;
|
|
121
|
+
controls.enablePan = true;
|
|
122
|
+
controls.minDistance = 10;
|
|
123
|
+
controls.maxDistance = 10000;
|
|
124
|
+
|
|
125
|
+
// Setup lighting
|
|
126
|
+
setupLighting();
|
|
127
|
+
|
|
128
|
+
// Setup grid and reference geometry
|
|
129
|
+
setupGridAndOrigin();
|
|
130
|
+
|
|
131
|
+
// Setup event handlers
|
|
132
|
+
setupEventHandlers(container);
|
|
133
|
+
|
|
134
|
+
// Start animation loop
|
|
135
|
+
startAnimationLoop();
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
getScene,
|
|
139
|
+
getCamera,
|
|
140
|
+
getRenderer,
|
|
141
|
+
getControls,
|
|
142
|
+
setView,
|
|
143
|
+
fitToObject,
|
|
144
|
+
addToScene,
|
|
145
|
+
removeFromScene,
|
|
146
|
+
toggleGrid,
|
|
147
|
+
toggleAxisLines,
|
|
148
|
+
toggleReferencePlanes,
|
|
149
|
+
dispose,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// Lighting Setup
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Setup scene lighting: ambient, directional (main), and fill light
|
|
159
|
+
*/
|
|
160
|
+
function setupLighting() {
|
|
161
|
+
// Ambient light - soft overall illumination
|
|
162
|
+
const ambientLight = new THREE.AmbientLight(COLORS.ambient, 0.4);
|
|
163
|
+
scene.add(ambientLight);
|
|
164
|
+
|
|
165
|
+
// Main directional light with shadows
|
|
166
|
+
const directionalLight = new THREE.DirectionalLight(COLORS.directional, 0.8);
|
|
167
|
+
directionalLight.position.set(100, 150, 100);
|
|
168
|
+
directionalLight.castShadow = true;
|
|
169
|
+
directionalLight.shadow.mapSize.width = 2048;
|
|
170
|
+
directionalLight.shadow.mapSize.height = 2048;
|
|
171
|
+
directionalLight.shadow.camera.left = -500;
|
|
172
|
+
directionalLight.shadow.camera.right = 500;
|
|
173
|
+
directionalLight.shadow.camera.top = 500;
|
|
174
|
+
directionalLight.shadow.camera.bottom = -500;
|
|
175
|
+
directionalLight.shadow.camera.near = 0.1;
|
|
176
|
+
directionalLight.shadow.camera.far = 2000;
|
|
177
|
+
directionalLight.shadow.bias = -0.0001;
|
|
178
|
+
scene.add(directionalLight);
|
|
179
|
+
|
|
180
|
+
// Fill light from opposite side with bluish tint
|
|
181
|
+
const fillLight = new THREE.DirectionalLight(COLORS.fill, 0.3);
|
|
182
|
+
fillLight.position.set(-100, 50, -100);
|
|
183
|
+
scene.add(fillLight);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// Grid and Reference Geometry
|
|
188
|
+
// ============================================================================
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Setup grid helper and origin reference geometry
|
|
192
|
+
*/
|
|
193
|
+
function setupGridAndOrigin() {
|
|
194
|
+
// Create grid helper
|
|
195
|
+
gridHelper = new THREE.GridHelper(
|
|
196
|
+
GRID_SIZE,
|
|
197
|
+
GRID_DIVISIONS,
|
|
198
|
+
COLORS.gridMain,
|
|
199
|
+
COLORS.gridSub
|
|
200
|
+
);
|
|
201
|
+
gridHelper.position.y = 0;
|
|
202
|
+
scene.add(gridHelper);
|
|
203
|
+
|
|
204
|
+
// Create axis lines (X=red, Y=green, Z=blue)
|
|
205
|
+
const axisLineMaterial = new THREE.LineBasicMaterial({ linewidth: 2 });
|
|
206
|
+
const axisGeometry = new THREE.BufferGeometry();
|
|
207
|
+
|
|
208
|
+
const positions = [];
|
|
209
|
+
// X axis (red)
|
|
210
|
+
positions.push(-AXIS_LENGTH, 0, 0);
|
|
211
|
+
positions.push(AXIS_LENGTH, 0, 0);
|
|
212
|
+
// Y axis (green)
|
|
213
|
+
positions.push(0, -AXIS_LENGTH, 0);
|
|
214
|
+
positions.push(0, AXIS_LENGTH, 0);
|
|
215
|
+
// Z axis (blue)
|
|
216
|
+
positions.push(0, 0, -AXIS_LENGTH);
|
|
217
|
+
positions.push(0, 0, AXIS_LENGTH);
|
|
218
|
+
|
|
219
|
+
const colors = [];
|
|
220
|
+
// X axis
|
|
221
|
+
colors.push(1, 0, 0); // Red start
|
|
222
|
+
colors.push(1, 0, 0); // Red end
|
|
223
|
+
// Y axis
|
|
224
|
+
colors.push(0, 1, 0); // Green start
|
|
225
|
+
colors.push(0, 1, 0); // Green end
|
|
226
|
+
// Z axis
|
|
227
|
+
colors.push(0, 0.533, 1); // Blue start
|
|
228
|
+
colors.push(0, 0.533, 1); // Blue end
|
|
229
|
+
|
|
230
|
+
axisGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(positions), 3));
|
|
231
|
+
axisGeometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colors), 3));
|
|
232
|
+
|
|
233
|
+
const axisMaterial = new THREE.LineBasicMaterial({
|
|
234
|
+
vertexColors: true,
|
|
235
|
+
linewidth: 2,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
axisLines = new THREE.LineSegments(axisGeometry, axisMaterial);
|
|
239
|
+
scene.add(axisLines);
|
|
240
|
+
|
|
241
|
+
// Create reference planes (semi-transparent quads)
|
|
242
|
+
createReferencePlanes();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create semi-transparent reference planes (XY, XZ, YZ)
|
|
247
|
+
*/
|
|
248
|
+
function createReferencePlanes() {
|
|
249
|
+
const planeSize = 200;
|
|
250
|
+
const planeAlpha = 0.05;
|
|
251
|
+
|
|
252
|
+
// XY plane (Z = 0)
|
|
253
|
+
const xyGeom = new THREE.PlaneGeometry(planeSize, planeSize);
|
|
254
|
+
const xyMat = new THREE.MeshBasicMaterial({
|
|
255
|
+
color: 0x0088ff,
|
|
256
|
+
transparent: true,
|
|
257
|
+
opacity: planeAlpha,
|
|
258
|
+
side: THREE.DoubleSide,
|
|
259
|
+
});
|
|
260
|
+
referencePlanes.xy = new THREE.Mesh(xyGeom, xyMat);
|
|
261
|
+
referencePlanes.xy.position.z = 0;
|
|
262
|
+
referencePlanes.xy.userData.refPlane = true;
|
|
263
|
+
|
|
264
|
+
// XZ plane (Y = 0)
|
|
265
|
+
const xzGeom = new THREE.PlaneGeometry(planeSize, planeSize);
|
|
266
|
+
const xzMat = new THREE.MeshBasicMaterial({
|
|
267
|
+
color: 0x00ff88,
|
|
268
|
+
transparent: true,
|
|
269
|
+
opacity: planeAlpha,
|
|
270
|
+
side: THREE.DoubleSide,
|
|
271
|
+
});
|
|
272
|
+
referencePlanes.xz = new THREE.Mesh(xzGeom, xzMat);
|
|
273
|
+
referencePlanes.xz.rotation.x = Math.PI / 2;
|
|
274
|
+
referencePlanes.xz.position.y = 0;
|
|
275
|
+
referencePlanes.xz.userData.refPlane = true;
|
|
276
|
+
|
|
277
|
+
// YZ plane (X = 0)
|
|
278
|
+
const yzGeom = new THREE.PlaneGeometry(planeSize, planeSize);
|
|
279
|
+
const yzMat = new THREE.MeshBasicMaterial({
|
|
280
|
+
color: 0xff8800,
|
|
281
|
+
transparent: true,
|
|
282
|
+
opacity: planeAlpha,
|
|
283
|
+
side: THREE.DoubleSide,
|
|
284
|
+
});
|
|
285
|
+
referencePlanes.yz = new THREE.Mesh(yzGeom, yzMat);
|
|
286
|
+
referencePlanes.yz.rotation.y = Math.PI / 2;
|
|
287
|
+
referencePlanes.yz.position.x = 0;
|
|
288
|
+
referencePlanes.yz.userData.refPlane = true;
|
|
289
|
+
|
|
290
|
+
// Planes are created but not added to scene by default
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ============================================================================
|
|
294
|
+
// Camera View Management
|
|
295
|
+
// ============================================================================
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Set preset camera view with smooth animated transition
|
|
299
|
+
* @param {string} viewName - View name: 'front', 'back', 'top', 'bottom', 'left', 'right', 'iso'
|
|
300
|
+
* @param {number} duration - Animation duration in ms (default: 800)
|
|
301
|
+
*/
|
|
302
|
+
export function setView(viewName, duration = 800) {
|
|
303
|
+
const viewDef = PRESET_VIEWS[viewName];
|
|
304
|
+
if (!viewDef) {
|
|
305
|
+
console.warn(`Unknown view: ${viewName}. Available: ${Object.keys(PRESET_VIEWS).join(', ')}`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Calculate distance based on scene bounds
|
|
310
|
+
const distance = calculateOptimalDistance();
|
|
311
|
+
|
|
312
|
+
const startPos = camera.position.clone();
|
|
313
|
+
const startTarget = new THREE.Vector3();
|
|
314
|
+
controls.getTarget(startTarget);
|
|
315
|
+
|
|
316
|
+
const endPos = new THREE.Vector3(
|
|
317
|
+
viewDef.pos.x * distance,
|
|
318
|
+
viewDef.pos.y * distance,
|
|
319
|
+
viewDef.pos.z * distance
|
|
320
|
+
);
|
|
321
|
+
const endTarget = new THREE.Vector3(
|
|
322
|
+
viewDef.target.x,
|
|
323
|
+
viewDef.target.y,
|
|
324
|
+
viewDef.target.z
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
animateCamera(startPos, endPos, startTarget, endTarget, duration);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Fit camera to view an object with proper framing
|
|
332
|
+
* @param {THREE.Object3D} object - Object to fit to
|
|
333
|
+
* @param {number} padding - Padding factor (default: 1.2)
|
|
334
|
+
*/
|
|
335
|
+
export function fitToObject(object, padding = 1.2) {
|
|
336
|
+
const box = new THREE.Box3().setFromObject(object);
|
|
337
|
+
const size = box.getSize(new THREE.Vector3());
|
|
338
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
339
|
+
const fov = camera.fov * (Math.PI / 180); // Convert to radians
|
|
340
|
+
let cameraDistance = maxDim / 2 / Math.tan(fov / 2);
|
|
341
|
+
cameraDistance *= padding;
|
|
342
|
+
|
|
343
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
344
|
+
|
|
345
|
+
const direction = camera.position.clone().sub(center).normalize();
|
|
346
|
+
const newPos = center.clone().addScaledVector(direction, cameraDistance);
|
|
347
|
+
|
|
348
|
+
animateCamera(camera.position.clone(), newPos, center.clone(), center.clone(), 600);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Animate camera from one position/target to another
|
|
353
|
+
* @private
|
|
354
|
+
*/
|
|
355
|
+
function animateCamera(startPos, endPos, startTarget, endTarget, duration) {
|
|
356
|
+
cameraAnimationState.isTransitioning = true;
|
|
357
|
+
cameraAnimationState.startPos = startPos.clone();
|
|
358
|
+
cameraAnimationState.endPos = endPos.clone();
|
|
359
|
+
cameraAnimationState.startTarget = startTarget.clone();
|
|
360
|
+
cameraAnimationState.endTarget = endTarget.clone();
|
|
361
|
+
cameraAnimationState.startTime = Date.now();
|
|
362
|
+
cameraAnimationState.duration = duration;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Update camera animation (called from animation loop)
|
|
367
|
+
* @private
|
|
368
|
+
*/
|
|
369
|
+
function updateCameraAnimation() {
|
|
370
|
+
if (!cameraAnimationState.isTransitioning) return;
|
|
371
|
+
|
|
372
|
+
const elapsed = Date.now() - cameraAnimationState.startTime;
|
|
373
|
+
let t = Math.min(elapsed / cameraAnimationState.duration, 1);
|
|
374
|
+
|
|
375
|
+
// Ease-in-out cubic
|
|
376
|
+
t = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
377
|
+
|
|
378
|
+
// Interpolate position and target
|
|
379
|
+
camera.position.lerpVectors(
|
|
380
|
+
cameraAnimationState.startPos,
|
|
381
|
+
cameraAnimationState.endPos,
|
|
382
|
+
t
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const currentTarget = new THREE.Vector3();
|
|
386
|
+
currentTarget.lerpVectors(
|
|
387
|
+
cameraAnimationState.startTarget,
|
|
388
|
+
cameraAnimationState.endTarget,
|
|
389
|
+
t
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
controls.target.copy(currentTarget);
|
|
393
|
+
|
|
394
|
+
if (t >= 1) {
|
|
395
|
+
cameraAnimationState.isTransitioning = false;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Calculate optimal camera distance based on scene bounds
|
|
401
|
+
* @private
|
|
402
|
+
*/
|
|
403
|
+
function calculateOptimalDistance() {
|
|
404
|
+
const box = new THREE.Box3();
|
|
405
|
+
scene.traverse((obj) => {
|
|
406
|
+
if (obj.geometry) {
|
|
407
|
+
box.expandByObject(obj);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
if (!box.isEmpty()) {
|
|
412
|
+
const size = box.getSize(new THREE.Vector3());
|
|
413
|
+
return Math.max(size.x, size.y, size.z) * 1.5;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return 200; // Default fallback
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ============================================================================
|
|
420
|
+
// Scene Management
|
|
421
|
+
// ============================================================================
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Add an object to the scene
|
|
425
|
+
* @param {THREE.Object3D} object - Object to add
|
|
426
|
+
*/
|
|
427
|
+
export function addToScene(object) {
|
|
428
|
+
if (scene) {
|
|
429
|
+
scene.add(object);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Remove an object from the scene
|
|
435
|
+
* @param {THREE.Object3D} object - Object to remove
|
|
436
|
+
*/
|
|
437
|
+
export function removeFromScene(object) {
|
|
438
|
+
if (scene) {
|
|
439
|
+
scene.remove(object);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Toggle grid visibility
|
|
445
|
+
* @param {boolean} visible - Show/hide grid
|
|
446
|
+
*/
|
|
447
|
+
export function toggleGrid(visible) {
|
|
448
|
+
if (gridHelper) {
|
|
449
|
+
gridHelper.visible = visible !== false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Toggle axis lines visibility
|
|
455
|
+
* @param {boolean} visible - Show/hide axes
|
|
456
|
+
*/
|
|
457
|
+
export function toggleAxisLines(visible) {
|
|
458
|
+
if (axisLines) {
|
|
459
|
+
axisLines.visible = visible !== false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Toggle reference planes visibility
|
|
465
|
+
* @param {boolean} visible - Show/hide reference planes
|
|
466
|
+
* @param {string} plane - Specific plane: 'xy', 'xz', 'yz', or null for all
|
|
467
|
+
*/
|
|
468
|
+
export function toggleReferencePlanes(visible, plane = null) {
|
|
469
|
+
if (plane) {
|
|
470
|
+
if (referencePlanes[plane]) {
|
|
471
|
+
if (visible) {
|
|
472
|
+
if (!referencePlanes[plane].parent) {
|
|
473
|
+
scene.add(referencePlanes[plane]);
|
|
474
|
+
}
|
|
475
|
+
referencePlanes[plane].visible = true;
|
|
476
|
+
} else {
|
|
477
|
+
referencePlanes[plane].visible = false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
// Toggle all planes
|
|
482
|
+
['xy', 'xz', 'yz'].forEach((p) => {
|
|
483
|
+
if (visible) {
|
|
484
|
+
if (!referencePlanes[p].parent) {
|
|
485
|
+
scene.add(referencePlanes[p]);
|
|
486
|
+
}
|
|
487
|
+
referencePlanes[p].visible = true;
|
|
488
|
+
} else {
|
|
489
|
+
referencePlanes[p].visible = false;
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ============================================================================
|
|
496
|
+
// Accessors
|
|
497
|
+
// ============================================================================
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Get the Three.js scene
|
|
501
|
+
*/
|
|
502
|
+
export function getScene() {
|
|
503
|
+
return scene;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Get the PerspectiveCamera
|
|
508
|
+
*/
|
|
509
|
+
export function getCamera() {
|
|
510
|
+
return camera;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Get the WebGLRenderer
|
|
515
|
+
*/
|
|
516
|
+
export function getRenderer() {
|
|
517
|
+
return renderer;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Get OrbitControls instance
|
|
522
|
+
*/
|
|
523
|
+
export function getControls() {
|
|
524
|
+
return controls;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ============================================================================
|
|
528
|
+
// Event Handlers
|
|
529
|
+
// ============================================================================
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Setup window resize and other event handlers
|
|
533
|
+
* @private
|
|
534
|
+
*/
|
|
535
|
+
function setupEventHandlers(container) {
|
|
536
|
+
// Window resize
|
|
537
|
+
const handleResize = () => {
|
|
538
|
+
const width = container.clientWidth;
|
|
539
|
+
const height = container.clientHeight;
|
|
540
|
+
|
|
541
|
+
camera.aspect = width / height;
|
|
542
|
+
camera.updateProjectionMatrix();
|
|
543
|
+
renderer.setSize(width, height);
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
window.addEventListener('resize', handleResize);
|
|
547
|
+
|
|
548
|
+
// Store cleanup reference
|
|
549
|
+
renderer.dispose._resizeHandler = handleResize;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ============================================================================
|
|
553
|
+
// Animation Loop
|
|
554
|
+
// ============================================================================
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Start the main animation loop
|
|
558
|
+
* @private
|
|
559
|
+
*/
|
|
560
|
+
function startAnimationLoop() {
|
|
561
|
+
isAnimating = true;
|
|
562
|
+
|
|
563
|
+
const animate = () => {
|
|
564
|
+
animationFrameId = requestAnimationFrame(animate);
|
|
565
|
+
|
|
566
|
+
// Update camera animation
|
|
567
|
+
updateCameraAnimation();
|
|
568
|
+
|
|
569
|
+
// Update controls
|
|
570
|
+
if (controls) {
|
|
571
|
+
controls.update();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Render
|
|
575
|
+
if (renderer && scene && camera) {
|
|
576
|
+
renderer.render(scene, camera);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
animate();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Stop the animation loop
|
|
585
|
+
* @private
|
|
586
|
+
*/
|
|
587
|
+
function stopAnimationLoop() {
|
|
588
|
+
if (animationFrameId) {
|
|
589
|
+
cancelAnimationFrame(animationFrameId);
|
|
590
|
+
animationFrameId = null;
|
|
591
|
+
}
|
|
592
|
+
isAnimating = false;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// ============================================================================
|
|
596
|
+
// Cleanup
|
|
597
|
+
// ============================================================================
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Dispose of all Three.js resources and cleanup event listeners
|
|
601
|
+
*/
|
|
602
|
+
export function dispose() {
|
|
603
|
+
stopAnimationLoop();
|
|
604
|
+
|
|
605
|
+
// Cleanup geometries and materials
|
|
606
|
+
scene.traverse((obj) => {
|
|
607
|
+
if (obj.geometry) {
|
|
608
|
+
obj.geometry.dispose();
|
|
609
|
+
}
|
|
610
|
+
if (obj.material) {
|
|
611
|
+
if (Array.isArray(obj.material)) {
|
|
612
|
+
obj.material.forEach((m) => m.dispose());
|
|
613
|
+
} else {
|
|
614
|
+
obj.material.dispose();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Cleanup renderer
|
|
620
|
+
if (renderer) {
|
|
621
|
+
renderer.dispose();
|
|
622
|
+
if (renderer.domElement && renderer.domElement.parentNode) {
|
|
623
|
+
renderer.domElement.parentNode.removeChild(renderer.domElement);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Cleanup controls
|
|
628
|
+
if (controls) {
|
|
629
|
+
controls.dispose();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Remove event listeners
|
|
633
|
+
window.removeEventListener('resize', renderer.dispose._resizeHandler);
|
|
634
|
+
|
|
635
|
+
// Clear references
|
|
636
|
+
scene = null;
|
|
637
|
+
camera = null;
|
|
638
|
+
renderer = null;
|
|
639
|
+
controls = null;
|
|
640
|
+
gridHelper = null;
|
|
641
|
+
axisLines = null;
|
|
642
|
+
referencePlanes = {};
|
|
643
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|