commit 761db06329ea070e7486023e4eea1cef5e09b723 Author: Carmine Savino Date: Mon Jul 13 11:58:30 2026 +0200 versione 0.8 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..f4f219b --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,98 @@ +# Copilot Instructions for infissionline-vite + +Welcome to the `infissionline-vite` project! This document provides essential guidelines for AI coding agents to be productive in this codebase. Follow these instructions to understand the architecture, workflows, and conventions specific to this project. + +--- + +## Project Overview + +This project is a web-based configurator for infissi (windows and doors) built using Vite. It includes a modular structure for managing assets, data, and JavaScript logic. The primary goal is to provide a dynamic and interactive user experience for configuring and visualizing products. + +### Key Components + +1. **Frontend**: + - **HTML Templates**: Located in `src/index.html` and `src/assets/_menu*.html`. + - **CSS Styles**: Defined in `src/assets/style.css`. + - **JavaScript Logic**: Core functionality resides in `src/js/`. + - `app.js`: Entry point for initializing the application. + - `configurator.js`: Handles product configuration logic. + - `menu_builder.js`: Dynamically builds menus. + - `params_parser.js`: Parses URL parameters. + - `price_calculator.js`: Calculates product prices. + - `starter.js`: Bootstraps the application. + +2. **Data Management**: + - **JSON Files**: Product data is stored in `src/assets/data/`. + - `modelli/`: Contains model-specific data for `alluminio` and `pvc`. + - `sistemi/`: Defines system-level configurations (e.g., handle heights). + - **Images**: Product images are organized in `src/assets/data/icone/` and `src/assets/data/immagini/`. + +3. **Build System**: + - Vite is used for development and production builds. + - Configuration is defined in `vite.config.js`. + +--- + +## Developer Workflows + +### Building the Project +- Use `pnpm install` to install dependencies. +- Run `pnpm dev` to start the development server. +- Use `pnpm build` to create a production build. + +### Debugging +- Open the browser console to inspect JavaScript errors. +- Use `console.log` for debugging specific parts of the code. +- Modify `src/js/` files to test changes dynamically during development. + +### Testing +- No explicit testing framework is set up. Use manual testing by interacting with the configurator in the browser. + +--- + +## Project-Specific Conventions + +1. **File Organization**: + - Separate `alluminio` and `pvc` data under `modelli/` and `sistemi/`. + - Maintain a clear hierarchy for images and models. + +2. **Data Format**: + - JSON files follow a structured format for product attributes. + - Example: `src/assets/data/modelli/alluminio/finestra_1_anta/modello.json`. + +3. **Dynamic Imports**: + - Use dynamic imports in JavaScript for better performance. + +4. **Naming Conventions**: + - Use lowercase and underscores for file names. + - Follow camelCase for JavaScript variables and functions. + +--- + +## Integration Points + +- **External Libraries**: + - Vite: Build tool for modern web development. + - Ensure all dependencies are listed in `package.json`. + +- **Cross-Component Communication**: + - JavaScript modules communicate via imports/exports. + - Data flows from JSON files to the frontend via `configurator.js`. + +--- + +## Examples + +### Adding a New Product Model +1. Create a new folder under `src/assets/data/modelli/{material}/{product_name}`. +2. Add a `modello.json` file with the product's attributes. +3. Update `configurator.js` to include the new model if necessary. + +### Debugging Price Calculation +1. Modify `src/js/price_calculator.js`. +2. Add `console.log` statements to inspect intermediate values. +3. Test changes by running `pnpm dev` and interacting with the configurator. + +--- + +For further questions or updates, refer to the `src/js/` directory for examples of existing patterns. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9eef5d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.venv/ +/kreios/ +/dist/ +/node_modules/ +kreios.zip \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7fd8d6c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project overview + +Web-based 3D configurator for infissi (windows/doors) — "infissionline-vite". It renders a Three.js scene +(GLB models) driven by JSON product data and a jQuery-based menu UI, and is meant to run **inside an +``; + + popup.classList.add(popup_position); + popup.dataset.position = popup_position; + + marker.appendChild(popup); + object.visible = false; + + marker.onclick = () => { + if (popup.style.display == 'none') { + popup.style.display = 'block'; + } else { + popup.style.display = 'none'; + } + }; + + markers.push({ element: marker, position: position }); + } + + this.changeXscale = (name, scale) => { + let infisso = scene.getObjectByName(name); + scene.traverse((object) => { + if (object.name == name) { + object.scale.x = scale; + } + }); + //infisso.scale.x = scale; + } + + this.changeTexture = async (data) => { + + let texture = null; + let bump = null; + + texture = data.texture_map ? await new THREE.TextureLoader().loadAsync(`./assets/data/immagini/${window.sistema}/${data.folder}/${data.texture_folder}/${data.id}/${data.texture_map}`) : null; + bump = data.texture_bump ? await new THREE.TextureLoader().loadAsync(`./assets/data/immagini/${window.sistema}/${data.folder}/${data.texture_folder}/${data.id}/${data.texture_bump}`) : null; + + // A newer selection for this same group may have been made while the texture above was loading. + // Discard this stale result so the last user/init action always wins, regardless of load order. + if (data.isStale && data.isStale()) return; + + scene.traverse((object) => { + if (object.isMesh) { + if (data.categories.includes(object.userData.categoria)) { + + object.material.map = null; + object.material.bumpMap = null; + if (texture != null) { + texture.colorSpace = THREE.SRGBColorSpace; + object.material.map = texture; + } + + if (bump != null) { + bump.colorSpace = THREE.NoColorSpace; + object.material.bumpMap = bump; + } + + object.material.needsUpdate = true; + if (data.roughness_valore != "") object.material.roughness = data.roughness_valore; + if (data.metalness_valore != "") object.material.metalness = data.metalness_valore; + if (data.opacity_valore != "") object.material.opacity = data.opacity_valore; + if (data.bump_valore != "") object.material.bumpScale = data.bump_valore; + //if(data.color_valore != "") object.material.color.setHex(parseInt(data.color_valore.replace("#", "0x"))); + if (data.color_valore != "") object.material.color = new THREE.Color(data.color_valore); + + object.material.needsUpdate = true; + } + } + }); + }, + this.changeColor = async (data) => { + if (data.color_valore_corpo_base != "") { + scene.traverse((object) => { + if (object.isMesh) { + if (data.categories.includes(object.userData.categoria)) { + + object.material.color = new THREE.Color(data.color_valore_corpo_base); + + object.material.needsUpdate = true; + } + } + }); + } + + } + + const updateMarkers = () => { + markers.forEach(marker => { + const vector = new THREE.Vector3(marker.position.x, marker.position.y, marker.position.z); + vector.project(camera); + const popup = marker.element.querySelector('.popup'); + if (camera.rotation.y < 0) { + if (popup.dataset.position == 'right') { + popup.classList.add('left'); + popup.classList.remove('right'); + } else { + popup.classList.add('right'); + popup.classList.remove('left'); + } + + } else { + if (popup.dataset.position == 'right') { + + popup.classList.add('right'); + popup.classList.remove('left'); + } else { + popup.classList.add('left'); + popup.classList.remove('right'); + } + } + const x = (vector.x * 0.5 + 0.5) * dom.clientWidth; + const y = (vector.y * -0.5 + 0.5) * dom.clientHeight; + + marker.element.style.left = `${x}px`; + marker.element.style.top = `${y}px`; + }); + } + + + function onKeyDown(event) { + + dispatch(events.keydown, event); + + } + + function onKeyUp(event) { + + dispatch(events.keyup, event); + + } + + function onPointerDown(event) { + dispatch(events.pointerdown, event); + + } + + function onPointerUp(event) { + + dispatch(events.pointerup, event); + + } + + function onPointerMove(event) { + + dispatch(events.pointermove, event); + + } + + } + +}; + +export { APP }; diff --git a/src/js/configurator.js b/src/js/configurator.js new file mode 100644 index 0000000..4d720e1 --- /dev/null +++ b/src/js/configurator.js @@ -0,0 +1,187 @@ +let shared_data = null; + +const Configurator = { + init: (priceCalculator, config_data) => { + Configurator.priceCalculator = priceCalculator; + shared_data = config_data; + }, + enable: () => { + $(("#larghezza, #altezza, select, input[name=vetro], input[type=radio]")).on('input', (e) => { + + + Configurator.checkTextures() + + let res = Configurator.generatePrice(); + + if(res == null) return; + + res = Configurator.generateConfigData(res); + + $(".prezzo").html(`€ ${res.prezzo.toFixed(2)}IVA Inclusa`); + window.configurazione = res; + + if($(e.target).data('model')){ + caricaModello($(e.target).data('model')); + } + }); + }, + generatePrice: () => { + + const config_data = {}; + config_data.height = $('#altezza').val(); + config_data.width = $('#larghezza').val(); + config_data.finitura = $('#prezzi_finiture').val(); + config_data.percentuale_accessori = 0; + + $('input.percentuale:checked').each((i, el) => { + let perc = $(el).data('percentuale'); + + perc = perc == undefined ? 0 : parseFloat(perc); + perc = isNaN(perc) ? 0 : perc; + config_data.percentuale_accessori += (perc / 100); + if($(el).hasClass('check_finitura')){ + + config_data.override_supplemento_finitura = true; + } + }); + //config_data.finitura_interna = $('input[name=finitura_interna]:checked').val(); + //config_data.finitura_esterna = $('input[name=finitura_esterna]:checked').val(); + config_data.maniglia = $('input[name=maniglia]:checked').val(); + + //config_data.finitura_cassonetto = $('input[name=cassonetto]:checked').val(); + config_data.finitura_avvolgibile = $('input[name=avvolgibile]:checked').val(); + + config_data.vetro = $('input[name=vetro]:checked').val(); + config_data.cerniere_scomparsa = $('.cerniere_a_scomparsa').val(); + config_data.altezza_maniglia = $('.altezza_maniglia').val(); + config_data.azionamento = $('.azionamento').val(); + config_data.tipo_ispezione = $('.tipo_ispezione').val(); + config_data.apertura_portoncino = $('.apertura_portoncino option:selected').text(); + config_data.tipo_ispezione = $('.tipo_ispezione option:selected').text(); + + let price = Configurator.priceCalculator.getGridPrice(config_data.width, config_data.height); + + if(price < 0){ + $(".errore-misure-cnt").removeClass('d-none'); + $(".prezzo").addClass('invisible'); + $(".cart-button").addClass('disabled'); + }else{ + $(".errore-misure-cnt").addClass('d-none'); + $(".prezzo").removeClass('invisible'); + $(".cart-button").removeClass('disabled'); + } + + config_data.prezzo_base = price; + + const config = Configurator.priceCalculator.getConfigurationPrice(config_data); + config.configurazione.height = config_data.height; + config.configurazione.width = config_data.width; + config.configurazione.nome = shared_data.nome_esteso; + + if(config_data.apertura_portoncino != "") + config.configurazione.apertura_portoncino = config_data.apertura_portoncino; + + if(config_data.tipo_ispezione != "") + config.configurazione.tipo_ispezione = config_data.tipo_ispezione; + + return config; + }, + generateConfigData: (config) =>{ + let input_finiture = $(`.finitura input[type=radio]:checked`); + + //console.log(input_finiture); + input_finiture.each((i, el) => { + let id_finitura = $(el).val(); + let nome_finitura = $(el).attr('name'); + let finitura = shared_data.data_finiture.find(f => f.id == id_finitura); + config.configurazione[nome_finitura] = finitura.descrizione; + }); + + //se è un cassonetto + if(shared_data.data_avvolgibile != null && shared_data.data_avvolgibile.length > 0){ + + let id_avvolgibile = $(`input[name=avvolgibile]:checked`).val(); + let avvolgibile = shared_data.data_avvolgibile.find(a => a.id == id_avvolgibile); + config.configurazione.avvolgibile = avvolgibile.descrizione; + + let id_cassonetto = $(`input[name=avvolgibile]:checked`).val(); + let cassonetto = shared_data.data_finiture.find(a => a.id == id_cassonetto); + config.configurazione.cassonetto = cassonetto.descrizione; + + } + + let val_maniglia = $(`input[name=maniglia]:checked`).val(); + let finitura_maniglia = shared_data.data_maniglie.find(m => m.id == val_maniglia); + let altezza = parseFloat(config.configurazione.height); + let mod_altezza = altezza % 10; + + if(mod_altezza > 0) altezza = (altezza - mod_altezza) + 10; + + let altezza_maniglia = shared_data.altezza_maniglia.find((m) => m.altezza == altezza); + + if(altezza_maniglia != null){ + config.configurazione.altezza_maniglia = altezza_maniglia.altezza_maniglia; + } + + if(altezza_maniglia == null && shared_data.altezza_maniglia.length > 0){ + config.configurazione.altezza_maniglia = shared_data.altezza_maniglia[0]; + } + + if(finitura_maniglia != null) + config.configurazione.maniglia = finitura_maniglia.descrizione; + + $('input.auto-radio:checked').each((i, el) => { + + let nome = $(el).attr('name'); + if($(`.${nome}`).hasClass('d-none')) return; + config.configurazione[nome] = $(el).val(); + }); + + return config; + }, + checkTextures: () =>{ + + let $interna = $("input[name=finitura_interna]:checked"); + let $esterna = $("input[name=finitura_esterna]:checked"); + + let interna = $interna.val(); + // modelli senza rivestimento esterno separato (tipo 'uniforme'): segue l'interno + let esterna = $esterna.length ? $esterna.val() : interna; + let internaPredefinita = $interna.data('predefinito') === true; + let tipo = $interna.data('tipo'); + let percentuale = $interna.data('percentuale'); + + // bianco: stesso colore predefinito su interno ed esterno -> nessun sovrapprezzo + // uniforme: stesso colore (non predefinito) su interno ed esterno -> sovrapprezzo "uniforme" + // bicolore: colori interno ed esterno diversi tra loro -> sovrapprezzo "bicolore" + if(interna === esterna){ + $("#prezzi_finiture").val(internaPredefinita ? 'bianco' : 'uniforme'); + }else{ + $("#prezzi_finiture").val('bicolore'); + } + + if(percentuale != undefined && percentuale.length > 0){ + $("#prezzi_finiture").val(''); + } + + if(tipo == 'uniforme' && internaPredefinita){ + $("#prezzi_finiture").val('bianco'); + return; + } + + if(tipo == 'uniforme' && !internaPredefinita){ + $("#prezzi_finiture").val('uniforme'); + return; + } + + if(shared_data.data_avvolgibile != null && shared_data.data_avvolgibile.length > 0){ + $("#prezzi_finiture").val('bicolore'); + } + + + }, + priceCalculator: null + +} + +export default Configurator; \ No newline at end of file diff --git a/src/js/menu_builder.js b/src/js/menu_builder.js new file mode 100644 index 0000000..8d951ea --- /dev/null +++ b/src/js/menu_builder.js @@ -0,0 +1,443 @@ +import Configurator from './configurator.js'; +import PriceCalculator from './price_calculator.js'; + + +let container = null; +let textureRequestVersion = {}; +let data_finiture = null; +let data_maniglie = null; +let data_avvolgibile = null; +let data_vetri = null; +let listino = null; + +const MenuBuilder = { + build: async (params, parent_element_id, model, data) => { + + data_finiture = data.data_finiture; + data_maniglie = data.data_maniglie; + data_avvolgibile = data.data_avvolgibile; + data_vetri = data.data_vetri; + listino = data.listino; + + const price_data = { + misure: listino, + finiture: params.prezzi_finiture, + cerniere: params.select.cerniere_a_scomparsa, + //altezza_maniglia: params.select.altezza_maniglia, + azionamento: params.select.azionamento, + vetri: data_vetri + } + + window.addEventListener('orientationchange', () => { + location.reload(); + }); + PriceCalculator.init(price_data); + let template_file = params.template != null ? params.template : 'menu.html'; + let template = await fetch(`./assets/_${template_file}?rnd=${Math.random()}`); + + + template = await template.text(); + + + container = document.getElementById(parent_element_id); + container.innerHTML = template; + + let isMobile = window.innerWidth <= 1108; + console.log("is mobile", isMobile); + if (isMobile && window.innerWidth < window.innerHeight) { + console.log('Mobile portrait mode setup'); + const menuHeader = $('.menu-categorie').html(); + const opzioniMenu = $('.opzioni-mobile-menu').html(); + $('#menu-mobile').append(menuHeader); + $('#menu-mobile').append(opzioniMenu); + $('.opzioni-mobile-menu').remove(); + $('.menu-categorie').remove(); + + //console.log($("#menu-mobile").height()); + $("#canvas-container").height(window.innerHeight - ($('#menu-mobile').height() + 90)); + + const menuScroll = $(".menu-scroll").html(); + $(".mobile-popup .popup-content").html(menuScroll); + $(".mobile-popup").addClass("d-none"); + $(".opzioni-menu").removeClass("d-none"); + $(".menu-scroll").remove(); + + $(".mobile-popup .popup-content").css('max-height', window.innerHeight * 0.6); + $(".mobile-popup .popup-content").css('overflow-y', 'auto'); + + const canvas_height = $('#canvas-container').height(); + $('.button-180').css('bottom', canvas_height / 2); + + $(".btn-opzioni").on('click', function () { + const opzione = $(this).data('opzione'); + $('.mob-menu-item').addClass('d-none'); + + $(`.mobile-popup-backdrop, .${opzione}-menu`).removeClass('d-none'); + + $(".mobile-popup").removeClass("d-none"); + // porta lo scrolling del popup mobile all'inizio quando si apre + const popupContent = document.querySelector('.mobile-popup .popup-content'); + if (popupContent) { + popupContent.scrollTop = 0; + // assicurati che il primo elemento sia visibile / focusabile + const firstChild = popupContent.querySelector('*'); + if (firstChild && typeof firstChild.focus === 'function') firstChild.focus(); + } + }); + + $(".btn-popup").removeClass("d-none"); + $(".btn-popup, .mobile-popup-backdrop, .mobile-popup .btn-container").on('click', function () { + $(".mobile-popup-backdrop, .mobile-popup").addClass("d-none"); + }); + + //console.log('Mobile portrait mode detected'); + } else if (isMobile && window.innerWidth >= window.innerHeight) { + $("#left-container").addClass('overflow-auto'); + //console.log('Mobile landscape mode detected'); + } + + let listino_min = listino[0]; + let listino_max = listino[listino.length - 1]; + const maxLarghezza = Math.max(...listino.map(item => item.larghezza)); + const maxAltezza = Math.max(...listino.map(item => item.altezza)); + const minLarghezza = Math.min(...listino.map(item => item.larghezza)); + const minAltezza = Math.min(...listino.map(item => item.altezza)); + $('.titolo').html(`${params.nome}`); + $('.larghezza-max').html(`${minLarghezza}-${maxLarghezza}cm`); + $('.altezza-max').html(`${minAltezza}-${maxAltezza}cm`); + + window.sistema = data.sistema; + + buildListaModelli(data.modelli, data.modello_id, data.categoria, data.sistema); + + if (!isMobile) { + let hMisure = $(".menu-categorie").outerHeight(); + + $('.menu-scroll').height(window.innerHeight - hMisure); + } else { + if(window.innerWidth < window.innerHeight) + container = document.querySelector('.mobile-popup'); + } + + + + for (const key in params.radios) { + let options = params.radios[key]; + + let res = buildRadios(options, key, model); + container.querySelector(`.${key}`).innerHTML = res.html; + console.log(key); + if (options.length > 0) + $(`.cnt-${key}`).removeClass('d-none'); + + if (res.has_sub_options) { + let radios = container.querySelectorAll(`.${key} input[type=radio]`); + radios.forEach((radio) => { + radio.addEventListener('change', () => { + toggleSubOption(key, { target: radio }); + }); + }); + } + + } + + if (isMobile) { + $(".sistema-menu, .finiture-menu, .accessori-menu,.vetro-menu, .misure-menu").addClass('d-none'); + + + } + + $('.apertura input').on('change', () => { + let val = $('.apertura input:checked').val(); + + let p = params.radios.apertura.find(r => r.label == val); + + cambiaApertura('infisso', p.scale_x); + + }); + + + /* scorro i tipi di finitura nella form e se è uniforme nascondo i tasti non necessari */ + params.finiture.forEach((f) => { + buildTextureRadio(model, f.nome, f.cartella_anteprime, f.cartella, f.categorie, data[f.data], f.tipo); + if(f.nome == 'finitura_interna' && f.tipo == 'uniforme'){ + //buildTextureRadio(model, 'finitura_esterna', f.cartella_anteprime, f.cartella, f.categorie, data[f.data]); + $('.finiture-esterne-menu').addClass('d-none'); + $('.btn-opzioni.finiture-esterne').addClass('d-none'); + $('.btn-opzioni.finiture-interne p').text('Finiture'); + $('.btn-opzioni.finiture-interne img').attr('src','./assets/img/opzioni/finiture.png'); + $('.finiture-interne-menu .menu-header').text('Finiture'); + } + }) + + $('.info-popup').on('click', function (e) { + e.preventDefault(); + + if (!$('#popup').hasClass('d-none')) { + $('#popup').addClass('d-none'); + $('#popup img').remove(); + } + + const imageUrl = $(this).data('image-url'); + const imgElement = document.createElement('img'); + imgElement.src = imageUrl; + imgElement.alt = 'Popup Image'; + imgElement.classList.add('img-fluid', 'popup-image'); + document.querySelector('#popup').appendChild(imgElement); + $("#popup").removeClass('d-none'); + + }); + + $('#popup .close').on('click', function () { + $('#popup').addClass('d-none'); + $('#popup img').remove(); + }); + + for (const key in params.select) { + let options = params.select[key]; + + buildSelect(options, key); + + } + + /* opzioni disabilitate per richiesta del cliente */ + /* + if(params.select.cerniere_a_scomparsa != null) + buildSelect(params.select.cerniere_a_scomparsa, 'cerniere_a_scomparsa'); + + if(params.select.altezza_maniglia != null) + buildSelect(params.select.altezza_maniglia, 'altezza_maniglia'); + */ + let size = PriceCalculator.getMinSize(); + + $('#larghezza').val(size.larghezza + '.00'); + $('#altezza').val(size.altezza + '.00'); + + + + return PriceCalculator; + } +}; + +const buildListaModelli = (modelli, modello, categoria, sistema) => { + + let lista_modelli = modelli.filter(m => m.enabled !== false); + let html = ''; + lista_modelli.forEach(m => { + + let selected = m.id == modello ? 'selected' : ''; + + html += `
+ + ${m.id} + +

${m.nome}

+ +
`; + + }); + + $('.lista-modelli').html(html); +} + +const buildSelectFiniture = (options) => { + let select = container.querySelector(`#prezzi_finiture`); + select.innerHTML = ``; + options.forEach(option => { + select.innerHTML += ``; + }); + + select.addEventListener('change', (e) => { + + let val = e.target.value; + if (val != 'bicolore' && val != 'uniforme') { + + $(container).find(`.menu-container.finitura`).each((i, el) => { + + if (!$(el).hasClass(val) && val != 'uniforme') { + $(el).find('.menu-options input').first().trigger('click'); + } + }); + } + + $('.menu-container.finitura').addClass('d-none'); + document.querySelectorAll(`.finitura.${val}`).forEach(el => { + el.classList.remove('d-none'); + }); + }); +} + +const buildSelect = (options, parent) => { + let select = container.querySelector(`.${parent}`); + + select.innerHTML = ``; + options.forEach(option => { + + select.innerHTML += ``; + }); + + select.addEventListener('change', (e) => { + let val = e.target.value; + const el = options.find(o => o.valore == val); + const prezzo = el.prezzo ?? 0; + + if (el.func != null) { + window[el.func](el.categoria); + } + + if (el.flip != null) + checkFlip(el.flip); + + }); + + $(`.cnt-${parent}`).removeClass('d-none'); +} + +const buildTextureRadio = async (model, parent, icon_folder, texture_folder, categories, data, tipo = '') => { + + let html = ''; + + let defaultIndex = data.findIndex(option => option.predefinito === true); + if (defaultIndex < 0) defaultIndex = 0; + + data.forEach((option, i) => { + let popup_link = option.pop_up != null ? option.pop_up : ''; + + if (popup_link != '') { + popup_link = ``; + } + + let checked = i == defaultIndex && parent != 'maniglia' ? 'checked' : ''; + html = + `
+ +
${option.descrizione} ${popup_link}
+
`; + + + let radio = document.createElement('div'); + radio.innerHTML = html; + radio = radio.firstChild; + + radio.addEventListener('change', async (e) => { + + textureRequestVersion[parent] = (textureRequestVersion[parent] || 0) + 1; + const requestVersion = textureRequestVersion[parent]; + const isStale = () => textureRequestVersion[parent] !== requestVersion; + + option.categories = categories; + option.folder = model; + option.texture_folder = texture_folder; + option.isStale = isStale; + $(".mobile-popup-backdrop, .mobile-popup").addClass("d-none"); + + await cambiaTexture(option); + if (isStale()) return; + + if (option.categories.includes('interno')) { + option.categories = ['corpo_base']; + cambiaColoreCB(option); + } + }); + + + //if(!container.querySelector(`.menu-options.${parent}`)) console.log(parent); + container.querySelector(`.menu-options.${parent}`).appendChild(radio); + }); + + $(`.cnt-${parent}`).removeClass('d-none'); + //container.querySelector(`.${parent}`).innerHTML = html; +}; + +const buildRadios = (options, key, model) => { + let html = ''; + let has_sub_options = false; + + options.forEach((option, i) => { + + let percentuale = ''; + let modello = ''; + let class_percentuale = ''; + let has_options = false; + let sub_option_container = null; + let html_func = ''; + let has_options_class = ''; + let container_id = ''; + + if (option.opzioni != null) { + has_sub_options = true; + has_options = true; + + for (const key_opt in option.opzioni) { + let options = option.opzioni[key_opt]; + console.log(key_opt) + + let res = buildRadios(options, key_opt, model); + container.querySelector(`.${key_opt}`).innerHTML = res.html; + sub_option_container = container.querySelector(`.${key_opt}`).parentElement.parentElement; + sub_option_container.classList.add('d-none'); + } + } + + if (option.model != null) { + modello = `data-model="${option.model}"`; + } + + if (option.percentuale != null) { + percentuale = `data-percentuale="${option.percentuale}"`; + class_percentuale = 'percentuale'; + } + + if (has_options) { + has_options_class = 'has-options'; + html_func = `onclick="toggleSubOption('${sub_option_container.id}',event)"`; + container_id = `data-toggle-id="${sub_option_container.id}"`; + } + + let checked = i == 0 ? 'checked' : ''; + + html += + `
+ +
${option.label}
+
`; + + + + }); + + return { + html: html, + has_sub_options: has_sub_options + }; +}; + + +window.toggleSubOption = (id, e) => { + + let parent_class = id + '-sub-option'; + const sub_option_container = container.querySelector(`.${parent_class}`); + + if (e.target.classList.contains('has-options')) { + container.querySelector(`#${e.target.dataset.toggleId}`).classList.remove('d-none'); + } else { + sub_option_container.classList.add('d-none'); + } + +} +export default MenuBuilder; + + diff --git a/src/js/params_parser.js b/src/js/params_parser.js new file mode 100644 index 0000000..76a3a11 --- /dev/null +++ b/src/js/params_parser.js @@ -0,0 +1,14 @@ +const ParamsParser = { + getQueryParams: () => { + const params = new URLSearchParams(window.location.search); + const queryParams = {}; + + params.forEach((value, key) => { + queryParams[key] = value; + }); + + return queryParams; + } +}; + +export default ParamsParser; diff --git a/src/js/price_calculator.js b/src/js/price_calculator.js new file mode 100644 index 0000000..bfc2436 --- /dev/null +++ b/src/js/price_calculator.js @@ -0,0 +1,122 @@ + +let listini = null; + +const toPercentuale = (valore) => { + const perc = parseFloat(valore); + return isNaN(perc) ? 0 : perc / 100; +}; + +const toPrezzo = (valore) => { + const prezzo = parseFloat(valore); + return isNaN(prezzo) ? 0 : prezzo; +}; + +const PriceCalculator = { + init: async (data) => { + + listini = data; + + + }, + getMinSize: () => { + + const firstSize = listini.misure.find(item => item.prezzo !== 0); + + return firstSize; + + }, + getGridPrice: (width, height) => { + let altezza = parseFloat(height); + let larghezza = parseFloat(width); + + let mod_altezza = altezza % 10; + let mod_larghezza = larghezza % 10; + + if(mod_altezza > 0) altezza = (altezza - mod_altezza) + 10; + if(mod_larghezza > 0) larghezza = (larghezza - mod_larghezza) + 10; + + let misure = listini.misure.find((el) => { + if(el.altezza == altezza && el.larghezza == larghezza) + return el; + }); + + if(misure == null) return -1; + return misure.prezzo; + }, + getConfigurationPrice: (data) => { + + let prezzo_finale = data.prezzo_base; + let percentuale_finiture = 0; + let prezzo_accessori = 0; + let percentuale_vetro = 0; + let percentuale_accessori = 0; + + const config = {}; + + if(data.finitura && data.override_supplemento_finitura !== true){ + let supplemento = (listini.finiture ?? []).find((el) => el.finitura === data.finitura); + + config.tipo_finitura = supplemento; + + percentuale_finiture = toPercentuale(supplemento?.percentuale); + } + + if(data.cerniere_scomparsa != null){ + let supplemento = listini.cerniere.find((el) => { + if(el.valore == data.cerniere_scomparsa) + return el; + }); + + config.cerniere_scomparsa = supplemento; + + prezzo_accessori += toPrezzo(supplemento?.prezzo); + } + + if(data.altezza_maniglia != null){ + + let supplemento = listini.altezza_maniglia.find((el) => { + if(el.valore == data.altezza_maniglia) + return el; + }); + + config.altezza_maniglia = supplemento; + + prezzo_accessori += toPrezzo(supplemento?.prezzo); + } + + if(data.azionamento != null){ + + let supplemento = listini.azionamento.find((el) => { + if(el.valore == data.azionamento) + return el; + }); + + config.azionamento = supplemento; + + prezzo_accessori += toPrezzo(supplemento?.prezzo); + } + + if(data.vetro != null){ + let vetro = listini.vetri.find((el) => { + if(el.id == data.vetro) + return el; + }); + + config.vetro = vetro; + percentuale_vetro = toPercentuale(vetro?.percentuale); + } + + + prezzo_finale = (prezzo_finale * (1 + percentuale_finiture+percentuale_vetro+data.percentuale_accessori)) + prezzo_accessori; + prezzo_finale += prezzo_finale * 22 / 100; // IVA 22% + + return { + prezzo: prezzo_finale, + configurazione: config + } + } + + +} + +export default PriceCalculator; \ No newline at end of file diff --git a/src/js/starter.js b/src/js/starter.js new file mode 100644 index 0000000..fc4a52d --- /dev/null +++ b/src/js/starter.js @@ -0,0 +1,349 @@ + +import * as THREE from 'three'; +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import $ from 'jquery'; +import { APP } from './app.js'; +import ParamsParser from './params_parser.js'; +import MenuBuilder from './menu_builder.js'; +import '@fortawesome/fontawesome-free/css/all.min.css'; +import './price_calculator.js'; + +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'bootstrap/dist/js/bootstrap.bundle.min.js'; +import Configurator from './configurator.js'; + + +window.$ = $; +window.THREE = THREE; // Used by APP Scripts. +window.OrbitControls = OrbitControls; // Used by APP Scripts. +window.GLTFLoader = GLTFLoader; // Used by APP Scripts. + + +$(() => { + + $('.cart-button').on('click', async () => { + + if($('.cart-button').hasClass('disabled')) + return; + const img = await window.generaImagine(); + const data = { + payload: window.configurazione, + img: img, + qty: parseInt($('.quantity-input').val()), + fn: 'aggiungi_prodotto' + }; + + + window.parent.postMessage(data, "https://www.infissionline.it"); + window.parent.postMessage(data, "https://infissionline.test"); + }); +}); + +const start = async () => { + const params = ParamsParser.getQueryParams(); + + let data_folder = `./assets/data/sistemi/${params.sistema}`; + let modello_folder = `./assets/data/modelli/${params.sistema}`; + + + const modelli = await (await fetch(`${data_folder}/modelli.json?rnd=` + Math.random())).json(); + + const modello = modelli.find(m => m.id === params.type); + + const data = await (await fetch(`${modello_folder}/${modello.cartella}/form.json?rnd=${Math.random()}`)).json(); + const data_finiture = modello.moduli.includes('modello') ? (await (await fetch(`${modello_folder}/${modello.cartella}/modello.json?rnd=${Math.random()}`)).json()).filter(el => el.id != "") : []; + const data_maniglie = modello.moduli.includes('maniglia') ? (await (await fetch(`${modello_folder}/${modello.cartella}/maniglia.json?rnd=${Math.random()}`)).json()).filter(el => el.id != "") : []; + const data_avvolgibile = modello.moduli.includes('avvolgibile') ? (await (await fetch(`${modello_folder}/${modello.cartella}/avvolgibile.json?rnd=${Math.random()}`)).json()).filter(el => el.id != "") : []; + const data_vetri = modello.moduli.includes('vetro') ? (await (await fetch(`${modello_folder}/${modello.cartella}/vetro.json?rnd=${Math.random()}`)).json()).filter(el => el.id != "") : []; + const listino = modello.moduli.includes('listino') ? await (await fetch(`${modello_folder}/${modello.cartella}/listino.json?rnd=${Math.random()}`)).json() : []; + const altezza_maniglia = modello.moduli.includes('altezza_maniglia') ? await (await fetch(`${data_folder}/altezza_maniglia_${modello.categoria}.json?rnd=${Math.random()}`)).json() : []; + + let priceCalculator = await MenuBuilder.build(data, 'left-container', modello.cartella_texture || modello.cartella, { + modelli: modelli, + categoria: modello.categoria, + sistema: params.sistema, + data_finiture: data_finiture, + data_maniglie: data_maniglie, + data_avvolgibile: data_avvolgibile, + data_vetri: data_vetri, + listino: listino, + modello_id: modello.id + }); + + Configurator.init(priceCalculator, { + modello: modello, + nome_esteso: data.nome, + data_finiture: data_finiture, + data_maniglie: data_maniglie, + data_avvolgibile: data_avvolgibile, + data_vetri: data_vetri, + altezza_maniglia: altezza_maniglia, + listino: listino + }); + + Configurator.enable(); + + $(`.menu-header-sistema .${params.sistema}`).addClass('selected'); + $(`.v-divider.${params.sistema}`).addClass('d-none'); + + var player = new APP.Player('canvas-container'); + var playerOff = new APP.Player('canvas-offscreen'); + + var loader = new THREE.FileLoader(); + loader.load('./assets/app.json', function (text) { + player.load(JSON.parse(text)); + playerOff.load(JSON.parse(text)); + player.rotateBackground(Math.PI); + playerOff.rotateBackground(Math.PI); + playerOff.setSize(playerOff.dom.clientWidth, playerOff.dom.clientHeight); + + var gltfLoader = new GLTFLoader(); + + gltfLoader.load(`./assets/models/${params.sistema}/${modello.modello}`, function (gltf) { + document.getElementById('loadingBarContainer').style.display = 'none'; + //gltf.scene.rotation.y = THREE.MathUtils.degToRad(80); + gltf.scene.name = 'infisso'; + window.current_model = modello.modello; + gltf.scene.userData.name = modello.modello; + + //$("input[type=radio]").first().trigger("click"); + + gltf.scene.position.y = 0; + gltf.scene.position.z = 0.1; + //gltf.scene.rotation.set(0, Math.PI, 0); + player.addToScene(gltf.scene); + player.viewFrontSide(); + + playerOff.addToScene(gltf.scene.clone()); + playerOff.viewFrontSide(); + $('#canvas-offscreen').addClass('d-none'); + player.dom.style.position = 'relative'; + + if (params.type != 'cassonetto') { + $("input[name=maniglia]").first().trigger("click"); + $("input[name=finitura_interna]").eq(1).trigger("click"); + $("input[name=finitura_interna]").eq(0).trigger("click"); + } + + $("#larghezza").first().trigger("change"); + + //player.addMarker("Cylinder028", 'left', 'https://www.youtube.com/embed/l_zqI0v7JcU?si=Vq2k8nWHYk13esMQ'); + //player.addMarker("marker_1", 'left', 'https://www.youtube.com/embed/l_zqI0v7JcU?si=Vq2k8nWHYk13esMQ'); + //player.addMarker("marker_2", 'right', 'https://www.youtube.com/embed/LCi9jdF_4MQ?si=b8cyxSOI2sKEmmcm'); + + }, + function (xhr) { + var percentComplete = (xhr.loaded / xhr.total) * 100; + document.getElementById('loadingBarProgress').style.width = percentComplete + '%'; + document.getElementById('loadingPercentage').innerText = Math.round(percentComplete) + '%'; + }, function (error) { + console.error('An error happened while loading the GLB file:', error); + }); + + player.setSize(player.dom.clientWidth, player.dom.clientHeight); + player.play(); + + window.addEventListener('resize', function () { + //console.log(player.dom.clientWidth, player.dom.clientHeight); + player.setSize(player.dom.clientWidth, player.dom.clientHeight); + player.render(); + }); + + }); + + $('.quantity-btn').on('click', (e) => { + + const input = document.querySelector('.quantity-input'); + let value = parseInt(input.value); + + if (e.target.classList.contains('increment')) { + input.value = value + 1; + } else if (e.target.classList.contains('decrement') && value > 1) { + input.value = value - 1; + } + }); + + window.generaImagine = async () => { + playerOff.render(); + $('#canvas-offscreen').removeClass('d-none'); + const canvas = $('#canvas-offscreen canvas')[0]; + const dataUrl = await canvas.toDataURL('image/png'); + + $('#canvas-offscreen').addClass('d-none'); + return dataUrl; + const a = document.createElement('a'); + a.href = dataUrl; + a.download = 'immagine.png'; + a.click(); + } + + window.render = () => { + player.render(); + } + + let currentView = 'front'; + + window.cambiaTexture = (data) => { + return new Promise(async (resolve, reject) => { + try { + + await player.changeTexture(data) + //console.log(data) + //console.log('window.rotate', window.rotate); + if (data && data.categories.includes('esterno') && currentView !== 'back'){ + if(window.rotate){ + player.flipView(); + currentView = 'back'; + } + + } + + + if (data && (data.categories.includes('interno') || data.categories.includes('maniglia')) && currentView !== 'front'){ + + if(window.rotate){ + player.flipView(); + currentView = 'front'; + } + + } + + render(); + resolve(); + } catch (error) { + reject(error); + } + }); + } + + window.flip180 = () => { + player.flipView(); + currentView = currentView === 'front' ? 'back' : 'front'; + render(); + } + + window.checkFlip = (direction) => { + + if(direction != currentView) + flip180(); + + } + + window.cambiaColoreCB = (data) => { + player.changeColor(data); + render(); + } + + window.cambiaApertura = (name, scale) => { + player.changeXscale(name, scale); + render(); + } + + window.mostraCerniere = (categoria) => { + player.showObject(categoria); + render(); + } + + window.nascondiCerniere = (categoria) => { + player.hideObject(categoria); + render(); + } + + window.rotate = true; + + window.caricaModello = (modello) => { + + let textureButtons = $(".texture-button:checked"); + + window.rotate = false; + + const radios = {}; + $('.texture-button[name]').each(function() { + radios[$(this).attr('name')] ??= this; + }); + + document.querySelectorAll(".texture-button[name=maniglia]").forEach(el => el.checked = false) + + //console.log(textureButtons) + if(player.swapModel(modello)){ + + for(let el in radios){ + $(radios[el]).trigger("click"); + } + + $(textureButtons).each((i, el) => { + $(el).trigger("click"); + }); + + setTimeout(() => { + window.rotate = true; + }, 1000); + // + return; + } + + document.getElementById('loadingBarContainerAlt').style.display = 'flex'; + document.getElementById('loadingBarProgressAlt').style.width = '0%'; + document.getElementById('loadingPercentageAlt').innerText = '0%'; + + var gltfLoader = new GLTFLoader(); + + gltfLoader.load(`./assets/models/${modello}`, function (gltf) { + document.getElementById('loadingBarContainerAlt').style.display = 'none'; + $('#canvas-offscreen').removeClass('d-none'); + //gltf.scene.rotation.y = THREE.MathUtils.degToRad(80); + gltf.scene.name = 'infisso'; + + gltf.scene.userData.name = modello; + + setTimeout(() => { + for(let el in radios){ + $(radios[el]).trigger("click"); + } + }); + + gltf.scene.position.y = 0; + gltf.scene.position.z = 0.1; + //gltf.scene.rotation.set(0, Math.PI, 0); + player.hideModel('infisso'); + playerOff.hideModel('infisso'); + player.addToScene(gltf.scene); + player.viewFrontSide(); + + playerOff.addToScene(gltf.scene.clone()); + //playerOff.viewFrontSide(); + + $('#canvas-offscreen').addClass('d-none'); + player.dom.style.position = 'relative'; + + if (params.type != 'cassonetto') { + $("input[name=maniglia]").first().trigger("click"); + } + + $("#larghezza").first().trigger("change"); + + $(textureButtons).each((i, el) => { + $(el).trigger("click"); + }); + + setTimeout(() => { + window.rotate = true; + }, 1000); + //player.addMarker("Cylinder028", 'left', 'https://www.youtube.com/embed/l_zqI0v7JcU?si=Vq2k8nWHYk13esMQ'); + //player.addMarker("marker_1", 'left', 'https://www.youtube.com/embed/l_zqI0v7JcU?si=Vq2k8nWHYk13esMQ'); + //player.addMarker("marker_2", 'right', 'https://www.youtube.com/embed/LCi9jdF_4MQ?si=b8cyxSOI2sKEmmcm'); + + }, + function (xhr) { + var percentComplete = (xhr.loaded / xhr.total) * 100; + document.getElementById('loadingBarProgressAlt').style.width = percentComplete + '%'; + document.getElementById('loadingPercentageAlt').innerText = Math.round(percentComplete) + '%'; + }, function (error) { + console.error('An error happened while loading the GLB file:', error); + }); + } +} + + +start(); \ No newline at end of file diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..c92c88e --- /dev/null +++ b/vite.config.js @@ -0,0 +1,50 @@ +import { defineConfig } from 'vite'; +import path from 'path'; +import { viteStaticCopy } from 'vite-plugin-static-copy'; +import { ViteMinifyPlugin } from 'vite-plugin-minify' + +export default defineConfig({ + server: { + host: '0.0.0.0', + port: 5174 + }, + base: '', + root: 'src', + build: { + outDir: '../dist', + minify: true, + cssMinify: true, + + emptyOutDir: true, + rollupOptions: { + input: { + main: path.resolve(__dirname, 'src/index.html') + } + }, + }, + plugins: [ + viteStaticCopy({ + targets: [ + { + src: 'assets', + dest: '' + } + ] + }), + ViteMinifyPlugin({ + minify: { + removeComments: true, + collapseWhitespace: true, + minifyCSS: true, + minifyJS: true, + minifyURLs: true + } + }) + ], + resolve: { + alias: { + 'three': path.resolve(__dirname, 'node_modules/three'), + '@': path.resolve(__dirname, 'src') + } + } +});