- 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.
8.6 KiB
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()):
ParamsParser.getQueryParams()(src/js/params_parser.js) reads the URL query string. Two params drive everything:sistema(pvc|alluminio|legno|acciaio) andtype(model id). Seedocs/URL_PARAMS.mdfor the full param contract and the list of validtypevalues persistema. Note: that doc describes a more modular refactor (Application.js,communication/UrlParamsParser.js,communication/IframeBridge.js) that does not exist yet insrc/js— the current implementation is the flatterstarter.js/params_parser.jsdescribed here.- Catalog data comes from the Laravel backoffice (
backoffice/, API base insrc/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) andGET /api/sistemi/<sistema>/modelli/<modello>/configurazione, which returns the menu wiring (form: radios, selects, finish menus — the oldform.jsonshape), finishes, handles, glass types and the price-grid sizes, with image URLs served byGET /api/media/...(CORS-enabled, needed for WebGL textures). Onlyavvolgibile.jsonandsistemi/<sistema>/altezza_maniglia_<categoria>.jsonare still read fromsrc/assets/data. MenuBuilder.build()(src/js/menu_builder.js) injects an HTML template (assets/_menu.html, orassets/_menu-cassonetto.htmlwhentype=cassonetto) into#left-container, builds the model-picker list, handles the mobile-portrait/landscape layout reflow, and initializesPriceCalculator. It returns the price calculator instance used by the rest of the app.Configurator.init()/.enable()(src/js/configurator.js) wires a single delegatedinputhandler on the size fields, selects and radios. On every change (debounced) it sends the selected codes toPOST /api/sistemi/<sistema>/modelli/<modello>/prezzo, stores the result inwindow.configurazioneand updates the DOM. "Add to cart" sends the same codes (scelte): the backend recomputes the price and ignores any price coming from the client.APP.Player(src/js/app.js) wraps a Three.js renderer/scene/camera (scene graph loaded fromsrc/assets/app.json, a three.js-editor-style export) plus the actual product GLB, loaded frommodello.modello_url(see "3D models" below) with aDRACOLoader. Two players are created:player(visible canvas) andplayerOff(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 peruserData.categoriamesh tag — categories likeinterno,esterno,maniglia,vetro,cerniere),flipView(180° GSAP-animated turn between front/back view),addMarker(in-scene hotspot with a video popup).- 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 fromopzionirows (finish type bianco/uniforme/bicolore, hinges, profili extra, ...): percentage, fixed, or per linear metre (ml from L/H multipliers + extra mm). Groups withui = multipla(profili extra) accept several codes, with same-alternativaoptions mutually exclusive. Sistema-level rows can be overridden per model by group. Option groups themselves live ingruppi_opzioni(title, control radio/select/multipla, menu section, and the modelmodulikey that enables them): a group without fixed markup in_menu.htmlis rendered byMenuBuilderinto.opzioni-sistema/.opzioni-accessori. Select options can call awindow[func](e.g.nascondiCategoriawithcategoria: manigliafor the motor) via theirparametri. The glass variation is Σ max(pane area, minimo_mq) × maggiorazione_mq, with panes from the model'sregole_vetro(spec: "Specifica tecnica vetri"). All of it is edited in Filament under "Configuratore".src/js/price_calculator.jsonly 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.categoriain 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
moduliin the backoffice (Modello::MODULI, e.g.finitura_esterna,apertura,telaio,vetro). The per-model JSON files undersrc/assets/data/modelli/are legacy and no longer read (exceptavvolgibile.json). - Texture and icon images live in the backoffice (
storage/app/public/configuratore/..., seeded once byConfiguratoreSeederfrombackoffice/database/seeders/data/configuratore/) and are loaded lazily per selection (APP.Player.changeTexture) from absolute URLs; a monotonically-checkedisStale()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 (inlineonclick/onchangein the_menu*.htmltemplates) and the JS modules — expect to grep for awindow.<name> =assignment when tracing menu behavior, not for exported module functions.