Skip to content

Commit de7cfa8

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

File tree

2 files changed

+315
-259
lines changed

2 files changed

+315
-259
lines changed

src/objects-manager.ts

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
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.extrusionsGroup.removeFromParent();
96+
this.travelMovesGroup.removeFromParent();
97+
this.disposables.forEach((d) => d.dispose());
98+
this.disposables = [];
99+
}
100+
101+
updateClippingPlanes(minZ: number, maxZ: number) {
102+
this.updateClippingPlanesForShaderMaterials(minZ, maxZ);
103+
this.updateLineClipping(minZ, maxZ);
104+
}
105+
106+
private renderPathsAsLines(paths: Path[], color: Color): LineSegments2 {
107+
console.log('rendering lines', paths.length);
108+
const material = new LineMaterial({
109+
color: Number(color.getHex()),
110+
linewidth: this.lineWidth
111+
});
112+
113+
// lines need to be offset.
114+
// The gcode specifies the nozzle height which is the top of the extrusion.
115+
// The line doesn't have a constant height in world coords so it should be rendered at horizontal midplane of the extrusion layer.
116+
// Otherwise the line will be clipped by the clipping plane.
117+
const offset = -this.lineHeight / 2;
118+
const lineVertices: number[] = [];
119+
paths.forEach((path) => {
120+
for (let i = 0; i < path.vertices.length - 3; i += 3) {
121+
lineVertices.push(path.vertices[i], path.vertices[i + 1] - 0.1, path.vertices[i + 2] + offset);
122+
lineVertices.push(path.vertices[i + 3], path.vertices[i + 4] - 0.1, path.vertices[i + 5] + offset);
123+
}
124+
});
125+
126+
const geometry = new LineSegmentsGeometry().setPositions(lineVertices);
127+
this.disposables.push(material);
128+
this.disposables.push(geometry);
129+
return new LineSegments2(geometry, material);
130+
}
131+
132+
/**
133+
* Renders paths as 3D tubes
134+
* @param paths - Array of paths to render
135+
* @param color - Color to use for the tubes
136+
*/
137+
private renderPathsAsTubes(paths: Path[], color: Color): BatchedMesh {
138+
console.log('rendering tubes', paths.length);
139+
const colorNumber = Number(color.getHex());
140+
const geometries: BufferGeometry[] = [];
141+
142+
const material = createColorMaterial(colorNumber, this.ambientLight, this.directionalLight, this.brightness);
143+
144+
this.materials.push(material);
145+
146+
paths.forEach((path) => {
147+
const geometry = path.geometry({
148+
extrusionWidthOverride: this.extrusionWidth,
149+
lineHeightOverride: this.lineHeight
150+
});
151+
152+
if (!geometry) return;
153+
154+
this.disposables.push(geometry);
155+
geometries.push(geometry);
156+
});
157+
158+
const batchedMesh = this.createBatchMesh(geometries, material);
159+
this.disposables.push(material);
160+
return batchedMesh;
161+
}
162+
163+
/**
164+
* Creates a batched mesh from multiple geometries sharing the same material
165+
* @param geometries - Array of geometries to batch
166+
* @param material - Material to use for the batched mesh
167+
* @returns Batched mesh instance
168+
*/
169+
private createBatchMesh(geometries: BufferGeometry[], material: Material): BatchedMesh {
170+
const maxVertexCount = geometries.reduce((acc, geometry) => geometry.attributes.position.count * 3 + acc, 0);
171+
172+
const batchedMesh = new BatchedMesh(geometries.length, maxVertexCount, undefined, material);
173+
this.disposables.push(batchedMesh);
174+
175+
geometries.forEach((geometry) => {
176+
const geometryId = batchedMesh.addGeometry(geometry);
177+
// NOTE: for older versions of three.js, addInstance is not available
178+
// This allow webgl1 browsers to use the batched mesh
179+
batchedMesh.addInstance?.(geometryId);
180+
});
181+
182+
return batchedMesh;
183+
}
184+
185+
/**
186+
* Applies clipping planes to the specified material based on the minimum and maximum Z values.
187+
*
188+
* This method creates clipping planes for the top and bottom of the specified Z range,
189+
* then applies them to the material's clippingPlanes property.
190+
*
191+
* @param material - Shader material to apply clipping planes to
192+
* @param minZ - The minimum Z value for the clipping plane.
193+
* @param maxZ - The maximum Z value for the clipping plane.
194+
*/
195+
private createClippingPlanes(minZ?: number | undefined, maxZ?: number | undefined) {
196+
const planes = [];
197+
if (minZ !== undefined) {
198+
planes.push(new Plane(new Vector3(0, 1, 0), -minZ));
199+
}
200+
if (maxZ !== undefined) {
201+
planes.push(new Plane(new Vector3(0, -1, 0), maxZ));
202+
}
203+
return planes;
204+
}
205+
206+
/**
207+
* Updates the clipping planes for all `LineSegments2` objects in the scene.
208+
* This method filters the scene's children to find instances of `LineSegments2`,
209+
* then applies the clipping planes to their materials.
210+
*
211+
* @param minZ - The minimum Z value for the clipping plane.
212+
* @param maxZ - The maximum Z value for the clipping plane.
213+
*/
214+
private updateLineClipping(minZ: number | undefined, maxZ: number | undefined) {
215+
// TODO: apply clipping selectively to travels lines and extrusion lines
216+
// and/or use a clipping group
217+
this.scene.traverse((obj) => {
218+
if (obj instanceof LineSegments2) {
219+
const material = obj.material as LineMaterial;
220+
material.clippingPlanes = this.createClippingPlanes(minZ, maxZ);
221+
}
222+
});
223+
}
224+
225+
/**
226+
* Updates the clipping planes for all shader materials in the scene.
227+
* This method sets the min and max Z values for the clipping planes in the shader materials.
228+
*
229+
* @param minZ - The minimum Z value for the clipping plane.
230+
* @param maxZ - The maximum Z value for the clipping plane
231+
*/
232+
233+
private updateClippingPlanesForShaderMaterials(minZ: number, maxZ: number) {
234+
this.materials.forEach((material) => {
235+
material.uniforms.clipMinY.value = minZ;
236+
material.uniforms.clipMaxY.value = maxZ;
237+
});
238+
}
239+
240+
/**
241+
* Creates a new Three.js group for organizing rendered paths
242+
* @param name - Name for the group
243+
* @returns Configured Three.js group
244+
* @remarks
245+
* Sets up the group's orientation and position based on build volume dimensions.
246+
* If no build volume is defined, uses a default position.
247+
*/
248+
private createGroup(name: string): Group {
249+
const group = new Group();
250+
group.name = name;
251+
group.quaternion.setFromEuler(new Euler(-Math.PI / 2, 0, 0));
252+
return group;
253+
}
254+
}

0 commit comments

Comments
 (0)