diff --git a/app/components/Form.tsx b/app/components/Form.tsx index ce45f1f..f1fd145 100644 --- a/app/components/Form.tsx +++ b/app/components/Form.tsx @@ -140,10 +140,14 @@ export function Form() { const [emptyProfile, setEmptyProfile] = useState(false); const { sceneView } = useContext(SceneViewContext) as SceneViewContextType; - function handleChangeSlicingBox() { + function handleChangeSlicingBox(e: ChangeEvent) { if (!sceneView) return; - sceneView.toggleClippingBox(); + if (e.target.checked) { + sceneView.toggleClippingBox(true); + } else { + sceneView.toggleClippingBox(false); + } } function handleChangeCG() { @@ -217,10 +221,8 @@ export function Form() { if (e.target.checked) { sceneView.explode(true); - // setExploded(true); } else { sceneView.explode(false); - // setExploded(false); } } diff --git a/app/components/RangeSlider.tsx b/app/components/RangeSlider.tsx index 1981b90..22f45fc 100644 --- a/app/components/RangeSlider.tsx +++ b/app/components/RangeSlider.tsx @@ -14,7 +14,7 @@ export function RangeSlider() { const z = parseFloat(t.value); if (!isNaN(z)) { setScale(z); - sceneView.scene.scale.set(1, 1, z); + sceneView.setZScale(z); } }; return ( diff --git a/app/three/SceneView.ts b/app/three/SceneView.ts index ed49508..9fb2dc8 100644 --- a/app/three/SceneView.ts +++ b/app/three/SceneView.ts @@ -45,7 +45,6 @@ export type CustomEvent = CustomEventInit<{ export class SceneView extends EventTarget { private _scene: Scene; - private _dragControls: DragControls; private _model: Group; private _camera: PerspectiveCamera; private _container: HTMLElement; @@ -57,12 +56,13 @@ export class SceneView extends EventTarget { private static _DRAG_THRESHOLD = 5; private _callback: EventListenerOrEventListenerObject | null = null; private _orbitControls: OrbitControls; + private _dragControls: DragControls | null = null; private _renderer: WebGLRenderer; + private static _DISPLACEMENT = 2000; constructor( scene: Scene, model: Group, - dragControls: DragControls, camera: PerspectiveCamera, container: HTMLElement, extent: Extent, @@ -71,7 +71,6 @@ export class SceneView extends EventTarget { ) { super(); this._scene = scene; - this._dragControls = dragControls; this._model = model; this._camera = camera; this._container = container; @@ -84,13 +83,11 @@ export class SceneView extends EventTarget { static async create(container: HTMLElement, modelId: string) { const data = await init(container, modelId); if (data) { - const { scene, model, dragControls, camera, extent, controls, renderer } = - data; + const { scene, model, camera, extent, controls, renderer } = data; return new SceneView( scene, model, - dragControls, camera, container, extent, @@ -110,17 +107,11 @@ export class SceneView extends EventTarget { return this._model; } - toggleClippingBox() { - const box = this._scene?.getObjectByName("clipping-box"); - if (box) { - // Set DragControls - if (box.visible) { - this._dragControls.enabled = false; - } else { - this._dragControls.enabled = true; - } - - box.visible = !box.visible; + toggleClippingBox(on = true) { + if (on) { + this._resetClippingBox(); + } else { + this._removeClippingBoxObjects(); } } @@ -344,57 +335,12 @@ export class SceneView extends EventTarget { this._orbitControls.reset(); } - explode(explode: boolean) { - const DISPLACEMENT = 2000; - for (let i = 1; i < this._model.children.length; i++) { - const mesh = this._model.children[i]; - - if (explode) { - const displacement = - (this._model.children.length - i - 1) * DISPLACEMENT; - mesh.userData.originalPosition = mesh.position.clone(); - mesh.translateZ(displacement); - - if (i === 1) { - this._model.userData.zmax = this._extent.zmax; - this._extent.zmax += displacement; - } - } else { - if (mesh.userData.originalPosition) { - mesh.position.copy(mesh.userData.originalPosition); - } - } - } - - // Reset extent - if (!explode && this._model.userData.zmax) { - this._extent.zmax = this._model.userData.zmax; - } - - // Reset clipping box + private _removeClippingBoxObjects() { + // Remove existing boxes const box = this._scene.getObjectByName("clipping-box"); - let visible = false; if (box) { - visible = box.visible; this._scene.remove(box); } - const { planes, dragControls } = buildClippingplanes( - this._renderer, - this._camera, - this._orbitControls, - this._extent, - this._model.children as Mesh[], - this._scene, - visible - ); - - this._dragControls.dispose(); - this._dragControls = dragControls; - - // Add clipping planes to the meshes - for (const mesh of this._model.children) { - ((mesh as Mesh).material as Material).clippingPlanes = planes; - } // Remove existing cap meshes for (const o in Orientation) { @@ -405,11 +351,101 @@ export class SceneView extends EventTarget { capMeshGroup = this._scene.getObjectByName(capMeshGroupName); } } + + // Remove drag controls + if (this._dragControls) { + this._dragControls.dispose(); + this._dragControls = null; + } + + // Remove clipping planes + for (const mesh of this._model.children) { + ((mesh as Mesh).material as Material).clippingPlanes = null; + } + } + + // Reset clipping box + private _resetClippingBox() { + this._removeClippingBoxObjects(); + + const { planes, dragControls } = buildClippingplanes( + this._renderer, + this._camera, + this._orbitControls, + this._extent, + this._model.children as Mesh[], + this._scene + ); + + this._dragControls = dragControls; + + // Add clipping planes to the meshes + for (const mesh of this._model.children) { + ((mesh as Mesh).material as Material).clippingPlanes = planes; + } + } + + // Explode meshes + explode(explode: boolean) { + if (explode) { + // Save previous zmax value + this._scene.userData.zmax = this._extent.zmax; + + const maxDisplacement = + this._model.children.length * SceneView._DISPLACEMENT; + + this._extent.zmax += maxDisplacement; + } else { + // Reset extent + this._extent.zmax = this._scene.userData.zmax; + } + + // Reset clipping box + const box = this._scene.getObjectByName("clipping-box"); + if (box && box.visible) { + this._resetClippingBox(); + } + + for (let i = 1; i < this._model.children.length; i++) { + const mesh = this._model.children[i]; + + if (explode) { + const displacement = + (this._model.children.length - i - 1) * SceneView._DISPLACEMENT; + mesh.userData.originalPosition = mesh.position.clone(); + mesh.translateZ(displacement); + } else { + if (mesh.userData.originalPosition) { + mesh.position.copy(mesh.userData.originalPosition); + } + } + } + } + + // Set z scaling factor + setZScale(scale: number) { + // Update extent + //this._extent = { + // ...this._extent, + // zmin: (scale * this._extent.zmin) / this._scene.scale.z, + // zmax: (scale * this._extent.zmax) / this._scene.scale.z, + //}; + + // Set scale factor + this._scene.scale.set(1, 1, scale); + + // Reset clipping box + const box = this._scene.getObjectByName("clipping-box"); + if (box && box.visible) { + this._resetClippingBox(); + } } } async function init(container: HTMLElement, modelId = MODEL_ID) { const modelData = await getMetadata(SERVICE_URL + modelId); + if (!modelData) return null; + const mappedFeatures = modelData.mappedfeatures; const modelarea = modelData.modelarea; @@ -436,23 +472,6 @@ async function init(container: HTMLElement, modelId = MODEL_ID) { model.name = "geologic-model"; scene.add(model); - // Build the clipping planes and add them to the scene - const visible = false; - const { planes, dragControls } = buildClippingplanes( - renderer, - camera, - controls, - extent, - meshes, - scene, - visible - ); - - // Add clipping planes to the meshes - for (const mesh of meshes) { - mesh.material.clippingPlanes = planes; - } - // Add a coordinate grid to the scene const { gridHelper, annotations } = buildCoordinateGrid(extent); const { heightGridHelper, heightAnnotations } = buildHeightGrid(extent); @@ -489,5 +508,12 @@ async function init(container: HTMLElement, modelId = MODEL_ID) { map.visible = false; scene.add(map); - return { scene, model, dragControls, camera, extent, controls, renderer }; + return { + scene, + model, + camera, + extent, + controls, + renderer, + }; } diff --git a/app/three/utils/build-clipping-planes.ts b/app/three/utils/build-clipping-planes.ts index c3158a2..a7d3ead 100644 --- a/app/three/utils/build-clipping-planes.ts +++ b/app/three/utils/build-clipping-planes.ts @@ -53,8 +53,7 @@ export function buildClippingplanes( orbitControls: OrbitControls, extent: Extent, meshes: Mesh[], - scene: Scene, - visible: boolean + scene: Scene ) { // Set current extent to given extent currentExtent = { ...extent }; @@ -137,7 +136,12 @@ export function buildClippingplanes( } // Plane is given in Hesse normal form: a * x + b* y + c * y + d = 0, where normal = (a, b, c) and d = d - const plane = new Plane(p.normal, p.d); + const plane = new Plane( + p.normal, + p.orientation === Orientation.Z || p.orientation === Orientation.NZ + ? scene.scale.z * p.d + : p.d + ); // Visual representation of the clipping plane const planeGeometry = new PlaneGeometry(width, height); @@ -197,7 +201,6 @@ export function buildClippingplanes( const clippingBox = new Group(); clippingBox.add(planeMeshGroup, edgeMeshGroup); clippingBox.name = "clipping-box"; - clippingBox.visible = visible; scene.add(clippingBox); // Enable DragControls for the clipping planes @@ -207,8 +210,6 @@ export function buildClippingplanes( renderer.domElement ); - dragControls.enabled = visible; - dragControls.addEventListener("dragstart", () => { // Disable OrbitControls when dragging starts orbitControls.enabled = false; @@ -251,6 +252,7 @@ export function buildClippingplanes( // Reset position of plane plane.constant = orientation === Orientation.Z ? -newZ : newZ; + plane.constant = scene.scale.z * plane.constant; // Update current extent if (orientation === Orientation.Z) { @@ -578,6 +580,7 @@ function generateCapMeshes( scene: Scene ) { const capMeshes: Mesh[] = []; + const scaleFactor = scene.scale.z; // Iterate over the list of geologic meshes for (const mesh of meshes) { @@ -598,17 +601,17 @@ function generateCapMeshes( const v1 = new Vector3( position[i1], position[i1 + 1], - position[i1 + 2] + mesh.position.z + scaleFactor * (position[i1 + 2] + mesh.position.z) ); const v2 = new Vector3( position[i2], position[i2 + 1], - position[i2 + 2] + mesh.position.z + scaleFactor * (position[i2 + 2] + mesh.position.z) ); const v3 = new Vector3( position[i3], position[i3 + 1], - position[i3 + 2] + mesh.position.z + scaleFactor * (position[i3 + 2] + mesh.position.z) ); // Check if the triangle is cut by the plane @@ -624,7 +627,12 @@ function generateCapMeshes( if (d3 * d1 < 0) intersections.push(intersectEdge(v3, v1, d3, d1)); if (intersections.length === 2) { - edges.push([intersections[0], intersections[1]]); + // Rescale local coordinates and push to edges + const start = intersections[0]; + const end = intersections[1]; + start.z = start.z / scaleFactor; + end.z = end.z / scaleFactor; + edges.push([start, end]); } } @@ -634,7 +642,13 @@ function generateCapMeshes( // Clip cap surfaces with clipping planes const clippingPlanes = planes.filter((p) => !p.normal.equals(plane.normal)); - const offset = orientation === Orientation.Z ? 1 : -1; + const offset = + orientation === Orientation.NX || + orientation === Orientation.NY || + orientation === Orientation.NZ + ? 1 + : -1; + const material = new MeshStandardMaterial({ color: (mesh.material as MeshStandardMaterial).color, side: DoubleSide, diff --git a/app/three/utils/build-scene.ts b/app/three/utils/build-scene.ts index 663c758..a33fec2 100644 --- a/app/three/utils/build-scene.ts +++ b/app/three/utils/build-scene.ts @@ -73,7 +73,7 @@ export function buildScene(container: HTMLElement, extent: Extent) { controls.target.set(center.x, center.y, center.z); controls.enableDamping = true; controls.dampingFactor = 0.1; - controls.maxDistance = maxSize * 3; + controls.maxDistance = maxSize * 5; controls.minDistance = maxSize / 5; controls.update(); controls.saveState(); diff --git a/app/three/utils/utils.ts b/app/three/utils/utils.ts index 8569625..f34466b 100644 --- a/app/three/utils/utils.ts +++ b/app/three/utils/utils.ts @@ -20,18 +20,22 @@ export function getCenter3D(extent: Extent) { } export async function getMetadata(serviceUrl: string) { - const response = await fetch(serviceUrl, { - method: "GET", - mode: "cors", - headers: { - "Content-Type": "application/json", - }, - }); + try { + const response = await fetch(serviceUrl, { + method: "GET", + mode: "cors", + headers: { + "Content-Type": "application/json", + }, + }); - if (response.ok) { - return response.json(); - } else { - throw new Error("HTTP error status: " + response.status); + if (response.ok) { + return response.json(); + } else { + throw new Error("HTTP error status: " + response.status); + } + } catch (e) { + console.log(e); } }