diff --git a/config.js b/config.js index 00b9d9a..478a971 100644 --- a/config.js +++ b/config.js @@ -44,7 +44,88 @@ export const config = { uri : `${BASE_URI}/scenes/ssgp/`, popup: theater2Popup, coords: [45.4401, 12.3408], - model: `SSGP.glb`, + nodes: [ + /* + { + label: 'Struttura principale', + model: 'models/ssgp/Teatro_SSGP_Full_ConSottrazioni.glb', + isMain: true, + }, + */ + { + label: 'Struttura parete di fondo', + model: 'models/ssgp/Teatro_SSGP_Layer_Struttura_parete_di_fondo.glb', + isMain: true, + }, + { + label: 'Ballatoio', + model: 'models/ssgp/Teatro_SSGP_Ballatoio.glb', + }, + { + label: 'Boccascena', + model: 'models/ssgp/Teatro_SSGP_Boccascena.glb', + }, + { + label: 'Fossa orchestra', + model: 'models/ssgp/Teatro_SSGP_Fossa_orchestra.glb', + }, + { + label: 'Graticcia', + model: 'models/ssgp/Teatro_SSGP_Graticcia.glb', + }, + { + label: 'Ordine 1', + model: 'models/ssgp/Teatro_SSGP_Ordine1.glb', + }, + { + label: 'Ordine 2', + model: 'models/ssgp/Teatro_SSGP_Ordine2.glb', + }, + { + label: 'Ordine 3', + model: 'models/ssgp/Teatro_SSGP_Ordine3.glb', + }, + { + label: 'Ordine 4', + model: 'models/ssgp/Teatro_SSGP_Ordine4.glb', + }, + { + label: 'Ordine 5', + model: 'models/ssgp/Teatro_SSGP_Ordine5.glb', + }, + { + label: 'Palcoscenico', + model: 'models/ssgp/Teatro_SSGP_Palcoscenico.glb', + }, + { + label: 'Parapetto scala piani', + model: 'models/ssgp/Teatro_SSGP_parapetto_scala_piani.glb', + }, + { + label: 'Percorsi scale corridoi', + model: 'models/ssgp/Teatro_SSGP_Percorsi_scale_corridoi.glb', + }, + { + label: 'Platea peplano', + model: 'models/ssgp/Teatro_SSGP_Platea_peplano.glb', + }, + { + label: 'Quinte architettoniche fisse', + model: 'models/ssgp/Teatro_SSGP_Layer_quinte_architettoniche_fisse.glb', + }, + { + label: 'Quinte architettoniche mobili', + model: 'models/ssgp/Teatro_SSGP_Layer_quinte_architettoniche_mobili.glb', + }, + { + label: 'Spazio tecnico superiore', + model: 'models/ssgp/Teatro_SSGP_Layer_Spazio_tecnico_sup_soffitta.glb', + }, + { + label: 'Spazio tecnico inferiore', + model: 'models/ssgp/Teatro_SSGP_Spazio_tecnico_inf.glb', + }, + ], pano: `pano/defsky-grass.jpg`, } ], diff --git a/js/scene.js b/js/scene.js index 7fcb053..3dfea54 100644 --- a/js/scene.js +++ b/js/scene.js @@ -2,111 +2,11 @@ import AppState from "./state.js"; import { config } from "../config.js"; +import UI from "./ui.js"; const Scene = {}; -Scene.UI = {}; - -Scene.UI.domParser = new DOMParser; -/** - * - * @param {String} triggerSelector - Usually, the close modal trigger element(s) selector - */ -Scene.UI.pauseAudio = function(triggerSelector) { - // What if more than one audio element is playing? - const audio = document.querySelector('audio'); - - if (audio) { - document.querySelectorAll(triggerSelector).forEach(el => { - el.addEventListener('click', () => audio.pause()); - }); - document.querySelector('.modal').addEventListener('blur', () => { - audio.pause(); - }); - } -} - -/** - * 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 - * @param {String} targetSelector The selector for the target toolbar to be displayed - */ -Scene.UI.toggleClipper = function(triggerSelector, targetSelector) { - const trigger = document.querySelector(triggerSelector); - const toolbar = document.querySelector(targetSelector); - - if (!AppState.clipping.listeners.button) { - trigger.addEventListener( - 'click', - () => { - toolbar.classList.toggle('d-none'); - const aoCurrentState = AppState.ambientOcclusion; - - if (!toolbar.classList.contains('d-none')) { - AppState.clipping.enabled = true; - - //if (AppState.clipping.controls) AppState.clipping.controls.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 => { - 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 { - Scene.resetClipping(); - let noBorder = trigger.className.replace(/ border.*$/g, ''); - trigger.className = noBorder; - Scene.toggleAmbientOcclusion(aoCurrentState); - } - } - ); - AppState.clipping.listeners.button = true; - } -} +Scene.UI = UI; /** * @todo Experimental... @@ -289,137 +189,6 @@ Scene.toggleAmbientOcclusion = function(isEnabled) { console.log('Ambient occlusion', isEnabled ? 'ON' : 'OFF'); } -/** - * - * @param {String} direction - The axis direction, one of 'x','y','z' - * @param {String} label - The slider label - * @param {Number[]} range - The slider's range - * @param {Number} step - The slider's step - */ -Scene.createLightSlider = function(direction, label, range, step) { - const currentVal = ATON.getMainLightDirection()[direction]; - const lightSlider = ATON.UI.createSlider({ - range, - label, - value: Number.parseFloat(currentVal).toPrecision(1), - oninput: val => { - const lightDir = ATON.getMainLightDirection(); - // Keep existing direction values for the other axes - lightDir[direction] = Number.parseFloat(val); - this.changeLightDirection(lightDir); - }, - }); - - lightSlider.classList.add('ms-4'); - lightSlider.querySelector('input').step = step; - - return lightSlider; -} - -/** - * Right-side main menu panel - * @param {String} triggerId - The menu button id - */ -Scene.toggleContentMenu = function(triggerId) { - const btn = document.querySelector(`#${triggerId}`); - const audio1 = this.UI.domParser.parseFromString(config.menu.audioBtn1, 'text/html') - .querySelector('button'); - - btn.addEventListener('click', () => { - ATON.UI.setSidePanelRight(); - ATON.UI.showSidePanel({header: ' Contenuti'}); - ATON.UI.elSidePanel.appendChild(audio1); - }); -} - -/** - * A left side settings panel - * @param {String} triggerId - The settings button id - */ -Scene.toggleSettingsPanel = function(triggerId) { - const btn = document.querySelector(`#${triggerId}`); - const lightHeading = document.createElement('h2'); - lightHeading.className = 'fs-5 ms-2 mb-3 mt-3'; - lightHeading.innerHTML = ' Direzione luce'; - const envHeading = document.createElement('h2'); - envHeading.className = 'fs-5 ms-2 mb-3 mt-3'; - envHeading.innerHTML = ' Ambiente'; - - btn.addEventListener('click', () => { - ATON.UI.setSidePanelLeft(); - ATON.UI.showSidePanel({header: ' Impostazioni'}); - ATON.UI.elSidePanel.appendChild(lightHeading); - - const lightSliderX = this.createLightSlider('x', 'Asse X', [-2, 2], 0.1); - const lightSliderY = this.createLightSlider('y', 'Asse Y', [-2, 2], 0.1); - const lightSliderZ = this.createLightSlider('z', 'Asse Z', [-2, 2], 0.1); - - ATON.UI.elSidePanel.appendChild(lightSliderX); - ATON.UI.elSidePanel.appendChild(lightSliderY); - ATON.UI.elSidePanel.appendChild(lightSliderZ); - - ATON.UI.elSidePanel.appendChild(envHeading); - - const ambientOcclSwitch = document.createElement('div'); - ambientOcclSwitch.className = 'form-check form-switch ms-4 mt-2'; - ambientOcclSwitch.innerHTML = ` - - - `; - - const shadowsSwitch = document.createElement('div'); - shadowsSwitch.className = 'form-check form-switch ms-4 mt-2'; - shadowsSwitch.innerHTML = ` - - - `; - - const lightProbeSwitch = document.createElement('div'); - lightProbeSwitch.className = 'form-check form-switch ms-4 mt-2'; - lightProbeSwitch.innerHTML = ` - - - `; - - shadowsSwitch.querySelector('input[type="checkbox"').checked = AppState.shadows; - ambientOcclSwitch.querySelector('input[type="checkbox"').checked = AppState.ambientOcclusion; - lightProbeSwitch.querySelector('input[type="checkbox"').checked = AppState.lightProbe; - - ATON.UI.elSidePanel.appendChild(ambientOcclSwitch); - ATON.UI.elSidePanel.appendChild(shadowsSwitch); - ATON.UI.elSidePanel.appendChild(lightProbeSwitch); - - // TODO: move somewhere else... - document.querySelector('#aoSwitch').addEventListener( - 'change', - event => { - this.toggleAmbientOcclusion(event.target.checked); - AppState.ambientOcclusion = event.target.checked; - } - ); - document.querySelector('#shadowsSwitch').addEventListener( - 'change', - event => { - const checked = event.target.checked; - ATON.toggleShadows(checked); - AppState.shadows = checked; - } - ); - // Not working properly? - document.querySelector('#lpSwitch').addEventListener( - 'change', - event => { - const checked = event.target.checked; - ATON.setAutoLP(checked); - //if (!checked) ATON.clearLightProbes(); - AppState.lightProbe = checked; - if (checked) ATON.updateLightProbes(); - console.log('Light probe: ', checked); - } - ); - }); -} - Scene.init = function() { ATON.realize(); ATON.UI.addBasicEvents(); @@ -431,19 +200,19 @@ Scene.init = function() { ATON.toggleShadows(true); ATON.setExposure(config.scene.initialExposure); // Open settings side panel when clicking on settings btn - Scene.toggleSettingsPanel('settings'); - Scene.toggleContentMenu('menu'); + Scene.UI.toggleSettingsPanel('settings'); + Scene.UI.toggleContentMenu('menu'); AppState.camera = ATON.Nav._camera; AppState.renderer = ATON._renderer; ATON.Nav.setUserControl(true); } - +/** + * Reset clipping state after disabling from UI + * @todo DragControls errors!! + */ Scene.resetClipping = function () { - - console.warn('Resetting clipping!!'); - AppState.clipping.enabled = false; ATON.disableClipPlanes(); AppState.clipping.controls.deactivate(); @@ -466,31 +235,42 @@ Scene.resetClipping = function () { */ Scene.openScene = function(marker) { Scene.init(); + Scene.UI.toggleClipperBar('#clipper', '#clipper-bar'); - Scene.UI.toggleClipper('#clipper', '#clipper-bar'); + // Load 3D models and create nodes + Scene.loadNodes(marker.nodes); - // Load 3D model then - let mainNode = ATON.createSceneNode(marker.label); - mainNode.load(marker.model); - // TODO: only for the main ('larger') node in the scene - AppState.mainNodeId = marker.label; 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(); + //Scene.showEdges(mainNode); ATON.setAutoLP(config.scene.autoLP); AppState.lightProbe = config.scene.autoLP; Scene.toggleAmbientOcclusion(true); AppState.ambientOcclusion = true; - AppState.root = ATON.getRootScene(); - // ATON.Node.getBound() returns a THREE.Sphere object - AppState.clipping.boundingSphere = mainNode.getBound(); +} + +/** + * + * @param {Array} nodes + */ +Scene.loadNodes = function (nodes) { + nodes.forEach(n => { + let node = ATON.createSceneNode(n.label); + node.load(n.model); + node.setRotation(0, 1.5, 0); + node.attachToRoot(); + + if (n.isMain) { + AppState.mainNodeId = n.label; + // ATON.Node.getBound() returns a THREE.Sphere object + AppState.clipping.boundingSphere = node.getBound(); + } + + AppState.nodes.push({id: n.label, active: true}); + }); } export default Scene; \ No newline at end of file diff --git a/js/state.js b/js/state.js index c4e6c0c..00f7877 100644 --- a/js/state.js +++ b/js/state.js @@ -4,7 +4,11 @@ let AppState = { // The root scene object root: null, + nodeIds: [], + // {id: String, active: Boolean} + nodes: [], mainNodeId: null, + layersMenuBuilt: false, initialRotation: null, camera: null, renderer: null, diff --git a/js/ui.js b/js/ui.js new file mode 100644 index 0000000..39c6f0e --- /dev/null +++ b/js/ui.js @@ -0,0 +1,261 @@ +import { config } from "../config.js"; +import Scene from "./scene.js"; +import AppState from "./state.js"; +/** + * @namespace UI + */ +const UI = {}; + +UI.domParser = new DOMParser; +/** + * + * @param {String} triggerSelector - Usually, the close modal trigger element(s) selector + */ +UI.pauseAudio = function(triggerSelector) { + // What if more than one audio element is playing? + const audio = document.querySelector('audio'); + + if (audio) { + document.querySelectorAll(triggerSelector).forEach(el => { + el.addEventListener('click', () => audio.pause()); + }); + document.querySelector('.modal').addEventListener('blur', () => { + audio.pause(); + }); + } +} +/** + * Resets the UI state (essentially hides the clipper toolbar if visible...) + * @todo Other elements to reset?? Restore inital lighting conditions and viewpoint... + */ +UI.reset = function() { + document.querySelector('#clipper-bar')?.classList.add('d-none'); + document.querySelector('#clipper')?.classList.remove('border', 'border-2', 'border-white'); +} + +/** + * + * @param {HTMLElement} target + * @param {NodeListOf} btns + * @param {String} axis - One of 'x', 'y', 'z' + */ +UI.showClipping = function(target, btns, axis) { + if (axis) { + Scene.addClippingPlane(axis, -1); + target.classList.add('border', 'border-2', 'border-warning'); + btns.forEach(btn => { + if (btn.id !== target.id) { + btn.classList.remove('border', 'border-2', 'border-warning'); + } + }); + } +} +/** + * @todo Get clipping button from state? Review logic!! + * @param {String} triggerSelector + * @param {String} targetSelector The selector for the target toolbar to be displayed + */ +UI.toggleClipperBar = function(triggerSelector, targetSelector) { + const trigger = document.querySelector(triggerSelector); + const toolbar = document.querySelector(targetSelector); + const btns = toolbar.querySelectorAll('button'); + const clipTargets = { + clipX: {axis: 'x'}, + clipY: {axis: 'y'}, + clipZ: {axis: 'z'}, + }; + + if (!AppState.clipping.listeners.button) { + trigger.addEventListener( + 'click', + () => { + toolbar.classList.toggle('d-none'); + const aoCurrentState = AppState.ambientOcclusion; + + if (!toolbar.classList.contains('d-none')) { + AppState.clipping.enabled = true; + Scene.toggleAmbientOcclusion(false); + + btns.forEach(btn => { + btn.classList.remove('border', 'border-2', 'border-warning'); + }); + + trigger.className += ' border border-2 border-white'; + toolbar.addEventListener('click', event => { + UI.showClipping(event.target, btns, clipTargets[event.target.id]?.axis); + }); + } else { + Scene.resetClipping(); + let noBorder = trigger.className.replace(/ border.*$/g, ''); + trigger.className = noBorder; + Scene.toggleAmbientOcclusion(aoCurrentState); + } + } + ); + AppState.clipping.listeners.button = true; + } +} +/** + * A left side settings panel + * @param {String} triggerId - The settings button id + */ +UI.toggleSettingsPanel = function(triggerId) { + const btn = document.querySelector(`#${triggerId}`); + const lightHeading = document.createElement('h2'); + lightHeading.className = 'fs-5 ms-2 mb-3 mt-3'; + lightHeading.innerHTML = ' Direzione luce'; + const envHeading = document.createElement('h2'); + envHeading.className = 'fs-5 ms-2 mb-3 mt-3'; + envHeading.innerHTML = ' Ambiente'; + + btn.addEventListener('click', () => { + ATON.UI.setSidePanelLeft(); + ATON.UI.showSidePanel({header: ' Impostazioni'}); + ATON.UI.elSidePanel.appendChild(lightHeading); + + const lightSliderX = this.createLightSlider('x', 'Asse X', [-2, 2], 0.1); + const lightSliderY = this.createLightSlider('y', 'Asse Y', [-2, 2], 0.1); + const lightSliderZ = this.createLightSlider('z', 'Asse Z', [-2, 2], 0.1); + + ATON.UI.elSidePanel.appendChild(lightSliderX); + ATON.UI.elSidePanel.appendChild(lightSliderY); + ATON.UI.elSidePanel.appendChild(lightSliderZ); + + ATON.UI.elSidePanel.appendChild(envHeading); + + const ambientOcclSwitch = document.createElement('div'); + ambientOcclSwitch.className = 'form-check form-switch ms-4 mt-2'; + ambientOcclSwitch.innerHTML = ` + + + `; + + const shadowsSwitch = document.createElement('div'); + shadowsSwitch.className = 'form-check form-switch ms-4 mt-2'; + shadowsSwitch.innerHTML = ` + + + `; + + const lightProbeSwitch = document.createElement('div'); + lightProbeSwitch.className = 'form-check form-switch ms-4 mt-2'; + lightProbeSwitch.innerHTML = ` + + + `; + + shadowsSwitch.querySelector('input[type="checkbox"').checked = AppState.shadows; + ambientOcclSwitch.querySelector('input[type="checkbox"').checked = AppState.ambientOcclusion; + lightProbeSwitch.querySelector('input[type="checkbox"').checked = AppState.lightProbe; + + ATON.UI.elSidePanel.appendChild(ambientOcclSwitch); + ATON.UI.elSidePanel.appendChild(shadowsSwitch); + ATON.UI.elSidePanel.appendChild(lightProbeSwitch); + + // TODO: move somewhere else... + document.querySelector('#aoSwitch').addEventListener( + 'change', + event => { + Scene.toggleAmbientOcclusion(event.target.checked); + AppState.ambientOcclusion = event.target.checked; + } + ); + document.querySelector('#shadowsSwitch').addEventListener( + 'change', + event => { + const checked = event.target.checked; + ATON.toggleShadows(checked); + AppState.shadows = checked; + } + ); + // Not working properly? + document.querySelector('#lpSwitch').addEventListener( + 'change', + event => { + const checked = event.target.checked; + ATON.setAutoLP(checked); + //if (!checked) ATON.clearLightProbes(); + AppState.lightProbe = checked; + if (checked) ATON.updateLightProbes(); + console.log('Light probe: ', checked); + } + ); + }); +} +/** + * + * @param {String} direction - The axis direction, one of 'x','y','z' + * @param {String} label - The slider label + * @param {Number[]} range - The slider's range + * @param {Number} step - The slider's step + */ +UI.createLightSlider = function(direction, label, range, step) { + const currentVal = ATON.getMainLightDirection()[direction]; + const lightSlider = ATON.UI.createSlider({ + range, + label, + value: Number.parseFloat(currentVal).toPrecision(1), + oninput: val => { + const lightDir = ATON.getMainLightDirection(); + // Keep existing direction values for the other axes + lightDir[direction] = Number.parseFloat(val); + Scene.changeLightDirection(lightDir); + }, + }); + + lightSlider.classList.add('ms-4'); + lightSlider.querySelector('input').step = step; + + return lightSlider; +} +/** + * Right-side main menu panel + * @param {String} triggerId - The menu button id + */ +UI.toggleContentMenu = function(triggerId) { + const btn = document.querySelector(`#${triggerId}`); + let audio1 = this.domParser.parseFromString(config.menu.audioBtn1, 'text/html'); + audio1 = audio1.querySelector('button'); + + btn.addEventListener('click', () => { + ATON.UI.setSidePanelRight(); + ATON.UI.showSidePanel({header: ' Contenuti'}); + ATON.UI.elSidePanel.appendChild(audio1); + ATON.UI.elSidePanel.appendChild(this.domParser.parseFromString('
Layer
', 'text/html').querySelector('h5')); + this.buildLayersMenu(AppState.nodes, ATON.UI.elSidePanel); + }); +} +/** + * @todo Don't rebuild it every time the side panel is shown... + * @param {Array} nodes The scenes nodes (IDs and status) + * @param {HTMLElement} sidePanel ATON's side panel element + */ +UI.buildLayersMenu = function(nodes, sidePanel) { + for(let node of nodes) { + const checkboxStr = ` +
+ + +
+ `; + + let element = this.domParser.parseFromString(checkboxStr, 'text/html'); + element = element.querySelector('div.form-check'); + + sidePanel.appendChild(element); + // Will this ever work?? + element.addEventListener('change', event => toggleNode(node.id, event.target.checked)); + } + + /** + * This is terrible... + * @param {String} id + * @param {Boolean} status + */ + const toggleNode = (id, status) => { + ATON.getSceneNode(id).toggle(status); + AppState.nodes.find(n => n.id === id).active = status; + } +} + +export default UI; \ No newline at end of file