diff --git a/config.js b/config.js index 910a104..340c3dc 100644 --- a/config.js +++ b/config.js @@ -22,13 +22,6 @@ export const config = { scene : { initialExposure: 0.7, autoLP: true, - clipping: { - defaultPoint: [ - -20.3, - 7.3, - -18.3 - ], - } }, menu : { audioBtn1 diff --git a/js/map.js b/js/map.js index 73901c3..78bde3b 100644 --- a/js/map.js +++ b/js/map.js @@ -26,7 +26,6 @@ Map.init = function(mapContainerId) { //const markerIcon = L.divIcon({className: 'bi bi-bank text-large'}); config.markers.forEach(marker => { - debugger; const popup = this.domParser.parseFromString(marker.popup, 'text/html') .querySelector('div'); popup.querySelector('button').onclick = () => { @@ -37,10 +36,6 @@ Map.init = function(mapContainerId) { L.marker(marker.coords,).addTo(map) .bindPopup(popup, {autoClose:false, /*closeOnClick:false*/}) .togglePopup(); - - // Update state - AppState.scenes.push({id: marker.id, active: false}); - }); AppState.map = map; diff --git a/js/scene.js b/js/scene.js index 8b57bf0..b3360e4 100644 --- a/js/scene.js +++ b/js/scene.js @@ -1,6 +1,6 @@ // Global ATON -import { AppState, getSceneStatus, setSceneStatus } from "./state.js"; +import { AppState, getSceneStatus, setSceneStatus, getCurrentScene, setCurrentScene } from "./state.js"; import { config } from "../config.js"; const material = { @@ -15,6 +15,14 @@ Scene.UI = {}; Scene.UI.domParser = new DOMParser; +/** + * Resets the UI state (essentially hides the clipper toolbar if visible...) + * @todo Other elements to reset?? Restore inital lighting conditions and viewpoint... + */ +Scene.UI.reset = function() { + document.querySelector('#clipper-bar')?.classList.add('d-none'); + document.querySelector('#clipper')?.classList.remove('border', 'border-2', 'border-white'); +} /** * @todo Get clipping button from state? Review logic!! * @param {String} triggerSelector @@ -23,66 +31,72 @@ Scene.UI.domParser = new DOMParser; Scene.UI.toggleClipper = function(triggerSelector, targetSelector) { const trigger = document.querySelector(triggerSelector); const toolbar = document.querySelector(targetSelector); - trigger.addEventListener( - 'click', - () => { - toolbar.classList.toggle('d-none'); - const aoCurrentState = AppState.ambientOcclusion; - if (!AppState.clipping.enabled) { - AppState.clipping.enabled = true; - Scene.toggleAmbientOcclusion(false); - const btns = toolbar.querySelectorAll('button'); - btns.forEach(btn => { - btn.classList.remove('border', 'border-2', 'border-warning'); - }); + if (!AppState.clipping.listenerAdded) { + trigger.addEventListener( + 'click', + () => { + console.log('Clipping enabled?', AppState.clipping.enabled); - trigger.className += ' border border-2 border-white'; - toolbar.addEventListener('click', event => { - console.log('Clipping target:', event.target); - if (event.target.id === 'clipX') { - // Clip along X... - Scene.addClippingPlane('x', -1); - // Export to function... - event.target.classList.add('border', 'border-2', 'border-warning'); - btns.forEach(btn => { - if (btn.id !== event.target.id) { - btn.classList.remove('border', 'border-2', 'border-warning'); - } - }); - } - else if (event.target.id === 'clipY') { - // Clip along Y... - Scene.addClippingPlane('y', -1); - event.target.classList.add('border', 'border-2', 'border-warning'); - btns.forEach(btn => { - if (btn.id !== event.target.id) { - btn.classList.remove('border', 'border-2', 'border-warning'); - } - }) - } - else if (event.target.id === 'clipZ') { - // Clip along Z... - Scene.addClippingPlane('z', 1); - event.target.classList.add('border', 'border-2', 'border-warning'); - btns.forEach(btn => { - if (btn.id !== event.target.id) { - btn.classList.remove('border', 'border-2', 'border-warning'); - } - }) - } - }); - } else { - AppState.clipping.enabled = false; - ATON.disableClipPlanes(); - AppState.root.remove(AppState.clipping.helper); - AppState.clipping.helper = null; - let noBorder = trigger.className.replace(/ border.*$/g, ''); - trigger.className = noBorder; - Scene.toggleAmbientOcclusion(aoCurrentState); + toolbar.classList.toggle('d-none'); + const aoCurrentState = AppState.ambientOcclusion; + if (!AppState.clipping.enabled) { + AppState.clipping.enabled = true; + Scene.toggleAmbientOcclusion(false); + + const btns = toolbar.querySelectorAll('button'); + btns.forEach(btn => { + btn.classList.remove('border', 'border-2', 'border-warning'); + }); + + trigger.className += ' border border-2 border-white'; + toolbar.addEventListener('click', event => { + console.log('Clipping target:', event.target); + if (event.target.id === 'clipX') { + // Clip along X... + Scene.addClippingPlane('x', -1); + // Export to function... + event.target.classList.add('border', 'border-2', 'border-warning'); + btns.forEach(btn => { + if (btn.id !== event.target.id) { + btn.classList.remove('border', 'border-2', 'border-warning'); + } + }); + } + else if (event.target.id === 'clipY') { + // Clip along Y... + Scene.addClippingPlane('y', -1); + event.target.classList.add('border', 'border-2', 'border-warning'); + btns.forEach(btn => { + if (btn.id !== event.target.id) { + btn.classList.remove('border', 'border-2', 'border-warning'); + } + }) + } + else if (event.target.id === 'clipZ') { + // Clip along Z... + Scene.addClippingPlane('z', 1); + event.target.classList.add('border', 'border-2', 'border-warning'); + btns.forEach(btn => { + if (btn.id !== event.target.id) { + btn.classList.remove('border', 'border-2', 'border-warning'); + } + }) + } + }); + } else { + AppState.clipping.enabled = false; + ATON.disableClipPlanes(); + AppState.root.remove(AppState.clipping.helper); + AppState.clipping.helper = null; + let noBorder = trigger.className.replace(/ border.*$/g, ''); + trigger.className = noBorder; + Scene.toggleAmbientOcclusion(aoCurrentState); + } } - } - ); + ); + AppState.clipping.listenerAdded = true; + } } /** @@ -105,15 +119,59 @@ Scene.showEdges = function(object) { } /** - * @param {String} axis - The axis along wich the plane's normal should be directed, + * Calculate bounding box for the scene root object + * and return it along with its center and size (bad?). + * It only uses meshes to prevent "empty" nodes in the scene + * from being included in the calculation. + */ +Scene.getRootBoundingBox = function() { + const meshes = []; + AppState.root.traverse(obj => { + if (obj.isMesh) meshes.push(obj); + }); + + if (meshes.length === 0) return null; + + const bbox = new THREE.Box3().setFromObject(meshes[0]); + for (let i = 1; i < meshes.length; i++) { + bbox.union(new THREE.Box3().setFromObject(meshes[i])); + } + + const center = bbox.getCenter(new THREE.Vector3()); + const size = bbox.getSize(new THREE.Vector3()); + + return { bbox, center, size }; +} + +/** + * + * @param {THREE.Vector3} rootBBoxSize - The size of the bounding box for the root object + * @returns {THREE.Mesh} + */ +Scene.createClippingPlaneMesh = function(rootBBoxSize) { + const averageDim = (Number(rootBBoxSize.x) + Number(rootBBoxSize.y) + Number(rootBBoxSize.z)) / 3; + const planeSize = averageDim * 1.2; + const mesh = new THREE.Mesh( + new THREE.PlaneGeometry(planeSize, planeSize), + new THREE.MeshBasicMaterial({ color: 0xffff00, opacity: 0.1, side: THREE.DoubleSide, transparent: true }) + ); + + return mesh; +} + +/** + * @param {String} axis - The axis along which the plane's normal should be directed, * one of 'x', 'y', 'z' * @param {Number} orientation - Positive (1) or negative (-1) orientation on the axis */ Scene.addClippingPlane = function(axis, orientation = -1) { axis = axis.toLowerCase(); - const defaultPoint = new THREE.Vector3( - ...config.scene.clipping.defaultPoint - ); + const bboxData = AppState.clipping.rootBoundingBox ?? this.getRootBoundingBox(); + + if (!bboxData) return; + + const defaultPoint = bboxData.center.clone(); + const vector = [ axis === 'x' ? orientation : 0, axis === 'y' ? orientation : 0, @@ -121,7 +179,7 @@ Scene.addClippingPlane = function(axis, orientation = -1) { ]; // First, add a default clipping plane - // at a predefined point (bad?) + // at a default point (calculated...) Scene.activateClipper(vector, defaultPoint); console.log(vector, defaultPoint); @@ -140,6 +198,7 @@ Scene.addClippingPlane = function(axis, orientation = -1) { */ Scene.activateClipper = function(vector, point = null) { point ??= ATON.getSceneQueriedPoint(); + const bboxData = AppState.clipping.rootBoundingBox ?? this.getRootBoundingBox(); if (point) { console.log('Queried point:', point); @@ -147,13 +206,17 @@ Scene.activateClipper = function(vector, point = null) { // First remove any existing clipping planes ATON.disableClipPlanes(); // Normal of the clipping plane along the Y axis facing down - const plane = ATON.addClipPlane(new THREE.Vector3(...vector), point); + const normal = new THREE.Vector3(...vector).normalize(); + //const constant = -normal.dot(point); + const plane = ATON.addClipPlane(normal, point); // Add a visible plane helper for the clipping plane - const helper = new THREE.PlaneHelper(plane, 24, 0xffff00); + const visiblePlane = Scene.createClippingPlaneMesh(bboxData.size); + visiblePlane.position.copy(point); + visiblePlane.lookAt(point.clone().add(normal)); // Remove any already visbile helper plane if (AppState.clipping.helper !== null) AppState.root.remove(AppState.clipping.helper); - AppState.root.add(helper); - AppState.clipping.helper = helper; + AppState.root.add(visiblePlane); + AppState.clipping.helper = visiblePlane; console.log("I'm clipping, baby!"); } } @@ -322,17 +385,29 @@ Scene.init = function() { } /** - * @param {String} id - The back-to-map button id + * @param {String} btnId - The back-to-map button id */ -Scene.toggleScene = function(id) { - const btn = document.querySelector(`#${id}`); +Scene.toggleScene = function(btnId) { + const btn = document.querySelector(`#${btnId}`); const scene = document.querySelector('#scene'); btn.addEventListener('click', () => { + const currentScene = getCurrentScene(); + // Deactivate the current scene before toggling + setSceneStatus(currentScene.id, false); scene.classList.toggle('d-none'); // Pause rendering the 3D scene to free resources (hopefully) // when browsing the map ATON.renderPause(); + if (AppState.clipping.enabled) { + ATON.disableClipPlanes(); + AppState.clipping.enabled = false; + Scene.UI.reset(); + AppState.root.remove(AppState.clipping.helper); + AppState.clipping.helper = null; + } + AppState.root.setRotation(AppState.initialRotation ?? new THREE.Vector3(0, 1.5, 0)); + document.querySelector('#map').classList.toggle('d-none'); AppState.map.invalidateSize(); }); @@ -348,8 +423,22 @@ Scene.openScene = function(marker) { Scene.init(); } + Scene.UI.toggleClipper('#clipper', '#clipper-bar'); + scene.classList.toggle('d-none'); ATON.renderResume(); + // TODO: reset scene only if changing to a different model from the map + // set scene status to inactive, first get current scene id... + let currentScene = getCurrentScene(); + if (currentScene && currentScene.id !== marker.id) { + AppState.root.removeChildren(); + currentScene.current = false; + } + + if (!AppState.scenes.find(s => s.id === marker.id)) { + const newScene = {id: marker.id, active: false, current: true}; + AppState.scenes.push(newScene); + } if (!getSceneStatus(marker.id)) { // Set scene as active @@ -358,7 +447,9 @@ Scene.openScene = function(marker) { let mainNode = ATON.createSceneNode(marker.label).load(marker.model); ATON.setMainPanorama(marker.pano); //mainNode.setMaterial(new THREE.MeshPhongMaterial(material)); + // TODO: hardcoded... mainNode.setRotation(0, 1.5, 0) + AppState.initialRotation = new THREE.Vector3(0, 1.5, 0); Scene.showEdges(mainNode); mainNode.attachToRoot(); @@ -368,9 +459,10 @@ Scene.openScene = function(marker) { Scene.toggleAmbientOcclusion(true); AppState.ambientOcclusion = true; - Scene.UI.toggleClipper('#clipper', '#clipper-bar'); - AppState.root = ATON.getRootScene(); + + // TODO: set the scene as current!! + setCurrentScene(marker.id); } } diff --git a/js/state.js b/js/state.js index 005138f..af92dc3 100644 --- a/js/state.js +++ b/js/state.js @@ -1,6 +1,7 @@ export const AppState = { // The root scene object root: null, + initialRotation: null, scenes : [], ambientOcclusion : true, shadows : true, @@ -9,6 +10,8 @@ export const AppState = { clipping : { enabled: false, helper : null, + rootBoundingBox: null, + listenerAdded: false, } } @@ -28,5 +31,22 @@ export function getSceneStatus(id) { * @returns {Boolean} */ export function setSceneStatus(id, status) { - return AppState.scenes.find(s => s.id === id).active = status; + AppState.scenes.find(s => s.id === id).active = status; +} + +export function getCurrentScene() { + return AppState.scenes.find(s => s.current); +} + +/** + * + * @param {String} id The scene's id + * @returns + */ +export function setCurrentScene(id) { + // First set the correct status for the other scenes + let otherScenes = AppState.scenes.filter(s => s.id !== id); + otherScenes.forEach(scene => scene.current = false) + + AppState.scenes.find(s => s.id === id).current = true; } \ No newline at end of file