Rename js to src
This commit is contained in:
74
src/controllers/clipper_controller.js
Normal file
74
src/controllers/clipper_controller.js
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
252
src/controllers/menu_controller.js
Normal file
252
src/controllers/menu_controller.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/controllers/modal_controller.js
Normal file
41
src/controllers/modal_controller.js
Normal 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();
|
||||
}
|
||||
}
|
||||
28
src/controllers/settings_controller.js
Normal file
28
src/controllers/settings_controller.js
Normal 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;
|
||||
}
|
||||
}
|
||||
42
src/controllers/tabs_controller.js
Normal file
42
src/controllers/tabs_controller.js
Normal 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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
71
src/controllers/toolbar_controller.js
Normal file
71
src/controllers/toolbar_controller.js
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user