Add grid; clipping plane
This commit is contained in:
parent
0d0190fd8e
commit
13be63c40a
6 changed files with 236 additions and 44 deletions
69
app/three/utils/build-clipping-plane.ts
Normal file
69
app/three/utils/build-clipping-plane.ts
Normal 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 };
|
||||||
|
}
|
91
app/three/utils/build-grid.ts
Normal file
91
app/three/utils/build-grid.ts
Normal 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;
|
||||||
|
}
|
|
@ -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,20 +72,20 @@ 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") {
|
||||||
meshes.push(mesh);
|
const mesh = await buildMesh(layerData, clippingPlanes);
|
||||||
|
meshes.push(mesh);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return meshes;
|
return meshes;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
18
app/three/utils/utils.ts
Normal 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
|
||||||
|
);
|
||||||
|
}
|
Loading…
Add table
editor.link_modal.header
Reference in a new issue