From 05108b6091eac06123677fface1986fcb3e59e6d Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Tue, 12 May 2026 22:27:26 +0200 Subject: [PATCH] Feature: Add path segment rendering and point outlines --- src/viewport-three/viewport-host.ts | 146 +++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 3 deletions(-) diff --git a/src/viewport-three/viewport-host.ts b/src/viewport-three/viewport-host.ts index c3e368c4..db253153 100644 --- a/src/viewport-three/viewport-host.ts +++ b/src/viewport-three/viewport-host.ts @@ -7454,6 +7454,104 @@ export class ViewportHost { return new BufferGeometry().setFromPoints(points); } + private createPathPointVectors(path: ScenePath): Vector3[] { + const points = path.points.map( + (point) => + new Vector3(point.position.x, point.position.y, point.position.z) + ); + + if (path.loop && points.length > 1) { + points.push(points[0].clone()); + } + + return points; + } + + private createPathSegmentMesh( + start: Vector3, + end: Vector3, + radius: number, + material: MeshBasicMaterial, + pathId: string + ): Mesh | null { + const delta = end.clone().sub(start); + const length = delta.length(); + + if (length < 0.0001) { + return null; + } + + const mesh = new Mesh( + new CylinderGeometry(radius, radius, length, 10, 1), + material + ); + const direction = delta.normalize(); + + mesh.position.copy(start).add(end).multiplyScalar(0.5); + mesh.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), direction); + mesh.renderOrder = PATH_RENDER_ORDER; + mesh.userData.pathId = pathId; + + return mesh; + } + + private createPathSegmentMeshes(path: ScenePath): PathRenderObjects["segments"] { + const points = this.createPathPointVectors(path); + const segments: PathRenderObjects["segments"] = []; + + for (let index = 0; index < points.length - 1; index += 1) { + const outlineMesh = this.createPathSegmentMesh( + points[index], + points[index + 1], + PATH_SEGMENT_OUTLINE_RADIUS, + new MeshBasicMaterial({ + color: PATH_OUTLINE_COLOR, + depthTest: false, + depthWrite: false + }), + path.id + ); + const mesh = this.createPathSegmentMesh( + points[index], + points[index + 1], + PATH_SEGMENT_RADIUS, + new MeshBasicMaterial({ + color: PATH_COLOR, + depthTest: false, + depthWrite: false + }), + path.id + ); + + if (outlineMesh !== null && mesh !== null) { + segments.push({ + outlineMesh, + mesh + }); + } + } + + return segments; + } + + private addPathSegmentMeshes(segments: PathRenderObjects["segments"]) { + for (const segment of segments) { + this.pathGroup.add(segment.outlineMesh); + this.pathGroup.add(segment.mesh); + } + } + + private disposePathSegmentMeshes(segments: PathRenderObjects["segments"]) { + for (const segment of segments) { + this.pathGroup.remove(segment.outlineMesh); + this.pathGroup.remove(segment.mesh); + segment.outlineMesh.geometry.dispose(); + segment.outlineMesh.material.dispose(); + segment.mesh.geometry.dispose(); + segment.mesh.material.dispose(); + } + } + private rebuildPaths(document: SceneDocument, selection: EditorSelection) { this.clearPaths(); @@ -7467,7 +7565,9 @@ export class ViewportHost { const renderObjects = this.createPathRenderObjects(path, selection); this.pathGroup.add(renderObjects.line); + this.addPathSegmentMeshes(renderObjects.segments); for (const pointMesh of renderObjects.pointMeshes) { + this.pathGroup.add(pointMesh.outlineMesh); this.pathGroup.add(pointMesh.mesh); } this.pathRenderObjects.set(path.id, renderObjects); @@ -7486,33 +7586,59 @@ export class ViewportHost { new LineBasicMaterial({ color: isPathSelected(selection, path.id) ? PATH_SELECTED_COLOR - : PATH_COLOR + : PATH_COLOR, + depthTest: false, + depthWrite: false, + linewidth: PATH_LINE_WIDTH }) ); + line.renderOrder = PATH_RENDER_ORDER + 1; line.userData.pathId = path.id; + const segments = this.createPathSegmentMeshes(path); const pointMeshes = path.points.map((point) => { + const outlineMesh = new Mesh( + new SphereGeometry(PATH_POINT_OUTLINE_RADIUS, 16, 12), + new MeshBasicMaterial({ + color: PATH_OUTLINE_COLOR, + depthTest: false, + depthWrite: false + }) + ); const mesh = new Mesh( - new SphereGeometry(PATH_POINT_RADIUS, 12, 12), + new SphereGeometry(PATH_POINT_RADIUS, 16, 12), new MeshBasicMaterial({ color: isPathSelected(selection, path.id) ? PATH_POINT_SELECTED_COLOR - : PATH_POINT_COLOR + : PATH_POINT_COLOR, + depthTest: false, + depthWrite: false }) ); + outlineMesh.position.set( + point.position.x, + point.position.y, + point.position.z + ); mesh.position.set(point.position.x, point.position.y, point.position.z); + outlineMesh.renderOrder = PATH_RENDER_ORDER + 2; + mesh.renderOrder = PATH_RENDER_ORDER + 3; + outlineMesh.userData.pathId = path.id; + outlineMesh.userData.pathPointId = point.id; mesh.userData.pathId = path.id; mesh.userData.pathPointId = point.id; return { pointId: point.id, + outlineMesh, mesh }; }); return { line, + segments, pointMeshes }; } @@ -7521,9 +7647,13 @@ export class ViewportHost { this.pathGroup.remove(renderObjects.line); renderObjects.line.geometry.dispose(); renderObjects.line.material.dispose(); + this.disposePathSegmentMeshes(renderObjects.segments); for (const pointMesh of renderObjects.pointMeshes) { + this.pathGroup.remove(pointMesh.outlineMesh); this.pathGroup.remove(pointMesh.mesh); + pointMesh.outlineMesh.geometry.dispose(); + pointMesh.outlineMesh.material.dispose(); pointMesh.mesh.geometry.dispose(); pointMesh.mesh.material.dispose(); } @@ -7555,7 +7685,9 @@ export class ViewportHost { this.currentSelection ); this.pathGroup.add(renderObjects.line); + this.addPathSegmentMeshes(renderObjects.segments); for (const pointMesh of renderObjects.pointMeshes) { + this.pathGroup.add(pointMesh.outlineMesh); this.pathGroup.add(pointMesh.mesh); } this.pathRenderObjects.set(pathId, renderObjects); @@ -7577,6 +7709,9 @@ export class ViewportHost { renderObjects.line.geometry.dispose(); renderObjects.line.geometry = this.createPathLineGeometry(path); + this.disposePathSegmentMeshes(renderObjects.segments); + renderObjects.segments = this.createPathSegmentMeshes(path); + this.addPathSegmentMeshes(renderObjects.segments); for (const pointMesh of renderObjects.pointMeshes) { const point = path.points.find( @@ -7587,6 +7722,11 @@ export class ViewportHost { continue; } + pointMesh.outlineMesh.position.set( + point.position.x, + point.position.y, + point.position.z + ); pointMesh.mesh.position.set( point.position.x, point.position.y,