From 20d99b58150a93a4eddc068f105bffa21fa0912b Mon Sep 17 00:00:00 2001 From: Thomas Fuhrmann Date: Wed, 5 Mar 2025 09:25:23 +0100 Subject: [PATCH] Working clipping functionality --- app/three/utils/build-clipping-plane.ts | 305 +++++++++++++++++++++--- app/three/utils/build-grid.ts | 46 +++- app/three/utils/build-meshes.ts | 2 +- app/three/utils/init.ts | 10 +- 4 files changed, 313 insertions(+), 50 deletions(-) diff --git a/app/three/utils/build-clipping-plane.ts b/app/three/utils/build-clipping-plane.ts index 2106e18..fc50c60 100644 --- a/app/three/utils/build-clipping-plane.ts +++ b/app/three/utils/build-clipping-plane.ts @@ -2,6 +2,7 @@ import { DoubleSide, Mesh, MeshBasicMaterial, + Object3DEventMap, PerspectiveCamera, Plane, PlaneGeometry, @@ -10,60 +11,304 @@ import { } from "three"; import { DragControls, OrbitControls } from "three/examples/jsm/Addons.js"; import { Extent } from "./build-scene"; -import { getCenter3D } from "./utils"; -export function createClippingPlane( +enum Orientation { + X = "x", + Y = "y", + Z = "z", +} + +type PlaneMesh = Mesh; +type PlaneMeshMap = { + [key in Orientation]: PlaneMesh; +}; + +export function createClippingPlanes( renderer: WebGLRenderer, camera: PerspectiveCamera, orbitControls: OrbitControls, extent: Extent ) { - const center = getCenter3D(extent); + const planesData = [ + { + normal: new Vector3(1, 0, 0), + d: -extent.xmin, + orientation: Orientation.X, + }, + { + normal: new Vector3(0, 1, 0), + d: -extent.ymin, + orientation: Orientation.Y, + }, + { + normal: new Vector3(0, 0, -1), + d: extent.zmax, + orientation: Orientation.Z, + }, + ]; - const width = extent.xmax - extent.xmin; - const height = extent.ymax - extent.ymin; - const d = extent.zmax; + const planeMeshes: Mesh< + PlaneGeometry, + MeshBasicMaterial, + Object3DEventMap + >[] = []; + const planes: Plane[] = []; + let planeMeshMap = {} as Partial; + for (let p of planesData) { + let name; + let planeCenter; + let width; + let height; + if (p.orientation === Orientation.X) { + name = Orientation.X; + width = extent.ymax - extent.ymin; + height = extent.zmax - extent.zmin; + planeCenter = new Vector3( + -p.d, + extent.ymax - width / 2, + extent.zmax - height / 2 + ); + } else if (p.orientation === Orientation.Y) { + name = Orientation.Y; + width = extent.xmax - extent.xmin; + height = extent.zmax - extent.zmin; + planeCenter = new Vector3( + extent.xmax - width / 2, + -p.d, + extent.zmax - height / 2 + ); + } else { + name = Orientation.Z; + width = extent.xmax - extent.xmin; + height = extent.ymax - extent.ymin; + planeCenter = new Vector3( + extent.xmax - width / 2, + extent.ymax - height / 2, + p.d + ); + } - // Visual representation of the clipping Plane - // Plane is given in Hesse normal form - const normalVector = new Vector3(0, 0, -1); - const plane = new Plane(normalVector, d); + // Visual representation of the clipping plane + // Plane is given in Hesse normal form + const plane = new Plane(p.normal, p.d); - // Dragging Mechanism - const planeMesh = new Mesh( - new PlaneGeometry(width, height), - new MeshBasicMaterial({ - visible: true, - color: 0xff0000, - transparent: true, - opacity: 0.1, - side: DoubleSide, - }) - ); - planeMesh.position.set(center.x, center.y, d); + // Dragging Mechanism + const planeMesh = new Mesh( + new PlaneGeometry(width, height), + new MeshBasicMaterial({ + visible: true, + color: 0xff0000, + transparent: true, + opacity: 0.1, + side: DoubleSide, + clipIntersection: false, + }) + ); + planeMesh.name = name; + planeMesh.userData.plane = plane; + planeMesh.position.set(planeCenter.x, planeCenter.y, planeCenter.z); + if (p.orientation === Orientation.X) { + planeMesh.rotateY(Math.PI / 2); + planeMesh.rotateZ(Math.PI / 2); + } else if (p.orientation === Orientation.Y) { + planeMesh.rotateX(Math.PI / 2); + } + planeMeshes.push(planeMesh); + planes.push(plane); + + planeMeshMap[p.orientation] = planeMesh; + } + + for (let pm of planeMeshes) { + // Let clipping planes clip each other + const clippingPlanes = planes.filter( + (p) => !p.normal.equals(pm.userData.plane.normal) + ); + + pm.material.clippingPlanes = clippingPlanes; + } + + // Enable DragControls for the clipping planes const dragControls = new DragControls( - [planeMesh], + planeMeshes, camera, renderer.domElement ); - // Disable OrbitControls when dragging starts dragControls.addEventListener("dragstart", () => { + // Disable OrbitControls when dragging starts orbitControls.enabled = false; }); - // Re-enable OrbitControls when dragging ends dragControls.addEventListener("dragend", () => { + // Reenable OrbitControls when dragging ends orbitControls.enabled = true; }); dragControls.addEventListener("drag", (event) => { - const newZ = event.object.position.z; - plane.constant = newZ; - planeMesh.position.x = center.x; - planeMesh.position.y = center.y; + const object = event.object as PlaneMesh; + const plane = event.object.userData.plane; + const width = object.geometry.parameters.width; + const height = object.geometry.parameters.height; + if (object.name === Orientation.Z) { + // Fix rotation of dragged mesh + event.object.rotation.set(0, 0, 0); + + let newZ; + if (event.object.position.z > extent.zmax) { + newZ = extent.zmax; + } else if (event.object.position.z < extent.zmin) { + newZ = extent.zmin; + } else { + newZ = event.object.position.z; + } + + // Reset position of plane + plane.constant = newZ; + + // Set position of dragged mesh + object.position.x = extent.xmax - width / 2; + object.position.y = extent.ymax - height / 2; + object.position.z = newZ; + + // Resize other meshes + resizeMeshes(Orientation.Z, newZ, planeMeshMap as PlaneMeshMap, extent); + } else if (object.name === Orientation.Y) { + // Fix rotation of dragged mesh + event.object.rotation.set(Math.PI / 2, 0, 0); + + let newY; + if (event.object.position.y > extent.ymax) { + newY = extent.ymax; + } else if (event.object.position.y < extent.ymin) { + newY = extent.ymin; + } else { + newY = event.object.position.y; + } + + // Reset position of plane + plane.constant = -newY; + + // Set position of dragged mesh + object.position.x = extent.xmax - width / 2; + object.position.y = newY; + object.position.z = extent.zmax - height / 2; + + // Resize other meshes + resizeMeshes(Orientation.Y, newY, planeMeshMap as PlaneMeshMap, extent); + } else { + // Fix rotation of dragged mesh + event.object.rotation.set(0, Math.PI / 2, Math.PI / 2); + + let newX; + if (event.object.position.x > extent.xmax) { + newX = extent.xmax; + } else if (event.object.position.x < extent.xmin) { + newX = extent.xmin; + } else { + newX = event.object.position.x; + } + + // Reset position of plane + plane.constant = -newX; + + // Set position of dragged mesh + object.position.x = newX; + object.position.y = extent.ymax - width / 2; + object.position.z = extent.zmax - height / 2; + + // Resize other meshes + resizeMeshes(Orientation.X, newX, planeMeshMap as PlaneMeshMap, extent); + } }); - return { planeMesh, plane }; + return { planeMeshes, planes }; +} + +function resizeMeshes( + orientation: Orientation, + newCoordinate: number, + planeMeshes: PlaneMeshMap, + extent: Extent +) { + if (orientation === Orientation.X) { + // Resize y-clipping plane + let planeMesh = planeMeshes[Orientation.Y]; + let width = extent.xmax - newCoordinate; + let height = planeMesh.geometry.parameters.height; + const y = planeMesh.position.y; + planeMesh.geometry.dispose(); + planeMesh.geometry = new PlaneGeometry(width, height); + planeMesh.position.set( + extent.xmax - width / 2, + y, + extent.zmax - height / 2 + ); + + // Resize z-clipping-plane + planeMesh = planeMeshes[Orientation.Z]; + width = extent.xmax - newCoordinate; + height = planeMesh.geometry.parameters.height; + const z = planeMesh.position.z; + planeMesh.geometry.dispose(); + planeMesh.geometry = new PlaneGeometry(width, height); + planeMesh.position.set( + extent.xmax - width / 2, + extent.ymax - height / 2, + z + ); + } else if (orientation === Orientation.Y) { + // Resize x-clipping plane + let planeMesh = planeMeshes[Orientation.X]; + let width = extent.ymax - newCoordinate; + let height = planeMesh.geometry.parameters.height; + const x = planeMesh.position.x; + planeMesh.geometry.dispose(); + planeMesh.geometry = new PlaneGeometry(width, height); + planeMesh.position.set( + x, + extent.ymax - width / 2, + extent.zmax - height / 2 + ); + + // Resize z-clipping-plane + planeMesh = planeMeshes[Orientation.Z]; + width = planeMesh.geometry.parameters.width; + height = extent.ymax - newCoordinate; + const z = planeMesh.position.z; + planeMesh.geometry.dispose(); + planeMesh.geometry = new PlaneGeometry(width, height); + planeMesh.position.set( + extent.xmax - width / 2, + extent.ymax - height / 2, + z + ); + } else if (orientation === Orientation.Z) { + // Resize x-clipping-plane + let planeMesh = planeMeshes[Orientation.X]; + let width = planeMesh.geometry.parameters.width; + let height = newCoordinate - extent.zmin; + const x = planeMesh.position.x; + planeMesh.geometry.dispose(); + planeMesh.geometry = new PlaneGeometry(width, height); + planeMesh.position.set( + x, + extent.ymax - width / 2, + extent.zmax - height / 2 + ); + + // Resize y-clipping plane + planeMesh = planeMeshes[Orientation.Y]; + width = planeMesh.geometry.parameters.width; + height = newCoordinate - extent.zmin; + const y = planeMesh.position.y; + planeMesh.geometry.dispose(); + planeMesh.geometry = new PlaneGeometry(width, height); + planeMesh.position.set( + extent.xmax - width / 2, + y, + extent.zmax - height / 2 + ); + } } diff --git a/app/three/utils/build-grid.ts b/app/three/utils/build-grid.ts index 221cab1..d4b3326 100644 --- a/app/three/utils/build-grid.ts +++ b/app/three/utils/build-grid.ts @@ -3,12 +3,16 @@ import { GridHelper, Sprite, SpriteMaterial, - Vector3, Vector4, } from "three"; import { Extent } from "./build-scene"; import { getCenter3D } from "./utils"; +enum Orientation { + Horizontal, + Vertical, +} + export function buildGrid(extent: Extent) { const center = getCenter3D(extent); // Calculate the width and height of the grid @@ -38,7 +42,7 @@ export function buildGrid(extent: Extent) { const z = positionAttr.getZ(i); const v = new Vector4(x + center.x, z + center.y, 0, 1); - if (i % 4 === 0 && i > 4) { + if (i % 4 === 0) { startingPointsVertical.push(v); } else if (i % 2 == 0) { startingPointsHorizontal.push(v); @@ -47,12 +51,20 @@ export function buildGrid(extent: Extent) { const annotations = []; for (let point of startingPointsHorizontal) { - const label = createLabel(`${point.x.toFixed(2)}`, point); + const label = createLabel( + `${point.x.toFixed(2)}`, + point, + Orientation.Horizontal + ); annotations.push(label); } for (let point of startingPointsVertical) { - const label = createLabel(`${point.y.toFixed(2)}`, point); + const label = createLabel( + `${point.y.toFixed(2)}`, + point, + Orientation.Vertical + ); annotations.push(label); } @@ -60,31 +72,39 @@ export function buildGrid(extent: Extent) { } // Function to create annotation (sprite with text) -function createLabel(text: string, position: Vector4) { +function createLabel( + text: string, + position: Vector4, + orientation: Orientation +) { const spriteMaterial = new SpriteMaterial({ - map: new CanvasTexture(generateTextCanvas(text)), // Create text texture + map: new CanvasTexture(generateTextCanvas(text, orientation)), // Create text texture transparent: true, }); const sprite = new Sprite(spriteMaterial); sprite.position.set(position.x, position.y, position.z); - sprite.scale.set(10000, 5000, 1); // Scale the sprite to make the text readable + sprite.scale.set(5000, 2500, 1); // Scale the sprite to make the text readable return sprite; } // Function to generate a text canvas for the annotation -function generateTextCanvas(text: string) { +function generateTextCanvas(text: string, orientation: Orientation) { const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); if (context) { - // Set a background color for the canvas to make it visible - canvas.width = 800; // Set a fixed width for the canvas - canvas.height = 160; // Set a fixed height for the canvas + canvas.width = 800; + canvas.height = 160; // Set the text style context.font = "45px Arial"; - context.fillStyle = "black"; // Text color - context.fillText(text, 400, 80); // Draw the text on the canvas + context.fillStyle = "black"; + + if (orientation === Orientation.Horizontal) { + context.fillText(text, 300, 160); + } else { + context.fillText(text, 100, 90); + } } return canvas; diff --git a/app/three/utils/build-meshes.ts b/app/three/utils/build-meshes.ts index d671523..c7f356e 100644 --- a/app/three/utils/build-meshes.ts +++ b/app/three/utils/build-meshes.ts @@ -54,7 +54,7 @@ async function buildMesh(layerData: MappedFeature, clippingPlanes: Plane[]) { side: DoubleSide, wireframe: false, clippingPlanes: clippingPlanes, - clipIntersection: true, + clipIntersection: false, }); // material.onBeforeCompile = (materialShader) => { diff --git a/app/three/utils/init.ts b/app/three/utils/init.ts index 35204bf..facaaa7 100644 --- a/app/three/utils/init.ts +++ b/app/three/utils/init.ts @@ -3,7 +3,7 @@ import { buildMeshes } from "./build-meshes"; import { Extent, buildScene } from "./build-scene"; import { getMetadata } from "./get-metadata"; import { MODEL_ID, SERVICE_URL } from "../config"; -import { createClippingPlane } from "./build-clipping-plane"; +import { createClippingPlanes } from "./build-clipping-plane"; import { buildGrid } from "./build-grid"; export async function init(container: HTMLElement) { @@ -25,17 +25,15 @@ export async function init(container: HTMLElement) { extent ); - const { planeMesh, plane } = createClippingPlane( + const { planeMeshes, planes } = createClippingPlanes( renderer, camera, controls, extent ); - scene.add(planeMesh); + scene.add(...planeMeshes); - const clippingPlanes = [plane]; - - const meshes = await buildMeshes(mappedFeatures, clippingPlanes); + const meshes = await buildMeshes(mappedFeatures, planes); const mappedFeaturesGroup = new Group(); mappedFeaturesGroup.add(...meshes); scene.add(mappedFeaturesGroup);