From 9a8f6c7dc52a4bf0d892d443566b423af6815c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20P=2E?= Date: Tue, 7 Apr 2026 10:57:11 +0200 Subject: [PATCH] Structured layers menu (WIP) --- config.js | 84 +++++++++++++++++++++------------------- js/scene.js | 18 ++++++--- js/state.js | 1 + js/ui.js | 37 +++++++++++------- js/utils/nodeUtils.js | 42 ++++++++++++++++++++ scenes/salvador/index.js | 8 +++- scenes/ssgp/index.js | 8 +++- 7 files changed, 135 insertions(+), 63 deletions(-) create mode 100644 js/utils/nodeUtils.js diff --git a/config.js b/config.js index 0f612d7..fb6919f 100644 --- a/config.js +++ b/config.js @@ -44,15 +44,13 @@ export const config = { uri : `${BASE_URI}/scenes/ssgp/`, popup: theater2Popup, coords: [45.4401, 12.3408], - nodes: { - groups: [ + nodes: [ { label: 'Teatro', - layers: [ + children: [ { label: 'Struttura complessiva', model: 'models/ssgp/Teatro_SSGP_Full_ConSottrazioni.glb', - //isMain: true, }, { label: 'Involucro', @@ -64,30 +62,35 @@ export const config = { }, { label: 'Sala / Auditorium', - layers: [ + children: [ { label: 'Peplano / Platea', model: 'models/ssgp/Teatro_SSGP_Platea_peplano.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: 'Ordini di palchi', + children: [ + { + 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: 'Parapetto', @@ -101,7 +104,7 @@ export const config = { }, { label: 'Scena', - layers: [ + children: [ { label: 'Palcoscenico', model: 'models/ssgp/Teatro_SSGP_Palcoscenico.glb', @@ -122,36 +125,37 @@ export const config = { }, { label: 'Spazi tecnici', - layers: [ + children: [ { label: 'Spazio tecnico superiore', model: 'models/ssgp/Teatro_SSGP_Layer_Spazio_tecnico_sup_soffitta.glb', - }, - { - label: 'Graticcia', - model: 'models/ssgp/Teatro_SSGP_Graticcia.glb', - }, - { - label: 'Ballatoio', - model: 'models/ssgp/Teatro_SSGP_Ballatoio.glb', - }, - { - label: 'Spazio tecnico inferiore', - model: 'models/ssgp/Teatro_SSGP_Spazio_tecnico_inf.glb', + children: [ + { + label: 'Graticcia', + model: 'models/ssgp/Teatro_SSGP_Graticcia.glb', + }, + { + label: 'Ballatoio', + model: 'models/ssgp/Teatro_SSGP_Ballatoio.glb', + }, + { + label: 'Spazio tecnico inferiore', + model: 'models/ssgp/Teatro_SSGP_Spazio_tecnico_inf.glb', + }, + ] }, ] }, { label: 'Orchestra', - layers: [ + children: [ { label: 'Fossa orchestra', model: 'models/ssgp/Teatro_SSGP_Fossa_orchestra.glb', }, ] } - ], - }, + ], pano: `pano/gradient.jpg`, } ], diff --git a/js/scene.js b/js/scene.js index a36086e..bc1860b 100644 --- a/js/scene.js +++ b/js/scene.js @@ -232,18 +232,21 @@ Scene.resetClipping = function () { } /** * @param {Object} marker - The marker object from config + * @param {Object[]} nodes - The flat list of nodes for this scene */ -Scene.openScene = function(marker) { +Scene.openScene = function(marker, nodes) { Scene.init(); + // TODO: move to init logic Scene.UI.toggleClipperBar('#clipper', '#clipper-bar'); + // Filter nodes with models first + nodes = nodes.filter(n => n.model); // Load 3D models and create nodes - Scene.loadNodes(marker.nodes); + Scene.loadNodes(nodes); ATON.setMainPanorama(marker.pano); // TODO: hardcoded... AppState.initialRotation = new THREE.Vector3(0, 1.5, 0); - //Scene.showEdges(mainNode); ATON.setAutoLP(config.scene.autoLP); AppState.lightProbe = config.scene.autoLP; @@ -254,7 +257,7 @@ Scene.openScene = function(marker) { /** * - * @param {Array} nodes + * @param {Object[]} nodes */ Scene.loadNodes = function (nodes) { nodes.forEach(n => { @@ -264,7 +267,11 @@ Scene.loadNodes = function (nodes) { // Apply any transparency before attaching to scene if (n.opacity) { - node.setMaterial(new THREE.MeshPhongMaterial({transparent: true, opacity: n.opacity, color: '#fff'})); + node.setMaterial(new THREE.MeshPhongMaterial({ + transparent: true, + opacity: n.opacity, + color: '#fff' + })); } node.attachToRoot(); @@ -275,7 +282,6 @@ Scene.loadNodes = function (nodes) { AppState.clipping.boundingSphere = node.getBound(); } - AppState.nodes.push({id: n.label, active: true}); }); } diff --git a/js/state.js b/js/state.js index 5e95990..6f46dae 100644 --- a/js/state.js +++ b/js/state.js @@ -8,6 +8,7 @@ let AppState = { root: null, // {id: String, active: Boolean} nodes: [], + normalizedNodes: [], mainNodeId: null, currentScene: null, sceneHasAudio: false, diff --git a/js/ui.js b/js/ui.js index eb9dd52..7126e94 100644 --- a/js/ui.js +++ b/js/ui.js @@ -9,7 +9,7 @@ const UI = {}; UI.domParser = new DOMParser; UI.contentMenuTabs = ` - + -
-
+
+
`; @@ -230,7 +230,7 @@ UI.toggleContentMenu = function(triggerId) { const tabs = this.domParser.parseFromString(UI.contentMenuTabs, 'text/html'); ATON.UI.elSidePanel.appendChild(tabs.querySelector('#content-tabs')); ATON.UI.elSidePanel.appendChild(tabs.querySelector('.tab-content')); - this.buildLayersMenu(AppState.nodes, ATON.UI.elSidePanel.querySelector('#layer')); + this.buildLayersMenu(AppState.normalizedNodes, ATON.UI.elSidePanel.querySelector('#layer')); }); } /** @@ -240,19 +240,26 @@ UI.toggleContentMenu = function(triggerId) { */ UI.buildLayersMenu = function(nodes, sidePanel) { for(let node of nodes) { - const checkboxStr = ` -
- - -
- `; + const menuItem = document.createElement('div'); + menuItem.className = `form-check form-switch ms-${node.depth} ps-${node.depth} mt-2`; + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.className = "form-check-input"; + checkbox.checked = node.active; + checkbox.role = 'switch'; + checkbox.title = "Mostra / nascondi layer"; - let element = this.domParser.parseFromString(checkboxStr, 'text/html'); - element = element.querySelector('div.form-check'); + menuItem.appendChild(checkbox); - sidePanel.appendChild(element); + const label = document.createElement('label'); + label.className = "form-check-label"; + label.textContent = node.label; + + menuItem.appendChild(label); + + sidePanel.appendChild(menuItem); // Will this ever work?? - element.addEventListener('change', event => toggleNode(node.id, event.target.checked)); + menuItem.addEventListener('change', event => toggleNode(node.label, event.target.checked)); } /** @@ -262,7 +269,7 @@ UI.buildLayersMenu = function(nodes, sidePanel) { */ const toggleNode = (id, status) => { ATON.getSceneNode(id).toggle(status); - AppState.nodes.find(n => n.id === id).active = status; + AppState.normalizedNodes.find(n => n.label === id).active = status; } } diff --git a/js/utils/nodeUtils.js b/js/utils/nodeUtils.js new file mode 100644 index 0000000..fdcbf06 --- /dev/null +++ b/js/utils/nodeUtils.js @@ -0,0 +1,42 @@ +/** + * @module + */ + +/** + * + * @param {Object} node + * @param {Object[]} flatList + * @param {Number} depth + */ +function traverse(node, flatList, depth = 1) { + const normNode = {label: node.label}; + if (node.model) { + normNode.model = node.model; + } + flatList.push({ + ...normNode, + depth + }); + if (node.children) { + depth++; + for(let child of node.children) { + traverse(child, flatList, depth); + } + } +} + +/** + * Create a flat list of nodes from + * the nested structure in config + * @param {Array} nodes + * @returns {Object[]} A flat list of nodes +**/ +export function normalizeNodes (nodes) { + let flatNodes = []; + + for (let node of nodes) { + traverse(node, flatNodes); + } + + return flatNodes; +} diff --git a/scenes/salvador/index.js b/scenes/salvador/index.js index 1d1095f..14c90eb 100644 --- a/scenes/salvador/index.js +++ b/scenes/salvador/index.js @@ -1,9 +1,15 @@ import Scene from "../../js/scene.js"; import { config } from "../../config.js"; import AppState from "../../js/state.js"; +import { normalizeNodes } from "../../js/utils/nodeUtils.js"; AppState.currentScene = 'salvador'; AppState.sceneHasAudio = true; +const marker = config.markers.find(m => m.id === 'salvador'); +AppState.normalizedNodes = normalizeNodes(marker.nodes); + +AppState.normalizedNodes.forEach(node => node.active = true); + +Scene.openScene(marker, AppState.normalizedNodes); -Scene.openScene(config.markers.find(m => m.id === 'salvador')); Scene.UI.pauseAudio('[data-bs-dismiss="modal"]'); diff --git a/scenes/ssgp/index.js b/scenes/ssgp/index.js index e270aba..f9a2e1f 100644 --- a/scenes/ssgp/index.js +++ b/scenes/ssgp/index.js @@ -1,6 +1,12 @@ import Scene from "../../js/scene.js"; import { config } from "../../config.js"; import AppState from "../../js/state.js"; +import { normalizeNodes } from "../../js/utils/nodeUtils.js"; AppState.currentScene = 'ssgp'; -Scene.openScene(config.markers.find(m => m.id === 'ssgp')); +const marker = config.markers.find(m => m.id === 'ssgp'); +AppState.normalizedNodes = normalizeNodes(marker.nodes); + +AppState.normalizedNodes.forEach(node => node.active = true); + +Scene.openScene(marker, AppState.normalizedNodes);