Rename js to src

This commit is contained in:
2026-05-21 13:25:17 +02:00
parent 3305e74776
commit 0c3a049d12
19 changed files with 10 additions and 10 deletions

View File

@@ -0,0 +1,74 @@
// Global ATON
import { Controller } from "@hotwired/stimulus"
import AppState from "../state.js";
import { addClippingPlane, resetClipping } from "../utils/clipping.js";
import { toggleAmbientOcclusion } from "../utils/environment.js";
/**
* Handle events for the clipper toolbar,
* related to the clipping module
*/
export default class extends Controller {
static targets = ['trigger', 'clipper', 'axis'];
static values = { enabled: Boolean };
connect() {
console.log('#clipper controller connected');
}
clip(event) {
/**
* @type {string}
*/
const label = event.params.axis;
/**
* @type {HTMLButtonElement}
*/
const target = event.target;
/**
* @type {NodeListOf<HTMLButtonElement>}
*/
const axes = this.axisTargets;
const classes = ['border', 'border-2', 'border-warning'];
addClippingPlane(label, -1);
target.classList.add(...classes);
for (const btn of axes) {
if (btn.id !== target.id) {
btn.classList.remove(...classes);
}
}
}
/**
* Toggle clipper toolbar
*/
toggleClipper() {
/**
* @type {HTMLElement}
*/
const trigger = this.triggerTarget;
this.clipperTarget.classList.toggle('d-none');
// If the toolbar is shown, clipping is enabled and vice versa
this.enabledValue = !this.clipperTarget.classList.contains('d-none');
this.axisTargets.forEach(btn => {
btn.classList.remove('border', 'border-2', 'border-warning');
});
if (this.enabledValue) {
// AO should be turned off if clipping is enabled
toggleAmbientOcclusion(false);
trigger.className += ' border border-2 border-white';
}
if (!this.enabledValue) {
resetClipping();
trigger.className = trigger.className.replace(/ border.*$/g, '');
toggleAmbientOcclusion(AppState.ambientOcclusion);
}
AppState.clipping.enabled = this.enabledValue;
}
}

View File

@@ -0,0 +1,252 @@
// Global ATON
import { Controller } from "@hotwired/stimulus";
import AppState from "../state.js";
import { traverseOntology } from "../ontology.js";
const html = String.raw;
const domParser = new DOMParser;
// TODO: hard-coded, but follows a convention...
const ontologyJsonPath = location.pathname + 'ontology.json';
export default class extends Controller {
static targets = ['trigger', 'layers', 'ontology'];
connect() {
console.log('#menu controller connected');
}
/**
* Open settings panel
*/
async toggleMenu() {
ATON.UI.setSidePanelRight();
ATON.UI.showSidePanel({header: 'Menu'});
this.#buildMenuPanel(ATON.UI.elSidePanel);
this.#buildLayersMenu(AppState.treeNodes, this.layersTarget);
this.#buildOntologyMenu(await traverseOntology(ontologyJsonPath), this.ontologyTarget);
}
/**
* @param {Event} event
*/
toggleNode(event) {
console.debug(AppState.treeNodes);
/**
* The node's id
* @type {string}
*/
const id = event?.params.node;
const status = event?.target?.checked;
const node = AppState.normalizedNodes.find(n => n.id === id);
/**
* @type {HTMLElement|null}
*/
const eye = event.target.parentElement.querySelector('i');
if (eye) {
eye.classList.toggle('bi-eye');
eye.classList.toggle('bi-eye-slash');
}
if (node.children.length > 0) {
this.#toggleGroup(node, status);
this.#syncGroupCheckboxes(node, status, this.layersTarget);
} else {
ATON.getSceneNode(id).toggle(status);
node.active = status;
}
}
/**
* Recursively toggle children in a nodes group
* @param {Object} groupNode
* @param {Boolean} status
*/
#toggleGroup(groupNode, status) {
for (const child of groupNode.children) {
child.active = status;
if (child.model) {
ATON.getSceneNode(child.id).toggle(status);
}
if (child.children.length > 0) {
this.#toggleGroup(child, status);
}
}
}
/**
* Toggles checkboxes in a group based on status
* @param {Object} groupNode
* @param {Boolean} status
* @param {HTMLElement} container
*/
#syncGroupCheckboxes(groupNode, status, container) {
for (const child of groupNode.children) {
const checkbox = container.querySelector(
`[data-menu-node-param="${child.id}"]`
);
if (checkbox) checkbox.checked = status;
if (child.children.length > 0) {
this.#syncGroupCheckboxes(child, status, container);
}
}
}
/**
* Clone a <template> by id
* @param {String} id
* @returns {DocumentFragment}
*/
#cloneTemplate(id) {
return document.getElementById(id)?.content?.cloneNode(true);
}
/**
* Create the left-side settings panel
* content
* @param {Element} panel
*/
#buildMenuPanel(panel) {
const fragment = this.#cloneTemplate('tmpl-menu-tabs');
panel.appendChild(fragment);
}
/**
* @todo Don't rebuild it every time, use caching, return a container with checkboxes
* @param {Array<import("../state.js").NormalizedSceneNode>} tree The normalized scene nodes tree
* @param {HTMLElement} tab Tab content element
*/
#buildLayersMenu(tree, tab) {
const heading = document.createElement('h1');
heading.classList.add('fs-5', 'fw-bold', 'ms-0');
heading.textContent = 'Teatro'; // Hard-coded!!
tab.appendChild(heading);
for (const node of tree) {
tab.appendChild(
node.children.length > 0
? this.#createLayerGroup(node)
: this.#createLayerToggle(node)
);
}
}
/**
* @param {import("../state.js").NormalizedSceneNode} node
* @param {boolean} isGroup
* @returns {HTMLDivElement}
*/
#createLayerToggle(node, isGroup = false) {
// This should be calculated somehow!
const labelIndent = node.depth < 3 ? '1.5rem' : '1rem';
const labelText = isGroup ? '<i class="bi bi-eye"></i>' : node.id;
const toggle = html`
<label class="toggle-control ms-${node.depth} ps-${node.depth} mt-1 mb-1">
<input id="${node.id}" type="checkbox" ${node.active ? 'checked' : ''} role="switch"
data-menu-node-param="${node.id}"
data-action="change->menu#toggleNode">
<span class="control"></span>
<span class="ps-2 fs-6" title="Mostra / nascondi ${isGroup ? 'gruppo' : 'layer'}" style="margin-left: ${labelIndent}">
${labelText}
</span>
</label>
`;
return domParser.parseFromString(toggle, 'text/html').querySelector('label');
}
/**
* @param {import("../state.js").NormalizedSceneNode} node
* @returns {HTMLDetailsElement}
*/
#createLayerGroup(node) {
const { trigger, collapseDiv } = this.#createNodeCollapse(node);
const wrapper = document.createElement('div');
wrapper.classList.add('w-max-content', 'ps-1', 'mb-1');
collapseDiv.appendChild(this.#createLayerToggle(node, true));
for (const child of node.children) {
collapseDiv.appendChild(
child.children.length > 0
? this.#createLayerGroup(child)
: this.#createLayerToggle(child)
);
}
wrapper.appendChild(trigger);
wrapper.appendChild(collapseDiv);
wrapper.classList.add('border-start', 'border-bottom', 'rounded', `ms-${node.depth}`);
return wrapper;
}
/**
*
* @param {import("../state.js").NormalizedSceneNode} node
* @returns {{trigger: HTMLButtonElement, collapseDiv: HTMLDivElement}}
*/
#createNodeCollapse(node) {
const cleanId = node.id.replace(/[\s\/\-]+/g, '');
const trigger = document.createElement('button');
trigger.className = 'btn btn-link p-0 fs-6 fw-bold text-decoration-none text-reset';
trigger.setAttribute('data-bs-toggle', 'collapse');
trigger.setAttribute('data-bs-target', `#group-${cleanId}`);
trigger.setAttribute('data-action', 'menu#toggleChevron');
trigger.innerHTML = html`
<i class="bi bi-chevron-down me-1"></i>${node.id}
`;
// Add color "swatch" only for first level groups
/*
if (node.depth === 2) {
trigger.innerHTML += html`
<div class="d-inline-block border rounded ms-2"
style="background-color: ${node.color}; height: 15px; width:15px">
</div>
`;
}
*/
const collapseDiv = document.createElement('div');
collapseDiv.className = 'collapse';
collapseDiv.id = `group-${cleanId}`;
return {trigger, collapseDiv};
}
/**
*
* @param {Event} event
*/
toggleChevron(event) {
/**
* @type {HTMLButtonElement} collapse
*/
const collapse = event.target;
const icon = collapse.querySelector('i');
icon.classList.toggle('bi-chevron-down');
icon.classList.toggle('bi-chevron-up');
}
/**
* Temporary implementation to show domains only
* @todo Don't rebuild it every time, use caching, return a container
* @param {Object} ontology The traversed ontology object (temp)
* @param {HTMLElement} tab Tab content element
*/
#buildOntologyMenu(ontology, tab) {
console.debug(ontology);
const mainNode = tab.querySelector('#ontology-list');
mainNode.textContent = ontology.ontology;
let domainList = html`
<ul class="list-group mt-2" id="domains-list"></ul>
`;
// Very fragile and ugly!!
mainNode.innerHTML += domainList;
domainList = tab.querySelector('#domains-list');
for(let domain of ontology.domains) {
const domainItem = html`
<li class="list-group-item">${domain.label}</li>
`;
domainList.innerHTML += domainItem;
}
}
}

View File

@@ -0,0 +1,41 @@
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ['modal'];
connect() {
console.log('#modal controller connected');
}
/**
*
* @param {Event} event
*/
showSemanticModal(event) {
const modal = this.modalTarget;
const title = modal.querySelector('.modal-title');
// Clear any existing content first
title.innerHTML = '';
title.innerHTML = event.content?.title;
const body = modal.querySelector('.modal-body');
body.innerHTML = '';
const contentType = event.content?.type;
const content = document.createElement(contentType);
if (contentType === 'img') {
content.src = event.content?.imgSrc;
content.alt = event.content?.description.trim();
content.classList.add('img-fluid');
}
body.appendChild(content);
const description = document.createElement('p');
description.textContent = event.content?.description.trim();
description.classList.add('py-3', 'my-0', 'fst-italic');
body.appendChild(description);
bootstrap.Modal.getOrCreateInstance(modal).show();
}
}

View File

@@ -0,0 +1,28 @@
import { Controller } from "@hotwired/stimulus"
import AppState from "../state.js";
export default class extends Controller {
static targets = ['ao', 'shadows'];
connect() {
console.log('#settings controller connected');
this.aoTarget.checked = AppState.ambientOcclusion;
this.shadowsTarget.checked = AppState.shadows;
}
/**
* Toggle Ambient Occlusion
* @param {Event} event
*/
toggleAO(event) {
ATON.FX.togglePass(ATON.FX.PASS_AO, event.target.checked);
AppState.ambientOcclusion = event.target.checked;
}
/**
* Toggle shadows
* @param {Event} event
*/
toggleShadows(event) {
ATON.toggleShadows(event.target.checked);
AppState.shadows = event.target.checked;
}
}

View File

@@ -0,0 +1,42 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ['active', 'tab', 'content'];
connect() {
console.log('#tabs controller connected');
}
/**
*
* @param {Event} event
*/
activate(event) {
event.preventDefault();
this.deactivate();
const activeId = event.currentTarget.dataset.id;
event.currentTarget.parentElement.classList.add('active');
this.contentTargets.find(c => c.dataset.id === activeId)
.classList.remove('d-hide');
}
reset() {
this.deactivate();
const activeId = this.activeTarget.dataset.id;
this.activeTarget.classList.add('active');
this.contentTargets.find(c => c.dataset.id === activeId)
.classList.remove('d-hide');
}
deactivate() {
this.tabTargets.forEach(tab => {
tab.classList.remove('active');
});
this.contentTargets.forEach(content => {
content.classList.add('d-hide');
});
}
}

View File

@@ -0,0 +1,71 @@
// Global ATON
import { Controller } from "@hotwired/stimulus"
import AppState from "../state.js";
import { createExposureSlider, createLightSlider } from "../utils/environment.js";
const html = String.raw;
const panelHeader = html`
<i class="bi bi-gear-fill me-1"></i> Impostazioni
`;
export default class extends Controller {
static targets = ['settings', 'fullscreen'];
connect() {
console.log('#toolbar controller connected');
}
/**
* Open settings panel
* @param {Event} event
*/
toggleSettings(event) {
ATON.UI.setSidePanelLeft();
ATON.UI.showSidePanel({header: panelHeader});
this.#buildSettingsPanel(ATON.UI.elSidePanel);
}
toggleFullscreen() {
/**
* @type {HTMLAnchorElement}
*/
const target = this.fullscreenTarget;
const icon = target.querySelector('i');
if (!document.fullscreenElement) {
document.body.requestFullscreen();
icon.classList.remove('bi-fullscreen');
icon.classList.add('bi-fullscreen-exit');
target.title = 'Esci da schermo intero';
} else {
document.exitFullscreen();
icon.classList.remove('bi-fullscreen-exit');
icon.classList.add('bi-fullscreen');
target.title = 'Attiva schermo intero';
}
}
/**
* Clone a <template> by id
* @param {String} id
* @returns {DocumentFragment}
*/
#cloneTemplate(id) {
return document.getElementById(id).content.cloneNode(true);
}
/**
* Create the left-side settings panel
* content
* @param {Element} panel
*/
#buildSettingsPanel(panel) {
const fragment = this.#cloneTemplate('tmpl-settings');
let sliderContainer = fragment.querySelector('[data-sliders-container]');
let exposureContainer = fragment.querySelector('[data-slider-exposure-container]');
['x', 'y', 'z'].forEach((axis, i) => {
const label = ['Asse X', 'Asse Y', 'Asse Z'][i];
sliderContainer.appendChild(createLightSlider(axis, label, [-2, 2], 0.1));
})
exposureContainer.appendChild(createExposureSlider('Valore', [0, 5]));
panel.appendChild(fragment);
}
}