Add grid; clipping plane

This commit is contained in:
Fuhrmann 2025-03-03 14:40:21 +01:00
parent 0d0190fd8e
commit 13be63c40a
6 changed files with 236 additions and 44 deletions

View file

@ -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 };
}

View file

@ -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;
}

View file

@ -6,6 +6,10 @@ import {
Group, Group,
Mesh, Mesh,
MeshStandardMaterial, MeshStandardMaterial,
Plane,
PlaneHelper,
Scene,
Vector3,
} from "three"; } from "three";
import { uniforms } from "./uniforms"; import { uniforms } from "./uniforms";
@ -22,7 +26,7 @@ interface MappedFeature {
preview: { legend_color: string; legend_text: string }; 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 color = `#${layerData.preview.legend_color}`;
const name = layerData.preview.legend_text; const name = layerData.preview.legend_text;
const geomId = layerData.featuregeom_id.toString(); const geomId = layerData.featuregeom_id.toString();
@ -47,8 +51,10 @@ async function buildMesh(layerData: MappedFeature) {
metalness: 0.1, metalness: 0.1,
roughness: 0.75, roughness: 0.75,
flatShading: true, flatShading: true,
side: FrontSide, side: DoubleSide,
wireframe: false, wireframe: false,
clippingPlanes: clippingPlanes,
clipIntersection: true,
}); });
// material.onBeforeCompile = (materialShader) => { // material.onBeforeCompile = (materialShader) => {
@ -66,21 +72,21 @@ async function buildMesh(layerData: MappedFeature) {
mesh.castShadow = true; mesh.castShadow = true;
mesh.receiveShadow = 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; return mesh;
} }
export async function buildMeshes(mappedFeatures: MappedFeature[]) { export async function buildMeshes(
mappedFeatures: MappedFeature[],
clippingPlanes: Plane[]
) {
const meshes = []; const meshes = [];
for (let i = 0; i < mappedFeatures.length; i++) { for (let i = 0; i < mappedFeatures.length; i++) {
const layerData = mappedFeatures[i]; const layerData = mappedFeatures[i];
const mesh = await buildMesh(layerData); if (layerData.name !== "Topography") {
const mesh = await buildMesh(layerData, clippingPlanes);
meshes.push(mesh); meshes.push(mesh);
} }
}
return meshes; return meshes;
} }

View file

@ -1,16 +1,8 @@
import { import { Color, PerspectiveCamera, Scene, Vector3, WebGLRenderer } from "three";
BoxGeometry,
Camera,
Mesh,
MeshBasicMaterial,
PerspectiveCamera,
Scene,
Vector3,
WebGLRenderer,
} from "three";
import { buildDefaultLights } from "./build-default-lights"; import { buildDefaultLights } from "./build-default-lights";
import { OrbitControls } from "three/addons/controls/OrbitControls.js"; import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { getCenter3D, getMaxSize } from "./utils";
export interface Extent { export interface Extent {
xmin: number; xmin: number;
@ -26,34 +18,30 @@ let renderer: WebGLRenderer;
let camera: PerspectiveCamera; let camera: PerspectiveCamera;
let scene: Scene; let scene: Scene;
export async function buildScene(container: HTMLElement, extent: Extent) { export async function buildScene(container: HTMLElement, extent: Extent) {
const size = Math.max( const maxSize = getMaxSize(extent);
extent.xmax - extent.xmin, const center = getCenter3D(extent);
extent.ymax - extent.ymin,
extent.zmax - extent.zmin
);
const center = new Vector3(
(extent.xmin + extent.xmax) / 2,
(extent.ymin + extent.ymax) / 2,
0
);
const width = container.clientWidth; const width = container.clientWidth;
const height = container.clientHeight; const height = container.clientHeight;
camera = new PerspectiveCamera(30, width / height, 0.1, size * 25); camera = new PerspectiveCamera(
camera.position.set(center.x, center.y, size * 5); 50,
width / height,
maxSize * 0.1,
maxSize * 25
);
camera.position.set(center.x, center.y, extent.zmax + 150000);
camera.lookAt(center); camera.lookAt(center);
renderer = new WebGLRenderer({ renderer = new WebGLRenderer({
alpha: true, alpha: true,
logarithmicDepthBuffer: true,
}); });
renderer.setPixelRatio(window.devicePixelRatio); renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height); renderer.setSize(width, height);
renderer.localClippingEnabled = true; renderer.localClippingEnabled = true;
// renderer.autoClear = false;
// renderer.setClearColor(0x000000, 0.0); // second param is opacity, 0 => transparent
renderer.setAnimationLoop(animate); renderer.setAnimationLoop(animate);
window.addEventListener("resize", () => onWindowResize(container)); window.addEventListener("resize", () => onWindowResize(container));
@ -62,18 +50,16 @@ export async function buildScene(container: HTMLElement, extent: Extent) {
controls = new OrbitControls(camera, renderer.domElement); controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(center.x, center.y, center.z); // Focus on the center controls.target.set(center.x, center.y, center.z); // Focus on the center
controls.enableDamping = true; // Smooth camera movement controls.enableDamping = true; // Smooth camera movement
controls.maxDistance = maxSize * 5;
controls.update(); controls.update();
// Scene will hold all our elements such as objects, cameras and lights // Scene will hold all our elements such as objects, cameras and lights
scene = new Scene(); scene = new Scene();
scene.background = new Color(0xdddddd);
buildDefaultLights(scene); buildDefaultLights(scene);
// const queryString = window.location.search; return { renderer, scene, camera, controls };
// const urlParams = new URLSearchParams(queryString);
// const modelid = parseInt(urlParams.get("model_id") ?? "20", 10);
return { renderer, scene, camera };
} }
function onWindowResize(container: HTMLElement) { function onWindowResize(container: HTMLElement) {

View file

@ -1,8 +1,10 @@
import { Group, Vector3 } from "three"; import { AxesHelper, Group } from "three";
import { buildMeshes } from "./build-meshes"; import { buildMeshes } from "./build-meshes";
import { Extent, buildScene } from "./build-scene"; import { Extent, buildScene } from "./build-scene";
import { getMetadata } from "./get-metadata"; import { getMetadata } from "./get-metadata";
import { MODEL_ID, SERVICE_URL } from "../config"; import { MODEL_ID, SERVICE_URL } from "../config";
import { createClippingPlane } from "./build-clipping-plane";
import { buildGrid } from "./build-grid";
export async function init(container: HTMLElement) { export async function init(container: HTMLElement) {
const modelData = await getMetadata(SERVICE_URL + MODEL_ID); const modelData = await getMetadata(SERVICE_URL + MODEL_ID);
@ -18,11 +20,31 @@ export async function init(container: HTMLElement) {
zmax: modelarea.z.max, zmax: modelarea.z.max,
}; };
const { renderer, scene, camera } = await buildScene(container, extent); const { renderer, scene, camera, controls } = await buildScene(
const meshes = await buildMeshes(mappedFeatures); 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(); const mappedFeaturesGroup = new Group();
mappedFeaturesGroup.add(...meshes); mappedFeaturesGroup.add(...meshes);
scene.add(mappedFeaturesGroup); 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);
} }

18
app/three/utils/utils.ts Normal file
View file

@ -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
);
}