|
| 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