Files
kreios/CLAUDE.md
T
carmine.savino 1208187a17 feat: integrate Draco compression for 3D models in backoffice
- Added support for uploading and compressing GLB models using Draco in the backoffice.
- Implemented commands for importing and re-compressing 3D models.
- Updated model resource to handle 3D model uploads and validations.
- Introduced a new script for compressing GLB files with Draco.
- Modified Vite configuration to remove GLB files from the build output.
- Updated package dependencies to include GLTF-transform libraries for handling 3D models.
- Added tests to ensure proper functionality of 3D model handling and compression.
2026-11-01 10:35:50 +01:00

8.6 KiB
Raw Blame History

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) — "kreiosinfissi-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 <iframe> embedded on https://www.kreiosinfissi.it (or https://kreiosinfissi.test in dev), with the final "add to cart" action posted back to the parent window via postMessage.

Commands

Package manager is pnpm 10.34.7 (pinned in packageManager; build.sh/dev.sh run it via npx pnpm@10.34.7, so lockfiles stay at lockfileVersion: '9.0'). pnpm-lock.yaml is the lockfile of record even though package-lock.json is also present.

pnpm install       # install deps
./dev.sh          # single server: Laravel on :8000 + both frontends in `vite build --watch`
./build.sh        # build configuratore + app cliente into backoffice/public
pnpm build        # configuratore only -> backoffice/public/configuratore (root is src/)

Everything is served by Laravel on one origin: configuratore at /configuratore/?sistema=…&type=…, quote app (backoffice/cliente, Vue) at /preventivo/, API at /api, admin at /admin. The index.html of each frontend is served by routes in backoffice/routes/web.php; there is no HMR, reload after the rebuild.

There is no test suite (pnpm test is a stub that exits with an error) and no linter configured.

Regenerating PVC price lists

scripts/extract_listini_pvc.py parses PVC_LISTINO.xls and regenerates src/assets/data/modelli/pvc/<modello>/listino.json files. Requires a Python venv with xlrd (pip install -r scripts/requirements.txt). Run with python3 scripts/extract_listini_pvc.py from repo root. Any new title in the spreadsheet needs an entry in that script's KNOWN_FOLDERS map or it won't be matched to an existing model folder.

Architecture

Entry point is src/js/starter.js, loaded from src/index.html. Flow on load (start()):

  1. ParamsParser.getQueryParams() (src/js/params_parser.js) reads the URL query string. Two params drive everything: sistema (pvc | alluminio | legno | acciaio) and type (model id). See docs/URL_PARAMS.md for the full param contract and the list of valid type values per sistema. Note: that doc describes a more modular refactor (Application.js, communication/UrlParamsParser.js, communication/IframeBridge.js) that does not exist yet in src/js — the current implementation is the flatter starter.js/params_parser.js described here.
  2. Catalog data comes from the Laravel backoffice (backoffice/, API base in src/js/api_base.js): GET /api/sistemi (systems + active models that have a price list; models without one are hidden and flagged in the admin list) and GET /api/sistemi/<sistema>/modelli/<modello>/configurazione, which returns the menu wiring (form: radios, selects, finish menus — the old form.json shape), finishes, handles, glass types and the price-grid sizes, with image URLs served by GET /api/media/... (CORS-enabled, needed for WebGL textures). Only avvolgibile.json and sistemi/<sistema>/altezza_maniglia_<categoria>.json are still read from src/assets/data.
  3. MenuBuilder.build() (src/js/menu_builder.js) injects an HTML template (assets/_menu.html, or assets/_menu-cassonetto.html when type=cassonetto) into #left-container, builds the model-picker list, handles the mobile-portrait/landscape layout reflow, and initializes PriceCalculator. It returns the price calculator instance used by the rest of the app.
  4. Configurator.init() / .enable() (src/js/configurator.js) wires a single delegated input handler on the size fields, selects and radios. On every change (debounced) it sends the selected codes to POST /api/sistemi/<sistema>/modelli/<modello>/prezzo, stores the result in window.configurazione and updates the DOM. "Add to cart" sends the same codes (scelte): the backend recomputes the price and ignores any price coming from the client.
  5. APP.Player (src/js/app.js) wraps a Three.js renderer/scene/camera (scene graph loaded from src/assets/app.json, a three.js-editor-style export) plus the actual product GLB, loaded from modello.modello_url (see "3D models" below) with a DRACOLoader. Two players are created: player (visible canvas) and playerOff (offscreen, used only to render the PNG snapshot sent to the cart). Key methods: swapModel/ hideModel (toggle between pre-loaded variants in the same scene), changeTexture/changeColor (applied per userData.categoria mesh tag — categories like interno, esterno, maniglia, vetro, cerniere), flipView (180° GSAP-animated turn between front/back view), addMarker (in-scene hotspot with a video popup).
  6. Pricing is server-side only: backoffice/app/Support/Configuratore/CalcolatorePrezzo.php. Computed in cascading phases, each phase's percentages applied to the previous phase's total (VAT excluded): serramento = base × (1 + finitura %), then profili extra on the serramento (measures + finishes only), then the other options (telaio, apertura, accessori), then + variazione vetro. Base is the first grid size ≥ the requested one (mm). Supplements come from opzioni rows (finish type bianco/uniforme/bicolore, hinges, profili extra, ...): percentage, fixed, or per linear metre (ml from L/H multipliers + extra mm). Groups with ui = multipla (profili extra) accept several codes, with same-alternativa options mutually exclusive. Sistema-level rows can be overridden per model by group. Option groups themselves live in gruppi_opzioni (title, control radio/select/multipla, menu section, and the model moduli key that enables them): a group without fixed markup in _menu.html is rendered by MenuBuilder into .opzioni-sistema / .opzioni-accessori. Select options can call a window[func] (e.g. nascondiCategoria with categoria: maniglia for the motor) via their parametri. The glass variation is Σ max(pane area, minimo_mq) × maggiorazione_mq, with panes from the model's regole_vetro (spec: "Specifica tecnica vetri"). All of it is edited in Filament under "Configuratore". src/js/price_calculator.js only wraps the API call and the grid sizes.

3D models

GLB files are managed by the backoffice, one per Modello (uploaded in Filament, "Configurazione 3D" section). On save, App\Support\Configuratore\Modelli3d runs scripts/comprimi-glb.mjs (Node, @gltf-transform + draco3dgltf from the root node_modules) and keeps two copies: the uncompressed original on the private local disk (modelli-3d/originali/..., never served) and the Draco copy on the public disk (modelli-3d/draco/<sistema>/<codice>-<hash>.glb), served via GET /api/media/... with a 1-year immutable cache. /api/sistemi exposes it as modello_url; models without one are not shown in the configurator. php artisan modelli3d:importa imports the legacy src/assets/models files (one-off), modelli3d:ricomprimi regenerates the Draco copies from the originals. The Vite build never ships .glb files. In production set NODE_BINARY if PHP-FPM has no node on its PATH.

Data conventions

  • Every 3D-visible part is tagged with userData.categoria in the GLB/scene so JS can show/hide/retexture it without knowing mesh names (interno, esterno, maniglia, cerniere, vetro, ...).
  • Which menu sections a model shows (and which items enter the price) is driven by its moduli in the backoffice (Modello::MODULI, e.g. finitura_esterna, apertura, telaio, vetro). The per-model JSON files under src/assets/data/modelli/ are legacy and no longer read (except avvolgibile.json).
  • Texture and icon images live in the backoffice (storage/app/public/configuratore/..., seeded once by ConfiguratoreSeeder from backoffice/database/seeders/data/configuratore/) and are loaded lazily per selection (APP.Player.changeTexture) from absolute URLs; a monotonically-checked isStale() guard discards a texture load if the user changed the selection again before it finished.
  • window.* globals (window.configurazione, window.cambiaTexture, window.caricaModello, window.rotate, window.sistema, etc.) are the integration surface between the jQuery-driven menu markup (inline onclick/onchange in the _menu*.html templates) and the JS modules — expect to grep for a window.<name> = assignment when tracing menu behavior, not for exported module functions.