diff --git a/app/three/utils/build-clipping-plane.ts b/app/three/utils/build-clipping-plane.ts new file mode 100644 index 0000000..2106e18 --- /dev/null +++ b/app/three/utils/build-clipping-plane.ts @@ -0,0 +1,69 @@ +import { + DoubleSide, + Mesh, + MeshBasicMaterial, + PerspectiveCamera, + Plane, + PlaneGeometry, + Vector3, + WebGLRenderer, +} from "three"; +import { DragControls, OrbitControls } from "three/examples/jsm/Addons.js"; +import { Extent } from "./build-scene"; +import { getCenter3D } from "./utils"; + +export function createClippingPlane( + renderer: WebGLRenderer, + camera: PerspectiveCamera, + orbitControls: OrbitControls, + extent: Extent +) { + const center = getCenter3D(extent); + + const width = extent.xmax - extent.xmin; + const height = extent.ymax - extent.ymin; + const d = extent.zmax; + + // 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); + + // 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); + + const dragControls = new DragControls( + [planeMesh], + camera, + renderer.domElement + ); + + // Disable OrbitControls when dragging starts + dragControls.addEventListener("dragstart", () => { + orbitControls.enabled = false; + }); + + // Re-enable OrbitControls when dragging ends + dragControls.addEventListener("dragend", () => { + 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; + }); + + return { planeMesh, plane }; +} diff --git a/app/three/utils/build-grid.ts b/app/three/utils/build-grid.ts new file mode 100644 index 0000000..221cab1 --- /dev/null +++ b/app/three/utils/build-grid.ts @@ -0,0 +1,91 @@ +import { + CanvasTexture, + GridHelper, + Sprite, + SpriteMaterial, + Vector3, + Vector4, +} from "three"; +import { Extent } from "./build-scene"; +import { getCenter3D } from "./utils"; + +export function buildGrid(extent: Extent) { + const center = getCenter3D(extent); + // Calculate the width and height of the grid + const gridWidth = extent.xmax - extent.xmin; + const gridHeight = extent.ymax - extent.ymin; + + // Decide on the number of divisions (e.g., 20 divisions along each axis) + const divisions = 20; + + // Create a grid helper with the calculated grid size and divisions + const gridHelper = new GridHelper(Math.max(gridWidth, gridHeight), divisions); + + // Position the grid in the scene to match the given extent + gridHelper.position.set(center.x, center.y, 0); // Center the grid at the midpoint + + // Rotate the grid if needed to align with the world coordinates + gridHelper.rotation.x = Math.PI / 2; // Rotate to align with the XY plane + + // Retrieve the geometry of the grid helper + const geometry = gridHelper.geometry; + + const positionAttr = geometry.getAttribute("position"); + const startingPointsHorizontal = []; + const startingPointsVertical = []; + for (let i = 0; i < positionAttr.count; i++) { + const x = positionAttr.getX(i); + const z = positionAttr.getZ(i); + const v = new Vector4(x + center.x, z + center.y, 0, 1); + + if (i % 4 === 0 && i > 4) { + startingPointsVertical.push(v); + } else if (i % 2 == 0) { + startingPointsHorizontal.push(v); + } + } + + const annotations = []; + for (let point of startingPointsHorizontal) { + const label = createLabel(`${point.x.toFixed(2)}`, point); + annotations.push(label); + } + + for (let point of startingPointsVertical) { + const label = createLabel(`${point.y.toFixed(2)}`, point); + annotations.push(label); + } + + return { gridHelper, annotations }; +} + +// Function to create annotation (sprite with text) +function createLabel(text: string, position: Vector4) { + const spriteMaterial = new SpriteMaterial({ + map: new CanvasTexture(generateTextCanvas(text)), // 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 + return sprite; +} + +// Function to generate a text canvas for the annotation +function generateTextCanvas(text: string) { + 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 + + // Set the text style + context.font = "45px Arial"; + context.fillStyle = "black"; // Text color + context.fillText(text, 400, 80); // Draw the text on the canvas + } + + return canvas; +} diff --git a/app/three/utils/build-meshes.ts b/app/three/utils/build-meshes.ts index b7e66f0..d671523 100644 --- a/app/three/utils/build-meshes.ts +++ b/app/three/utils/build-meshes.ts @@ -6,6 +6,10 @@ import { Group, Mesh, MeshStandardMaterial, + Plane, + PlaneHelper, + Scene, + Vector3, } from "three"; import { uniforms } from "./uniforms"; @@ -22,7 +26,7 @@ interface MappedFeature { preview: { legend_color: string; legend_text: string }; } -async function buildMesh(layerData: MappedFeature) { +async function buildMesh(layerData: MappedFeature, clippingPlanes: Plane[]) { const color = `#${layerData.preview.legend_color}`; const name = layerData.preview.legend_text; const geomId = layerData.featuregeom_id.toString(); @@ -47,8 +51,10 @@ async function buildMesh(layerData: MappedFeature) { metalness: 0.1, roughness: 0.75, flatShading: true, - side: FrontSide, + side: DoubleSide, wireframe: false, + clippingPlanes: clippingPlanes, + clipIntersection: true, }); // material.onBeforeCompile = (materialShader) => { @@ -66,20 +72,20 @@ async function buildMesh(layerData: MappedFeature) { mesh.castShadow = true; mesh.receiveShadow = true; - // modelNode should be a THREE.Group object where all the model data gets added to - // in the original code modelNode is a direct reference to a THREE.Scene - // if (modelNode) { - // modelNode.add(mesh); - // } return mesh; } -export async function buildMeshes(mappedFeatures: MappedFeature[]) { +export async function buildMeshes( + mappedFeatures: MappedFeature[], + clippingPlanes: Plane[] +) { const meshes = []; for (let i = 0; i < mappedFeatures.length; i++) { const layerData = mappedFeatures[i]; - const mesh = await buildMesh(layerData); - meshes.push(mesh); + if (layerData.name !== "Topography") { + const mesh = await buildMesh(layerData, clippingPlanes); + meshes.push(mesh); + } } return meshes; diff --git a/app/three/utils/build-scene.ts b/app/three/utils/build-scene.ts index e1f8d4a..3b49a91 100644 --- a/app/three/utils/build-scene.ts +++ b/app/three/utils/build-scene.ts @@ -1,16 +1,8 @@ -import { - BoxGeometry, - Camera, - Mesh, - MeshBasicMaterial, - PerspectiveCamera, - Scene, - Vector3, - WebGLRenderer, -} from "three"; +import { Color, PerspectiveCamera, Scene, Vector3, WebGLRenderer } from "three"; import { buildDefaultLights } from "./build-default-lights"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; +import { getCenter3D, getMaxSize } from "./utils"; export interface Extent { xmin: number; @@ -26,34 +18,30 @@ let renderer: WebGLRenderer; let camera: PerspectiveCamera; let scene: Scene; export async function buildScene(container: HTMLElement, extent: Extent) { - const size = Math.max( - extent.xmax - extent.xmin, - extent.ymax - extent.ymin, - extent.zmax - extent.zmin - ); - - const center = new Vector3( - (extent.xmin + extent.xmax) / 2, - (extent.ymin + extent.ymax) / 2, - 0 - ); + const maxSize = getMaxSize(extent); + const center = getCenter3D(extent); const width = container.clientWidth; const height = container.clientHeight; - camera = new PerspectiveCamera(30, width / height, 0.1, size * 25); - camera.position.set(center.x, center.y, size * 5); + camera = new PerspectiveCamera( + 50, + width / height, + maxSize * 0.1, + maxSize * 25 + ); + + camera.position.set(center.x, center.y, extent.zmax + 150000); camera.lookAt(center); renderer = new WebGLRenderer({ alpha: true, + logarithmicDepthBuffer: true, }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(width, height); renderer.localClippingEnabled = true; - // renderer.autoClear = false; - // renderer.setClearColor(0x000000, 0.0); // second param is opacity, 0 => transparent renderer.setAnimationLoop(animate); window.addEventListener("resize", () => onWindowResize(container)); @@ -62,18 +50,16 @@ export async function buildScene(container: HTMLElement, extent: Extent) { controls = new OrbitControls(camera, renderer.domElement); controls.target.set(center.x, center.y, center.z); // Focus on the center controls.enableDamping = true; // Smooth camera movement + controls.maxDistance = maxSize * 5; controls.update(); // Scene will hold all our elements such as objects, cameras and lights scene = new Scene(); + scene.background = new Color(0xdddddd); buildDefaultLights(scene); - // const queryString = window.location.search; - // const urlParams = new URLSearchParams(queryString); - // const modelid = parseInt(urlParams.get("model_id") ?? "20", 10); - - return { renderer, scene, camera }; + return { renderer, scene, camera, controls }; } function onWindowResize(container: HTMLElement) { diff --git a/app/three/utils/init.ts b/app/three/utils/init.ts index 690c996..35204bf 100644 --- a/app/three/utils/init.ts +++ b/app/three/utils/init.ts @@ -1,8 +1,10 @@ -import { Group, Vector3 } from "three"; +import { AxesHelper, Group } from "three"; 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 { buildGrid } from "./build-grid"; export async function init(container: HTMLElement) { const modelData = await getMetadata(SERVICE_URL + MODEL_ID); @@ -18,11 +20,31 @@ export async function init(container: HTMLElement) { zmax: modelarea.z.max, }; - const { renderer, scene, camera } = await buildScene(container, extent); - const meshes = await buildMeshes(mappedFeatures); + const { renderer, scene, camera, controls } = await buildScene( + container, + extent + ); + const { planeMesh, plane } = createClippingPlane( + renderer, + camera, + controls, + extent + ); + scene.add(planeMesh); + + const clippingPlanes = [plane]; + + const meshes = await buildMeshes(mappedFeatures, clippingPlanes); const mappedFeaturesGroup = new Group(); mappedFeaturesGroup.add(...meshes); scene.add(mappedFeaturesGroup); - // scene.add(meshes[8]); + + const { gridHelper, annotations } = buildGrid(extent); + const annotationsGroup = new Group(); + annotationsGroup.add(...annotations); + scene.add(gridHelper, annotationsGroup); + + //const axesHelper = new AxesHelper(5); + //scene.add(axesHelper); } diff --git a/app/three/utils/utils.ts b/app/three/utils/utils.ts new file mode 100644 index 0000000..841f36f --- /dev/null +++ b/app/three/utils/utils.ts @@ -0,0 +1,18 @@ +import { Vector3 } from "three"; +import { Extent } from "./build-scene"; + +export function getMaxSize(extent: Extent) { + return Math.max( + extent.xmax - extent.xmin, + extent.ymax - extent.ymin, + extent.zmax - extent.zmin + ); +} + +export function getCenter3D(extent: Extent) { + return new Vector3( + (extent.xmin + extent.xmax) / 2, + (extent.ymin + extent.ymax) / 2, + (extent.zmax + extent.zmin) / 2 + ); +}