Skip to content

Commit 2a97b8c

Browse files
committed
POC: toggle extrusions and travel moves without re-rendering everyting
1 parent 9de20ff commit 2a97b8c

File tree

2 files changed

+308
-232
lines changed

2 files changed

+308
-232
lines changed

src/objects-manager.ts

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import { LineBox } from './helpers/line-box';
2+
import { Path } from './path';
3+
import { type Disposable } from './helpers/three-utils';
4+
5+
import {
6+
Group,
7+
Scene,
8+
Color,
9+
Plane,
10+
Vector3,
11+
ShaderMaterial,
12+
Euler,
13+
BatchedMesh,
14+
BufferGeometry,
15+
Material
16+
} from 'three';
17+
18+
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js';
19+
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js';
20+
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js';
21+
import { createColorMaterial } from './helpers/colorMaterial';
22+
23+
export class ObjectsManager {
24+
extrusionsGroup: Group;
25+
travelMovesGroup: Group;
26+
boundingBox: LineBox;
27+
scene: Scene;
28+
disposables: Disposable[] = [];
29+
clippingPlanes: Plane[] = [];
30+
31+
// shader material
32+
materials: ShaderMaterial[] = [];
33+
ambientLight = 0.4;
34+
directionalLight = 1.3;
35+
brightness = 1.3;
36+
37+
lineWidth: number;
38+
lineHeight: number;
39+
40+
extrusionWidth = 0.6;
41+
42+
private renderedPaths: Path[] = [];
43+
44+
constructor(scene: Scene, lineWidth: number, lineHeight = 0.2, extrusionWidth = 0.6) {
45+
this.scene = scene;
46+
this.extrusionsGroup = this.createGroup('Extrusions');
47+
this.travelMovesGroup = this.createGroup('Travel Moves');
48+
this.scene.add(this.extrusionsGroup);
49+
this.scene.add(this.travelMovesGroup);
50+
51+
this.lineWidth = lineWidth;
52+
this.lineHeight = lineHeight ?? 0.2;
53+
this.extrusionWidth = extrusionWidth;
54+
this.clippingPlanes = this.createClippingPlanes(lineWidth, lineHeight);
55+
}
56+
57+
hideTravels() {
58+
this.travelMovesGroup.visible = false;
59+
}
60+
61+
showTravels() {
62+
this.travelMovesGroup.visible = true;
63+
}
64+
65+
hideExtrusions() {
66+
this.extrusionsGroup.visible = false;
67+
}
68+
69+
showExtrusions() {
70+
this.extrusionsGroup.visible = true;
71+
}
72+
73+
renderTravelLines(paths: Path[], color: Color) {
74+
const unrenderedPaths = paths.filter((p) => !this.renderedPaths.includes(p));
75+
const line = this.renderPathsAsLines(unrenderedPaths, color);
76+
this.travelMovesGroup.add(line);
77+
this.renderedPaths.push(...unrenderedPaths);
78+
}
79+
80+
renderExtrusionLines(paths: Path[], color: Color) {
81+
const unrenderedPaths = paths.filter((p) => !this.renderedPaths.includes(p));
82+
const line = this.renderPathsAsLines(unrenderedPaths, color);
83+
this.extrusionsGroup.add(line);
84+
this.renderedPaths.push(...unrenderedPaths);
85+
}
86+
87+
renderExtrusionTubes(paths: Path[], color: Color) {
88+
const unrenderedPaths = paths.filter((p) => !this.renderedPaths.includes(p));
89+
const tubes = this.renderPathsAsTubes(unrenderedPaths, color);
90+
this.extrusionsGroup.add(tubes);
91+
this.renderedPaths.push(...unrenderedPaths);
92+
}
93+
94+
dispose() {
95+
this.disposables.forEach((d) => d.dispose());
96+
this.disposables = [];
97+
}
98+
99+
updateClippingPlanes(minZ: number, maxZ: number) {
100+
this.updateClippingPlanesForShaderMaterials(minZ, maxZ);
101+
this.updateLineClipping(minZ, maxZ);
102+
}
103+
104+
private renderPathsAsLines(paths: Path[], color: Color): LineSegments2 {
105+
console.log('rendering lines', paths.length);
106+
const material = new LineMaterial({
107+
color: Number(color.getHex()),
108+
linewidth: this.lineWidth
109+
// clippingPlanes: this.clippingPlanes
110+
});
111+
112+
// lines need to be offset.
113+
// The gcode specifies the nozzle height which is the top of the extrusion.
114+
// The line doesn't have a constant height in world coords so it should be rendered at horizontal midplane of the extrusion layer.
115+
// Otherwise the line will be clipped by the clipping plane.
116+
const offset = -this.lineHeight / 2;
117+
const lineVertices: number[] = [];
118+
paths.forEach((path) => {
119+
for (let i = 0; i < path.vertices.length - 3; i += 3) {
120+
lineVertices.push(path.vertices[i], path.vertices[i + 1] - 0.1, path.vertices[i + 2] + offset);
121+
lineVertices.push(path.vertices[i + 3], path.vertices[i + 4] - 0.1, path.vertices[i + 5] + offset);
122+
}
123+
});
124+
125+
const geometry = new LineSegmentsGeometry().setPositions(lineVertices);
126+
this.disposables.push(material);
127+
this.disposables.push(geometry);
128+
return new LineSegments2(geometry, material);
129+
}
130+
131+
/**
132+
* Renders paths as 3D tubes
133+
* @param paths - Array of paths to render
134+
* @param color - Color to use for the tubes
135+
*/
136+
private renderPathsAsTubes(paths: Path[], color: Color): BatchedMesh {
137+
console.log('rendering tubes', paths.length);
138+
const colorNumber = Number(color.getHex());
139+
const geometries: BufferGeometry[] = [];
140+
141+
const material = createColorMaterial(colorNumber, this.ambientLight, this.directionalLight, this.brightness);
142+
143+
this.materials.push(material);
144+
145+
paths.forEach((path) => {
146+
const geometry = path.geometry({
147+
extrusionWidthOverride: this.extrusionWidth,
148+
lineHeightOverride: this.lineHeight
149+
});
150+
151+
if (!geometry) return;
152+
153+
this.disposables.push(geometry);
154+
geometries.push(geometry);
155+
});
156+
157+
const batchedMesh = this.createBatchMesh(geometries, material);
158+
this.disposables.push(material);
159+
return batchedMesh;
160+
}
161+
162+
/**
163+
* Creates a batched mesh from multiple geometries sharing the same material
164+
* @param geometries - Array of geometries to batch
165+
* @param material - Material to use for the batched mesh
166+
* @returns Batched mesh instance
167+
*/
168+
private createBatchMesh(geometries: BufferGeometry[], material: Material): BatchedMesh {
169+
const maxVertexCount = geometries.reduce((acc, geometry) => geometry.attributes.position.count * 3 + acc, 0);
170+
171+
const batchedMesh = new BatchedMesh(geometries.length, maxVertexCount, undefined, material);
172+
this.disposables.push(batchedMesh);
173+
174+
geometries.forEach((geometry) => {
175+
const geometryId = batchedMesh.addGeometry(geometry);
176+
// NOTE: for older versions of three.js, addInstance is not available
177+
// This allow webgl1 browsers to use the batched mesh
178+
batchedMesh.addInstance?.(geometryId);
179+
});
180+
181+
return batchedMesh;
182+
}
183+
184+
/**
185+
* Applies clipping planes to the specified material based on the minimum and maximum Z values.
186+
*
187+
* This method creates clipping planes for the top and bottom of the specified Z range,
188+
* then applies them to the material's clippingPlanes property.
189+
*
190+
* @param material - Shader material to apply clipping planes to
191+
* @param minZ - The minimum Z value for the clipping plane.
192+
* @param maxZ - The maximum Z value for the clipping plane.
193+
*/
194+
private createClippingPlanes(minZ?: number | undefined, maxZ?: number | undefined) {
195+
const planes = [];
196+
if (minZ !== undefined) {
197+
planes.push(new Plane(new Vector3(0, 1, 0), -minZ));
198+
}
199+
if (maxZ !== undefined) {
200+
planes.push(new Plane(new Vector3(0, -1, 0), maxZ));
201+
}
202+
return planes;
203+
}
204+
205+
/**
206+
* Updates the clipping planes for all `LineSegments2` objects in the scene.
207+
* This method filters the scene's children to find instances of `LineSegments2`,
208+
* then applies the clipping planes to their materials.
209+
*
210+
* @param minZ - The minimum Z value for the clipping plane.
211+
* @param maxZ - The maximum Z value for the clipping plane.
212+
*/
213+
private updateLineClipping(minZ: number | undefined, maxZ: number | undefined) {
214+
// TODO: apply clipping selectively to travels lines and extrusion lines
215+
// and/or use a clipping group
216+
this.scene.traverse((obj) => {
217+
if (obj instanceof LineSegments2) {
218+
const material = obj.material as LineMaterial;
219+
material.clippingPlanes = this.createClippingPlanes(minZ, maxZ);
220+
}
221+
});
222+
}
223+
224+
/**
225+
* Updates the clipping planes for all shader materials in the scene.
226+
* This method sets the min and max Z values for the clipping planes in the shader materials.
227+
*
228+
* @param minZ - The minimum Z value for the clipping plane.
229+
* @param maxZ - The maximum Z value for the clipping plane
230+
*/
231+
232+
private updateClippingPlanesForShaderMaterials(minZ: number, maxZ: number) {
233+
this.materials.forEach((material) => {
234+
material.uniforms.clipMinY.value = minZ;
235+
material.uniforms.clipMaxY.value = maxZ;
236+
});
237+
}
238+
239+
/**
240+
* Creates a new Three.js group for organizing rendered paths
241+
* @param name - Name for the group
242+
* @returns Configured Three.js group
243+
* @remarks
244+
* Sets up the group's orientation and position based on build volume dimensions.
245+
* If no build volume is defined, uses a default position.
246+
*/
247+
private createGroup(name: string): Group {
248+
const group = new Group();
249+
group.name = name;
250+
group.quaternion.setFromEuler(new Euler(-Math.PI / 2, 0, 0));
251+
return group;
252+
}
253+
}

0 commit comments

Comments
 (0)