Structured layers menu (WIP)

This commit is contained in:
2026-04-07 10:57:11 +02:00
parent 2df5a71241
commit 9a8f6c7dc5
7 changed files with 135 additions and 63 deletions

View File

@@ -44,15 +44,13 @@ export const config = {
uri : `${BASE_URI}/scenes/ssgp/`, uri : `${BASE_URI}/scenes/ssgp/`,
popup: theater2Popup, popup: theater2Popup,
coords: [45.4401, 12.3408], coords: [45.4401, 12.3408],
nodes: { nodes: [
groups: [
{ {
label: 'Teatro', label: 'Teatro',
layers: [ children: [
{ {
label: 'Struttura complessiva', label: 'Struttura complessiva',
model: 'models/ssgp/Teatro_SSGP_Full_ConSottrazioni.glb', model: 'models/ssgp/Teatro_SSGP_Full_ConSottrazioni.glb',
//isMain: true,
}, },
{ {
label: 'Involucro', label: 'Involucro',
@@ -64,30 +62,35 @@ export const config = {
}, },
{ {
label: 'Sala / Auditorium', label: 'Sala / Auditorium',
layers: [ children: [
{ {
label: 'Peplano / Platea', label: 'Peplano / Platea',
model: 'models/ssgp/Teatro_SSGP_Platea_peplano.glb', model: 'models/ssgp/Teatro_SSGP_Platea_peplano.glb',
}, },
{ {
label: 'Ordine 1', label: 'Ordini di palchi',
model: 'models/ssgp/Teatro_SSGP_Ordine1.glb', children: [
}, {
{ label: 'Ordine 1',
label: 'Ordine 2', model: 'models/ssgp/Teatro_SSGP_Ordine1.glb',
model: 'models/ssgp/Teatro_SSGP_Ordine2.glb', },
}, {
{ label: 'Ordine 2',
label: 'Ordine 3', model: 'models/ssgp/Teatro_SSGP_Ordine2.glb',
model: 'models/ssgp/Teatro_SSGP_Ordine3.glb', },
}, {
{ label: 'Ordine 3',
label: 'Ordine 4', model: 'models/ssgp/Teatro_SSGP_Ordine3.glb',
model: 'models/ssgp/Teatro_SSGP_Ordine4.glb', },
}, {
{ label: 'Ordine 4',
label: 'Ordine 5', model: 'models/ssgp/Teatro_SSGP_Ordine4.glb',
model: 'models/ssgp/Teatro_SSGP_Ordine5.glb', },
{
label: 'Ordine 5',
model: 'models/ssgp/Teatro_SSGP_Ordine5.glb',
},
]
}, },
{ {
label: 'Parapetto', label: 'Parapetto',
@@ -101,7 +104,7 @@ export const config = {
}, },
{ {
label: 'Scena', label: 'Scena',
layers: [ children: [
{ {
label: 'Palcoscenico', label: 'Palcoscenico',
model: 'models/ssgp/Teatro_SSGP_Palcoscenico.glb', model: 'models/ssgp/Teatro_SSGP_Palcoscenico.glb',
@@ -122,36 +125,37 @@ export const config = {
}, },
{ {
label: 'Spazi tecnici', label: 'Spazi tecnici',
layers: [ children: [
{ {
label: 'Spazio tecnico superiore', label: 'Spazio tecnico superiore',
model: 'models/ssgp/Teatro_SSGP_Layer_Spazio_tecnico_sup_soffitta.glb', model: 'models/ssgp/Teatro_SSGP_Layer_Spazio_tecnico_sup_soffitta.glb',
}, children: [
{ {
label: 'Graticcia', label: 'Graticcia',
model: 'models/ssgp/Teatro_SSGP_Graticcia.glb', model: 'models/ssgp/Teatro_SSGP_Graticcia.glb',
}, },
{ {
label: 'Ballatoio', label: 'Ballatoio',
model: 'models/ssgp/Teatro_SSGP_Ballatoio.glb', model: 'models/ssgp/Teatro_SSGP_Ballatoio.glb',
}, },
{ {
label: 'Spazio tecnico inferiore', label: 'Spazio tecnico inferiore',
model: 'models/ssgp/Teatro_SSGP_Spazio_tecnico_inf.glb', model: 'models/ssgp/Teatro_SSGP_Spazio_tecnico_inf.glb',
},
]
}, },
] ]
}, },
{ {
label: 'Orchestra', label: 'Orchestra',
layers: [ children: [
{ {
label: 'Fossa orchestra', label: 'Fossa orchestra',
model: 'models/ssgp/Teatro_SSGP_Fossa_orchestra.glb', model: 'models/ssgp/Teatro_SSGP_Fossa_orchestra.glb',
}, },
] ]
} }
], ],
},
pano: `pano/gradient.jpg`, pano: `pano/gradient.jpg`,
} }
], ],

View File

@@ -232,18 +232,21 @@ Scene.resetClipping = function () {
} }
/** /**
* @param {Object} marker - The marker object from config * @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(); Scene.init();
// TODO: move to init logic
Scene.UI.toggleClipperBar('#clipper', '#clipper-bar'); Scene.UI.toggleClipperBar('#clipper', '#clipper-bar');
// Filter nodes with models first
nodes = nodes.filter(n => n.model);
// Load 3D models and create nodes // Load 3D models and create nodes
Scene.loadNodes(marker.nodes); Scene.loadNodes(nodes);
ATON.setMainPanorama(marker.pano); ATON.setMainPanorama(marker.pano);
// TODO: hardcoded... // TODO: hardcoded...
AppState.initialRotation = new THREE.Vector3(0, 1.5, 0); AppState.initialRotation = new THREE.Vector3(0, 1.5, 0);
//Scene.showEdges(mainNode);
ATON.setAutoLP(config.scene.autoLP); ATON.setAutoLP(config.scene.autoLP);
AppState.lightProbe = 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) { Scene.loadNodes = function (nodes) {
nodes.forEach(n => { nodes.forEach(n => {
@@ -264,7 +267,11 @@ Scene.loadNodes = function (nodes) {
// Apply any transparency before attaching to scene // Apply any transparency before attaching to scene
if (n.opacity) { 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(); node.attachToRoot();
@@ -275,7 +282,6 @@ Scene.loadNodes = function (nodes) {
AppState.clipping.boundingSphere = node.getBound(); AppState.clipping.boundingSphere = node.getBound();
} }
AppState.nodes.push({id: n.label, active: true}); AppState.nodes.push({id: n.label, active: true});
}); });
} }

View File

@@ -8,6 +8,7 @@ let AppState = {
root: null, root: null,
// {id: String, active: Boolean} // {id: String, active: Boolean}
nodes: [], nodes: [],
normalizedNodes: [],
mainNodeId: null, mainNodeId: null,
currentScene: null, currentScene: null,
sceneHasAudio: false, sceneHasAudio: false,

View File

@@ -9,7 +9,7 @@ const UI = {};
UI.domParser = new DOMParser; UI.domParser = new DOMParser;
UI.contentMenuTabs = ` UI.contentMenuTabs = `
<!-- Nav tabs --> <!-- Nav tabs -->
<ul class="nav nav-pills" id="content-tabs" role="tablist"> <ul class="nav nav-pills" id="content-tabs" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link active" id="layer-tab" data-bs-toggle="tab" data-bs-target="#layer" type="button" role="tab" aria-controls="layer" aria-selected="false"> <button class="nav-link active" id="layer-tab" data-bs-toggle="tab" data-bs-target="#layer" type="button" role="tab" aria-controls="layer" aria-selected="false">
@@ -24,8 +24,8 @@ UI.contentMenuTabs = `
</ul> </ul>
<!-- Tab panes --> <!-- Tab panes -->
<div class="tab-content"> <div class="tab-content ps-4 ms-3" style="overflow: auto">
<div class="tab-pane active p-3" id="layer" role="tabpanel" aria-labelledby="layer-tab" tabindex="0"></div> <div class="tab-pane active p-3 ms-3" id="layer" role="tabpanel" aria-labelledby="layer-tab" tabindex="0"></div>
<div class="tab-pane p-3" id="media" role="tabpanel" aria-labelledby="media-tab" tabindex="0"></div> <div class="tab-pane p-3" id="media" role="tabpanel" aria-labelledby="media-tab" tabindex="0"></div>
</div> </div>
`; `;
@@ -230,7 +230,7 @@ UI.toggleContentMenu = function(triggerId) {
const tabs = this.domParser.parseFromString(UI.contentMenuTabs, 'text/html'); const tabs = this.domParser.parseFromString(UI.contentMenuTabs, 'text/html');
ATON.UI.elSidePanel.appendChild(tabs.querySelector('#content-tabs')); ATON.UI.elSidePanel.appendChild(tabs.querySelector('#content-tabs'));
ATON.UI.elSidePanel.appendChild(tabs.querySelector('.tab-content')); 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) { UI.buildLayersMenu = function(nodes, sidePanel) {
for(let node of nodes) { for(let node of nodes) {
const checkboxStr = ` const menuItem = document.createElement('div');
<div class="form-check form-switch ms-4 mt-2"> menuItem.className = `form-check form-switch ms-${node.depth} ps-${node.depth} mt-2`;
<input class="form-check-input" type="checkbox" ${node.active ? 'checked' : ''} role="switch" title="Mostra / nascondi layer"> const checkbox = document.createElement('input');
<label class="form-check-label" for="aoSwitch">${node.id}</label> checkbox.type = 'checkbox';
</div> 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'); menuItem.appendChild(checkbox);
element = element.querySelector('div.form-check');
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?? // 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) => { const toggleNode = (id, status) => {
ATON.getSceneNode(id).toggle(status); ATON.getSceneNode(id).toggle(status);
AppState.nodes.find(n => n.id === id).active = status; AppState.normalizedNodes.find(n => n.label === id).active = status;
} }
} }

42
js/utils/nodeUtils.js Normal file
View File

@@ -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;
}

View File

@@ -1,9 +1,15 @@
import Scene from "../../js/scene.js"; import Scene from "../../js/scene.js";
import { config } from "../../config.js"; import { config } from "../../config.js";
import AppState from "../../js/state.js"; import AppState from "../../js/state.js";
import { normalizeNodes } from "../../js/utils/nodeUtils.js";
AppState.currentScene = 'salvador'; AppState.currentScene = 'salvador';
AppState.sceneHasAudio = true; 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"]'); Scene.UI.pauseAudio('[data-bs-dismiss="modal"]');

View File

@@ -1,6 +1,12 @@
import Scene from "../../js/scene.js"; import Scene from "../../js/scene.js";
import { config } from "../../config.js"; import { config } from "../../config.js";
import AppState from "../../js/state.js"; import AppState from "../../js/state.js";
import { normalizeNodes } from "../../js/utils/nodeUtils.js";
AppState.currentScene = 'ssgp'; 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);