wuic-framework-lib 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/{wuic-framework-lib-chart-list.component-Bl424n7x.mjs → wuic-framework-lib-chart-list.component-MeAS47VV.mjs} +4 -4
- package/fesm2022/{wuic-framework-lib-code-editor.component-N6HMl2mf.mjs → wuic-framework-lib-code-editor.component-WtZC2MZC.mjs} +6 -6
- package/fesm2022/{wuic-framework-lib-designer.component-Bd2fwl6H.mjs → wuic-framework-lib-designer.component-Dt7Yz5hn.mjs} +12 -12
- package/fesm2022/{wuic-framework-lib-first-run-wizard.component-NN6cWVHL.mjs → wuic-framework-lib-first-run-wizard.component-C8nhy5yK.mjs} +349 -33
- package/fesm2022/{wuic-framework-lib-framework-docs.component-B867JAYe.mjs → wuic-framework-lib-framework-docs.component-CGsugPCx.mjs} +1426 -979
- package/fesm2022/{wuic-framework-lib-pivot-builder.component-DWukH0L-.mjs → wuic-framework-lib-pivot-builder.component-S5zqy3vj.mjs} +164 -164
- package/fesm2022/{wuic-framework-lib-prompt-dialog-fallback-host.component-DWO1Rcgv.mjs → wuic-framework-lib-prompt-dialog-fallback-host.component-VY-gtgRW.mjs} +28 -28
- package/fesm2022/{wuic-framework-lib-report-designer.component-QcrU81m6.mjs → wuic-framework-lib-report-designer.component-QRxxueU-.mjs} +2 -2
- package/fesm2022/{wuic-framework-lib-report-scaffold-dialog.component-U-zdOoRV.mjs → wuic-framework-lib-report-scaffold-dialog.component-OGdglGPw.mjs} +246 -246
- package/fesm2022/{wuic-framework-lib-report-viewer.component-B4LhA3SL.mjs → wuic-framework-lib-report-viewer.component-CuXw3e0j.mjs} +4 -4
- package/fesm2022/{wuic-framework-lib-scene3d-designer.component-DjOfzKLo.mjs → wuic-framework-lib-scene3d-designer.component-LhC0KzWO.mjs} +20 -20
- package/fesm2022/{wuic-framework-lib-scene3d-light-baker-gpu-C96T9uIW.mjs → wuic-framework-lib-scene3d-light-baker-gpu-B5ywsrSp.mjs} +2 -2
- package/fesm2022/{wuic-framework-lib-scene3d-shader-effect-DjHyCAFK.mjs → wuic-framework-lib-scene3d-shader-effect-gBEFKNvi.mjs} +18 -18
- package/fesm2022/{wuic-framework-lib-scene3d-viewer.component-Dboz1fjl.mjs → wuic-framework-lib-scene3d-viewer.component-DiDqJO-P.mjs} +5 -5
- package/fesm2022/{wuic-framework-lib-scheduler-list.component--wY21R3o.mjs → wuic-framework-lib-scheduler-list.component-5JbxIBdp.mjs} +4 -4
- package/fesm2022/{wuic-framework-lib-spreadsheet-list-sf.component-Ek30J8v-.mjs → wuic-framework-lib-spreadsheet-list-sf.component-Dxk5d9au.mjs} +47 -47
- package/fesm2022/{wuic-framework-lib-timeline-list.component-aqCH4P0G.mjs → wuic-framework-lib-timeline-list.component-B1ro-16J.mjs} +6 -6
- package/fesm2022/{wuic-framework-lib-workflow-designer.component-DMM8BeXm.mjs → wuic-framework-lib-workflow-designer.component-D32ZMxq2.mjs} +6 -6
- package/fesm2022/{wuic-framework-lib-wuic-framework-lib-dfLTqam5.mjs → wuic-framework-lib-wuic-framework-lib-81WUtxz8.mjs} +847 -823
- package/fesm2022/wuic-framework-lib.mjs +1 -1
- package/package.json +19 -2
- package/styles/_wuic-tokens.scss +82 -82
- package/types/wuic-framework-lib.d.ts +5 -0
|
@@ -17,8 +17,8 @@ import { TransformControls } from 'three/examples/jsm/controls/TransformControls
|
|
|
17
17
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
18
18
|
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
|
|
19
19
|
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
|
|
20
|
-
import { i as isWebGpuSupported, d as defaultMeshRepeaterConfig, a as defaultRepeaterAxisDir, b as defaultCarouselConfig, c as defaultChartConfig, e as defaultSurfaceConfig, f as defaultGaugeConfig, g as defaultGraphConfig, h as defaultMapConfig, j as defaultWarehouseConfig, k as defaultKanbanConfig, l as defaultCalendarConfig, m as defaultStackedConfig, W as WUIC_TRANSIENT_FLAG, n as createScene3dRenderer, o as WUIC_SHADER_EFFECT_KEY, p as makeTextSpriteMesh, q as WUIC_MESH_REPEATER_KEY, r as addRepeaterPlaceholder, s as resolveAssetFormat, t as parseShaderEffect, S as SCENE3D_SHADER_EFFECT_DEFAULT, u as SCENE3D_SHADER_EFFECT_SCHEMA, v as buildEffectNodeMaterial, w as buildEffectShaderMaterial, x as getMeshRepeaterConfig, y as WUIC_REPEATER_CHILD, C as CarouselController, z as normalizeAssetToUnit, A as buildUploadTextureResolver, B as applyRepeaterTextureTo, D as defaultRepeaterTextureApply, E as regenerateRepeater, F as getPhysicsDef, G as createPhysicsWorld, H as WUIC_PHYSICS_KEY, I as WUIC_OBJECT_TYPE_KEY, J as carouselArrowDir, K as WUIC_REPEATER_RECORD, L as runRepeaterCallback, M as closeItemPopup, N as showItemPopup, O as WUIC_BINDING_KEY, P as swapNodeEffectMaterialsForSerialization, Q as serializeScene, R as deserializeScene } from './wuic-framework-lib-scene3d-shader-effect-
|
|
21
|
-
import { W as WtoolboxService, h as DataProviderService, M as MetadataProviderService, C as ChatbotHostRegistryService } from './wuic-framework-lib-wuic-framework-lib-
|
|
20
|
+
import { i as isWebGpuSupported, d as defaultMeshRepeaterConfig, a as defaultRepeaterAxisDir, b as defaultCarouselConfig, c as defaultChartConfig, e as defaultSurfaceConfig, f as defaultGaugeConfig, g as defaultGraphConfig, h as defaultMapConfig, j as defaultWarehouseConfig, k as defaultKanbanConfig, l as defaultCalendarConfig, m as defaultStackedConfig, W as WUIC_TRANSIENT_FLAG, n as createScene3dRenderer, o as WUIC_SHADER_EFFECT_KEY, p as makeTextSpriteMesh, q as WUIC_MESH_REPEATER_KEY, r as addRepeaterPlaceholder, s as resolveAssetFormat, t as parseShaderEffect, S as SCENE3D_SHADER_EFFECT_DEFAULT, u as SCENE3D_SHADER_EFFECT_SCHEMA, v as buildEffectNodeMaterial, w as buildEffectShaderMaterial, x as getMeshRepeaterConfig, y as WUIC_REPEATER_CHILD, C as CarouselController, z as normalizeAssetToUnit, A as buildUploadTextureResolver, B as applyRepeaterTextureTo, D as defaultRepeaterTextureApply, E as regenerateRepeater, F as getPhysicsDef, G as createPhysicsWorld, H as WUIC_PHYSICS_KEY, I as WUIC_OBJECT_TYPE_KEY, J as carouselArrowDir, K as WUIC_REPEATER_RECORD, L as runRepeaterCallback, M as closeItemPopup, N as showItemPopup, O as WUIC_BINDING_KEY, P as swapNodeEffectMaterialsForSerialization, Q as serializeScene, R as deserializeScene } from './wuic-framework-lib-scene3d-shader-effect-gBEFKNvi.mjs';
|
|
21
|
+
import { W as WtoolboxService, h as DataProviderService, M as MetadataProviderService, C as ChatbotHostRegistryService } from './wuic-framework-lib-wuic-framework-lib-81WUtxz8.mjs';
|
|
22
22
|
import * as i1 from '@angular/router';
|
|
23
23
|
import * as i3 from 'primeng/api';
|
|
24
24
|
|
|
@@ -233,21 +233,21 @@ const SCENE3D_PALETTE = [
|
|
|
233
233
|
];
|
|
234
234
|
|
|
235
235
|
/** Template GLSL di partenza per lo shader custom (uniform uTime animata dal loop). */
|
|
236
|
-
const SCENE3D_SHADER_VERTEX_TEMPLATE = `varying vec2 vUv;
|
|
237
|
-
varying vec3 vNormal;
|
|
238
|
-
void main() {
|
|
239
|
-
vUv = uv;
|
|
240
|
-
vNormal = normalize(normalMatrix * normal);
|
|
241
|
-
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
236
|
+
const SCENE3D_SHADER_VERTEX_TEMPLATE = `varying vec2 vUv;
|
|
237
|
+
varying vec3 vNormal;
|
|
238
|
+
void main() {
|
|
239
|
+
vUv = uv;
|
|
240
|
+
vNormal = normalize(normalMatrix * normal);
|
|
241
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
242
242
|
}`;
|
|
243
|
-
const SCENE3D_SHADER_FRAGMENT_TEMPLATE = `uniform float uTime;
|
|
244
|
-
varying vec2 vUv;
|
|
245
|
-
varying vec3 vNormal;
|
|
246
|
-
void main() {
|
|
247
|
-
float pulse = 0.5 + 0.5 * sin(uTime * 2.0);
|
|
248
|
-
vec3 base = vec3(vUv, pulse);
|
|
249
|
-
float light = 0.4 + 0.6 * max(dot(vNormal, normalize(vec3(0.5, 0.8, 0.6))), 0.0);
|
|
250
|
-
gl_FragColor = vec4(base * light, 1.0);
|
|
243
|
+
const SCENE3D_SHADER_FRAGMENT_TEMPLATE = `uniform float uTime;
|
|
244
|
+
varying vec2 vUv;
|
|
245
|
+
varying vec3 vNormal;
|
|
246
|
+
void main() {
|
|
247
|
+
float pulse = 0.5 + 0.5 * sin(uTime * 2.0);
|
|
248
|
+
vec3 base = vec3(vUv, pulse);
|
|
249
|
+
float light = 0.4 + 0.6 * max(dot(vNormal, normalize(vec3(0.5, 0.8, 0.6))), 0.0);
|
|
250
|
+
gl_FragColor = vec4(base * light, 1.0);
|
|
251
251
|
}`;
|
|
252
252
|
/**
|
|
253
253
|
* Scene3D Designer — designer WebGL (three.js) analogo a DesignerComponent
|
|
@@ -2323,7 +2323,7 @@ class Scene3dDesignerComponent {
|
|
|
2323
2323
|
// triangle soup): ordini di grandezza piu' veloce del raycaster CPU
|
|
2324
2324
|
// sulle scene dense. Fallback CPU trasparente su qualsiasi errore.
|
|
2325
2325
|
try {
|
|
2326
|
-
const { bakeVertexLightingGpu } = await import('./wuic-framework-lib-scene3d-light-baker-gpu-
|
|
2326
|
+
const { bakeVertexLightingGpu } = await import('./wuic-framework-lib-scene3d-light-baker-gpu-B5ywsrSp.mjs');
|
|
2327
2327
|
result = await bakeVertexLightingGpu(this.renderer, this.contentGroup, lights, bakeOptions);
|
|
2328
2328
|
engine = 'gpu';
|
|
2329
2329
|
}
|
|
@@ -4791,11 +4791,11 @@ class Scene3dDesignerComponent {
|
|
|
4791
4791
|
});
|
|
4792
4792
|
}
|
|
4793
4793
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: Scene3dDesignerComponent, deps: [{ token: DataProviderService }, { token: MetadataProviderService }, { token: i1.Router }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: ChatbotHostRegistryService }], target: i0.ɵɵFactoryTarget.Component });
|
|
4794
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: Scene3dDesignerComponent, isStandalone: true, selector: "wuic-scene3d-designer", viewQueries: [{ propertyName: "viewportContainer", first: true, predicate: ["viewportContainer"], descendants: true }, { propertyName: "previewCanvasHost", first: true, predicate: ["previewCanvasHost"], descendants: true }], ngImport: i0, template: "<!-- F2/F3: viewport WebGL (canvas three.js) + palette oggetti a sinistra +\n property panel a destra + toolbar (save/load/new/delete). -->\n<div class=\"scene3d-designer-shell\">\n\n <!-- Toolbar -->\n <div class=\"scene3d-toolbar\">\n <p-button\n icon=\"pi pi-file\"\n [label]=\"t('scene3d.toolbar.new', 'Nuova')\"\n severity=\"secondary\"\n size=\"small\"\n (onClick)=\"newScene()\">\n </p-button>\n\n <p-button\n icon=\"pi pi-save\"\n [label]=\"t('scene3d.toolbar.save', 'Salva')\"\n size=\"small\"\n [loading]=\"savingScene\"\n (onClick)=\"saveScene()\">\n </p-button>\n\n <p-select\n [options]=\"savedSceneOptions\"\n [(ngModel)]=\"selectedSavedSceneKey\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"t('scene3d.toolbar.load_placeholder', 'Carica scena...')\"\n appendTo=\"body\"\n styleClass=\"scene3d-load-select\"\n (onChange)=\"onSavedSceneSelectionChange()\">\n <!-- Anteprima scena (thumbnail dal worker server-side GPU); placeholder cubo se assente. -->\n <ng-template pTemplate=\"item\" let-opt>\n <div class=\"scene3d-scene-option\">\n @if (opt.thumbnail) {\n <img class=\"scene3d-scene-option-thumb\" [src]=\"opt.thumbnail\" [alt]=\"opt.label\" loading=\"lazy\" />\n } @else {\n <span class=\"scene3d-scene-option-thumb scene3d-scene-option-thumb-empty\">\n <i class=\"pi pi-box\"></i>\n </span>\n }\n <span class=\"scene3d-scene-option-label\">{{ opt.label }}</span>\n </div>\n </ng-template>\n </p-select>\n\n <p-button\n icon=\"pi pi-folder-open\"\n [label]=\"t('scene3d.toolbar.load', 'Carica')\"\n severity=\"secondary\"\n size=\"small\"\n [loading]=\"loadingScene\"\n [disabled]=\"!selectedSavedSceneKey\"\n (onClick)=\"loadScene()\">\n </p-button>\n\n <p-button\n icon=\"pi pi-trash\"\n [label]=\"t('scene3d.toolbar.delete_selected', 'Elimina selezionato')\"\n severity=\"danger\"\n size=\"small\"\n [disabled]=\"!selectedObjectUuid\"\n (onClick)=\"deleteSelectedObject()\">\n </p-button>\n\n <!-- Toggle luci default del viewport: OFF = si vede solo l'effetto delle luci droppate -->\n <p-button\n icon=\"pi pi-sun\"\n [label]=\"t('scene3d.toolbar.default_lights', 'Luci default')\"\n [severity]=\"defaultLightsEnabled ? 'primary' : 'secondary'\"\n [outlined]=\"!defaultLightsEnabled\"\n size=\"small\"\n [title]=\"t('scene3d.toolbar.default_lights_hint', 'Attiva/disattiva le luci di default del viewport')\"\n (onClick)=\"toggleDefaultLights()\">\n </p-button>\n\n <!-- Toggle shadow map del renderer (default ON) -->\n <p-button\n icon=\"pi pi-moon\"\n [label]=\"t('scene3d.toolbar.shadows', 'Ombre')\"\n [severity]=\"shadowsEnabled ? 'primary' : 'secondary'\"\n [outlined]=\"!shadowsEnabled\"\n size=\"small\"\n [title]=\"t('scene3d.toolbar.shadows_hint', 'Attiva/disattiva le ombre proiettate')\"\n (onClick)=\"toggleShadows()\">\n </p-button>\n\n <!-- Renderer WebGPU (opt-in): fallback WebGL se non supportato; shader GLSL e PT restano WebGL-only -->\n <p-button\n icon=\"pi pi-bolt\"\n label=\"WebGPU\"\n [severity]=\"rendererMode === 'webgpu' ? 'primary' : 'secondary'\"\n [outlined]=\"rendererMode !== 'webgpu'\"\n size=\"small\"\n [loading]=\"rendererSwitching\"\n [disabled]=\"!webGpuSupported\"\n [title]=\"webGpuSupported\n ? t('scene3d.renderer.toggle_hint', 'Renderer WebGPU (node-materials). Shader GLSL custom e path tracing restano disponibili solo in WebGL')\n : t('scene3d.renderer.webgpu_unavailable', 'WebGPU non disponibile in questo browser: resto su WebGL')\"\n (onClick)=\"toggleWebGpuRenderer()\">\n </p-button>\n\n <!-- Toggle guide di scena: proxy luci/camera + linee helper/frustum -->\n <p-button\n icon=\"pi pi-eye\"\n [label]=\"t('scene3d.toolbar.helpers', 'Guide')\"\n [severity]=\"helpersVisible ? 'primary' : 'secondary'\"\n [outlined]=\"!helpersVisible\"\n size=\"small\"\n [title]=\"t('scene3d.toolbar.helpers_hint', 'Mostra/nasconde i placeholder delle luci e le linee helper/frustum')\"\n (onClick)=\"toggleHelpers()\">\n </p-button>\n\n <!-- Auto-rigenera (toggle globale): se ON, ogni cambio di proprieta' rigenera\n automaticamente il repeater selezionato (con debounce), senza cliccare\n \"Rigenera\". Per gli altri oggetti palette i cambi si applicano gia' live. -->\n <p-button\n icon=\"pi pi-sync\"\n [label]=\"t('scene3d.toolbar.auto_regen', 'Auto-rigenera')\"\n [severity]=\"autoRegen ? 'primary' : 'secondary'\"\n [outlined]=\"!autoRegen\"\n size=\"small\"\n [title]=\"t('scene3d.toolbar.auto_regen_hint', 'Se attivo, ogni modifica di proprieta rigenera automaticamente (repeater), senza premere Rigenera')\"\n (onClick)=\"autoRegen = !autoRegen\">\n </p-button>\n\n <!-- Render qualita' (path tracing progressivo, lazy): GI + ombre colorate dai vetri -->\n <p-button\n icon=\"pi pi-sparkles\"\n [label]=\"pathTracingEnabled ? t('scene3d.toolbar.pt_exit', 'Realtime') : t('scene3d.toolbar.pt', 'Render PT')\"\n [severity]=\"pathTracingEnabled ? 'primary' : 'secondary'\"\n [outlined]=\"!pathTracingEnabled\"\n size=\"small\"\n [loading]=\"ptBuilding\"\n [title]=\"t('scene3d.toolbar.pt_hint', 'Render path-traced progressivo (GI, ombre colorate dai vetri). Modalita\\' preview: editing sospeso')\"\n (onClick)=\"togglePathTracing()\">\n </p-button>\n\n <!-- Fisica: Play/Stop simulazione + menu (Applica pose / Impostazioni scena) -->\n <p-splitButton\n [icon]=\"physicsSimulating ? 'pi pi-stop' : 'pi pi-play'\"\n [label]=\"physicsSimulating ? t('scene3d.toolbar.physics_stop', 'Stop fisica') : t('scene3d.toolbar.physics_play', 'Fisica')\"\n [severity]=\"physicsSimulating ? 'primary' : 'secondary'\"\n [outlined]=\"!physicsSimulating\"\n size=\"small\"\n [model]=\"physicsMenuItems\"\n appendTo=\"body\"\n [attr.title]=\"t('scene3d.toolbar.physics_hint', 'Simula i corpi rigidi configurati (Stop = ripristina le pose)')\"\n (onClick)=\"togglePhysicsSimulation()\">\n </p-splitButton>\n\n <!-- Bake luci per-vertice: cuoce l'illuminazione nei vertex colors (v1 lightmap) -->\n <p-button\n icon=\"pi pi-bolt\"\n [label]=\"t('scene3d.toolbar.bake', 'Bake luci')\"\n severity=\"secondary\"\n size=\"small\"\n [title]=\"t('scene3d.toolbar.bake_hint', 'Cuoce l\\'illuminazione corrente nei vertex colors (materiale statico unlit)')\"\n (onClick)=\"bakeLightsAction()\">\n </p-button>\n\n <!-- Crea una camera di scena che replica il punto di vista attuale del designer -->\n <p-button\n icon=\"pi pi-camera\"\n [label]=\"t('scene3d.toolbar.camera_from_view', 'Camera da vista attuale')\"\n severity=\"secondary\"\n size=\"small\"\n [title]=\"t('scene3d.toolbar.camera_from_view_hint', 'Crea una camera nella scena con posizione e inquadratura identiche alla vista corrente')\"\n (onClick)=\"createCameraFromView()\">\n </p-button>\n\n <!-- Vista camera: render dalla camera utente (selezionata o prima in scena).\n Con piu' camere in scena diventa uno split-button col picker. -->\n @if (sceneCameraCount > 1) {\n <p-splitButton\n icon=\"pi pi-video\"\n [label]=\"viewingThroughCamera ? t('scene3d.toolbar.designer_view', 'Vista designer') : t('scene3d.toolbar.camera_view', 'Vista camera')\"\n [severity]=\"viewingThroughCamera ? 'primary' : 'secondary'\"\n [outlined]=\"!viewingThroughCamera\"\n size=\"small\"\n [model]=\"cameraViewMenuItems\"\n appendTo=\"body\"\n [attr.title]=\"t('scene3d.toolbar.camera_view_hint', 'Alterna tra la vista di editing e la vista dalla camera della scena')\"\n (onClick)=\"toggleCameraView()\">\n </p-splitButton>\n } @else {\n <p-button\n icon=\"pi pi-video\"\n [label]=\"viewingThroughCamera ? t('scene3d.toolbar.designer_view', 'Vista designer') : t('scene3d.toolbar.camera_view', 'Vista camera')\"\n [severity]=\"viewingThroughCamera ? 'primary' : 'secondary'\"\n [outlined]=\"!viewingThroughCamera\"\n size=\"small\"\n [title]=\"t('scene3d.toolbar.camera_view_hint', 'Alterna tra la vista di editing e la vista dalla camera della scena')\"\n (onClick)=\"toggleCameraView()\">\n </p-button>\n }\n </div>\n\n <div class=\"scene3d-body\">\n\n <!-- Palette (sinistra) -->\n <div class=\"scene3d-palette\">\n <div class=\"scene3d-panel-title\">{{ t('scene3d.palette.title', 'Oggetti') }}</div>\n @for (item of paletteItems; track item.type) {\n <button\n type=\"button\"\n class=\"scene3d-palette-item\"\n [disabled]=\"!item.fromPalette\"\n [title]=\"item.label\"\n (click)=\"addFromPalette(item.type)\">\n <i [class]=\"item.icon\"></i>\n <span class=\"scene3d-palette-label\">{{ item.label }}</span>\n </button>\n }\n </div>\n\n <!-- Viewport (centro) -->\n <div #viewportContainer class=\"scene3d-viewport\">\n <!-- HUD path tracing: build, sample progressivi, scala risoluzione -->\n @if (ptBuilding || pathTracingEnabled) {\n <div class=\"scene3d-pt-hud\">\n @if (ptBuilding) {\n <span>{{ t('scene3d.pt.building', 'Costruzione BVH...') }}</span>\n } @else {\n <span>{{ ptSamples }} {{ t('scene3d.pt.samples_suffix', 'sample') }}</span>\n <label class=\"scene3d-pt-scale\">\n {{ t('scene3d.pt.scale', 'Risoluzione') }}\n <input type=\"range\" min=\"0.25\" max=\"1\" step=\"0.25\"\n [(ngModel)]=\"ptResolutionScale\" (ngModelChange)=\"onPtResolutionScaleChange()\" />\n </label>\n <button type=\"button\" class=\"scene3d-pt-exit\" (click)=\"togglePathTracing()\">\n {{ t('scene3d.pt.exit', 'Torna al realtime') }}\n </button>\n }\n </div>\n }\n\n <!-- Popup anteprima camera: real-time, trascinabile dall'header -->\n @if (cameraPreviewEnabled) {\n <div class=\"scene3d-cam-preview\" [style.left.px]=\"previewPos.x\" [style.top.px]=\"previewPos.y\">\n <div class=\"scene3d-cam-preview-header\" (mousedown)=\"startPreviewDrag($event)\">\n <span class=\"scene3d-cam-preview-title\">{{ t('scene3d.preview.title', 'Anteprima camera') }}</span>\n <button type=\"button\" class=\"scene3d-cam-preview-close\" (click)=\"toggleCameraPreview()\" (mousedown)=\"$event.stopPropagation()\">\u00D7</button>\n </div>\n <div #previewCanvasHost class=\"scene3d-cam-preview-body\"></div>\n </div>\n }\n </div>\n\n <!-- Property panel (destra) -->\n <div class=\"scene3d-properties\">\n <div class=\"scene3d-panel-title\">{{ t('scene3d.props.title', 'Proprieta') }}</div>\n\n @if (selectedObjectUuid) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-prop-name\">{{ t('scene3d.props.name', 'Nome') }}</label>\n <input id=\"scene3d-prop-name\" type=\"text\" [(ngModel)]=\"selectedObjectName\"\n (ngModelChange)=\"renameSelectedObject($event)\"\n [placeholder]=\"t('scene3d.props.name_placeholder', 'Nome oggetto')\" />\n </div>\n\n <!-- ID della camera: e' il camera_id per l'URL del viewer\n (#/scene3d_viewer/<scene_key>/<camera_id>). Persiste nel salvataggio. -->\n @if (selectedIsCamera) {\n <div class=\"scene3d-prop-group\">\n <span class=\"scene3d-prop-label\">{{ t('scene3d.props.camera_id', 'ID camera (viewer)') }}</span>\n <div class=\"scene3d-prop-readonly\">{{ selectedObjectUuid }}</div>\n </div>\n\n <!-- Anteprima real-time in popup trascinabile dal punto di vista della camera -->\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input\n type=\"checkbox\"\n [ngModel]=\"cameraPreviewEnabled && previewCameraUuid === selectedObjectUuid\"\n (ngModelChange)=\"toggleCameraPreview()\" />\n {{ t('scene3d.props.camera_preview', 'Anteprima camera') }}\n </label>\n </div>\n }\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-pos-x\">{{ t('scene3d.props.position', 'Posizione') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-pos-x\" type=\"number\" step=\"0.1\" [(ngModel)]=\"propPosition.x\" (ngModelChange)=\"onTransformInputChange()\" />\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propPosition.y\" (ngModelChange)=\"onTransformInputChange()\" />\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propPosition.z\" (ngModelChange)=\"onTransformInputChange()\" />\n </div>\n </div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-rot-x\">{{ t('scene3d.props.rotation', 'Rotazione (deg)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-rot-x\" type=\"number\" step=\"1\" [(ngModel)]=\"propRotation.x\" (ngModelChange)=\"onTransformInputChange()\" />\n <input type=\"number\" step=\"1\" [(ngModel)]=\"propRotation.y\" (ngModelChange)=\"onTransformInputChange()\" />\n <input type=\"number\" step=\"1\" [(ngModel)]=\"propRotation.z\" (ngModelChange)=\"onTransformInputChange()\" />\n </div>\n </div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-scale-x\">{{ t('scene3d.props.scale', 'Scala') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-scale-x\" type=\"number\" step=\"0.1\" [(ngModel)]=\"propScale.x\" (ngModelChange)=\"onTransformInputChange()\" />\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propScale.y\" (ngModelChange)=\"onTransformInputChange()\" />\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propScale.z\" (ngModelChange)=\"onTransformInputChange()\" />\n </div>\n </div>\n\n @if (selectedHasMaterialColor) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-prop-color\">{{ t('scene3d.props.color', 'Colore') }}</label>\n <input id=\"scene3d-prop-color\" type=\"color\" [(ngModel)]=\"propColor\" (ngModelChange)=\"onColorInputChange()\" />\n </div>\n }\n\n <!-- castShadow per-oggetto (default ON alla creazione, disattivabile) -->\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"propCastShadow\" (ngModelChange)=\"onCastShadowInputChange()\" />\n {{ t('scene3d.props.cast_shadow', 'Proietta ombra') }}\n </label>\n </div>\n\n <!-- Editor materiale PBR (MeshStandardMaterial) -->\n @if (selectedHasStandardMaterial) {\n <div class=\"scene3d-material-section\">\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.material.title', 'Materiale') }}</div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-metalness\">{{ t('scene3d.material.metalness', 'Metalness') }} ({{ propMaterial.metalness }})</label>\n <input id=\"scene3d-mat-metalness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.metalness\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-roughness\">{{ t('scene3d.material.roughness', 'Roughness') }} ({{ propMaterial.roughness }})</label>\n <input id=\"scene3d-mat-roughness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.roughness\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-opacity\">{{ t('scene3d.material.opacity', 'Opacita\\'') }} ({{ propMaterial.opacity }})</label>\n <input id=\"scene3d-mat-opacity\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.opacity\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-emissive\">{{ t('scene3d.material.emissive', 'Emissivo') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-mat-emissive\" type=\"color\" [(ngModel)]=\"propMaterial.emissive\" (ngModelChange)=\"onMaterialInputChange()\" />\n <input type=\"number\" step=\"0.1\" min=\"0\" [(ngModel)]=\"propMaterial.emissiveIntensity\" (ngModelChange)=\"onMaterialInputChange()\"\n [title]=\"t('scene3d.material.emissive_intensity', 'Intensita\\' emissiva')\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-side\">{{ t('scene3d.material.side', 'Facce') }}</label>\n <select id=\"scene3d-mat-side\" class=\"scene3d-material-select\" [(ngModel)]=\"propMaterial.side\" (ngModelChange)=\"onMaterialInputChange()\">\n <option value=\"front\">{{ t('scene3d.material.side_front', 'Fronte') }}</option>\n <option value=\"back\">{{ t('scene3d.material.side_back', 'Retro') }}</option>\n <option value=\"double\">{{ t('scene3d.material.side_double', 'Doppia') }}</option>\n </select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"propMaterial.wireframe\" (ngModelChange)=\"onMaterialInputChange()\" />\n {{ t('scene3d.material.wireframe', 'Wireframe') }}\n </label>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"propMaterial.flatShading\" (ngModelChange)=\"onMaterialInputChange()\" />\n {{ t('scene3d.material.flat', 'Flat shading') }}\n </label>\n </div>\n\n <!-- Vetro fisico (transmission): rifrazione reale di cio' che sta dietro.\n In Render PT l'assorbimento (colore+distanza) tinge anche la LUCE\n che attraversa il vetro sugli altri oggetti (Beer-Lambert). -->\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.material.glass', 'Vetro (transmission)') }}</div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-transmission\">{{ t('scene3d.material.transmission', 'Trasmissione') }} ({{ propMaterial.transmission }})</label>\n <input id=\"scene3d-mat-transmission\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.transmission\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n @if (propMaterial.transmission > 0) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-ior\">{{ t('scene3d.material.ior', 'IOR') }} ({{ propMaterial.ior }})</label>\n <input id=\"scene3d-mat-ior\" type=\"range\" min=\"1\" max=\"2.33\" step=\"0.01\" [(ngModel)]=\"propMaterial.ior\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-thickness\">{{ t('scene3d.material.thickness', 'Spessore') }} ({{ propMaterial.thickness }})</label>\n <input id=\"scene3d-mat-thickness\" type=\"range\" min=\"0\" max=\"5\" step=\"0.1\" [(ngModel)]=\"propMaterial.thickness\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n <!-- Assorbimento volumetrico: colora la luce trasmessa (visibile in PT). -->\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-attenuation-color\">{{ t('scene3d.material.attenuation_color', 'Colore assorbimento') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-mat-attenuation-color\" type=\"color\" [(ngModel)]=\"propMaterial.attenuationColor\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-attenuation-distance\">{{ t('scene3d.material.attenuation_distance', 'Distanza assorbimento') }} ({{ propMaterial.attenuationDistance }})</label>\n <input id=\"scene3d-mat-attenuation-distance\" type=\"range\" min=\"0.05\" max=\"10\" step=\"0.05\" [(ngModel)]=\"propMaterial.attenuationDistance\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.material.glass_hint', 'Con transmission tieni Opacita\\' = 1 (la trasparenza la gestisce il vetro). Il colore assorbimento tinge la luce filtrata: visibile in Render PT.') }}\n </div>\n }\n </div>\n }\n\n <!-- Shader GLSL custom -->\n @if (selectedIsMesh) {\n <div class=\"scene3d-material-section\">\n <div class=\"scene3d-panel-subtitle scene3d-shader-toggle\" (click)=\"shaderSectionOpen = !shaderSectionOpen\">\n {{ t('scene3d.shader.title', 'Shader custom (GLSL)') }} {{ shaderSectionOpen ? '\u25BE' : '\u25B8' }}\n </div>\n @if (shaderSectionOpen) {\n <!-- Effetto JSON (dual-backend): editor Monaco con schema, compilato\n in GLSL su WebGL e in node-material su WebGPU -->\n <div class=\"scene3d-anim-transport\">\n <p-button\n icon=\"pi pi-sliders-h\"\n [label]=\"t('scene3d.shader.effect_edit', 'Effetto JSON')\"\n size=\"small\"\n [severity]=\"selectedHasShaderEffect ? 'primary' : 'secondary'\"\n [title]=\"t('scene3d.shader.effect_tooltip', 'JSON con schema (completion/validazione in editor). Compilato in GLSL su WebGL e in node-material su WebGPU.')\"\n (onClick)=\"editShaderEffect()\">\n </p-button>\n @if (selectedHasShaderEffect) {\n <p-button\n icon=\"pi pi-undo\"\n [label]=\"t('scene3d.shader.effect_remove', 'Rimuovi effetto')\"\n severity=\"secondary\"\n size=\"small\"\n (onClick)=\"removeShaderEffect()\">\n </p-button>\n }\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-shader-vertex\">{{ t('scene3d.shader.vertex', 'Vertex shader') }}</label>\n <textarea id=\"scene3d-shader-vertex\" class=\"scene3d-shader-src\" rows=\"6\" spellcheck=\"false\" [(ngModel)]=\"shaderVertexSrc\"></textarea>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-shader-fragment\">{{ t('scene3d.shader.fragment', 'Fragment shader') }}</label>\n <textarea id=\"scene3d-shader-fragment\" class=\"scene3d-shader-src\" rows=\"8\" spellcheck=\"false\" [(ngModel)]=\"shaderFragmentSrc\"></textarea>\n </div>\n <div class=\"scene3d-anim-transport\">\n <p-button\n icon=\"pi pi-check\"\n [label]=\"t('scene3d.shader.apply', 'Applica shader')\"\n size=\"small\"\n (onClick)=\"applyCustomShader()\">\n </p-button>\n @if (selectedHasCustomShader) {\n <p-button\n icon=\"pi pi-undo\"\n [label]=\"t('scene3d.shader.restore', 'Ripristina')\"\n severity=\"secondary\"\n size=\"small\"\n (onClick)=\"restoreCustomShader()\">\n </p-button>\n }\n </div>\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.shader.hint', 'Uniform disponibile: uTime (secondi, animata). Errori GLSL in console.') }}\n </div>\n }\n </div>\n }\n\n <!-- F5: Binding rotta scaffoldata + field-map + open-record.\n Nascosto per il Mesh Repeater: il repeater binda per-record (config sua). -->\n @if (!selectedIsRepeater) {\n <div class=\"scene3d-binding-section\">\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.binding.title', 'Binding rotta') }}</div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-route\">{{ t('scene3d.binding.route', 'Rotta') }}</label>\n <p-select\n inputId=\"scene3d-binding-route\"\n [options]=\"routeOptions\"\n [(ngModel)]=\"bindingRoute\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"t('scene3d.binding.route_placeholder', 'Seleziona rotta...')\"\n appendTo=\"body\"\n styleClass=\"scene3d-binding-select\"\n (onChange)=\"onBindingRouteChange()\">\n </p-select>\n </div>\n\n @if (selectedHasBinding) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-record-id\">{{ t('scene3d.binding.record_id', 'Record ID (opzionale)') }}</label>\n <input\n id=\"scene3d-binding-record-id\"\n type=\"text\"\n [(ngModel)]=\"bindingRecordId\"\n (ngModelChange)=\"onBindingDetailChange()\"\n [placeholder]=\"t('scene3d.binding.record_id_placeholder', 'Vuoto = binding di rotta')\" />\n </div>\n\n <div class=\"scene3d-field-map\">\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.binding.field_map', 'Mappa campi') }}</div>\n\n @if (bindingColumnsLoading) {\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.binding.columns_loading', 'Caricamento colonne...') }}</div>\n }\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-map-label\">{{ t('scene3d.binding.map_label', 'Etichetta') }}</label>\n <p-select\n inputId=\"scene3d-binding-map-label\"\n [options]=\"bindingColumnOptions\"\n [(ngModel)]=\"bindingFieldMap.label\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"t('scene3d.binding.column_placeholder', 'Colonna...')\"\n appendTo=\"body\"\n styleClass=\"scene3d-binding-select\"\n (onChange)=\"onBindingDetailChange()\">\n </p-select>\n </div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-map-color\">{{ t('scene3d.binding.map_color', 'Colore') }}</label>\n <p-select\n inputId=\"scene3d-binding-map-color\"\n [options]=\"bindingColumnOptions\"\n [(ngModel)]=\"bindingFieldMap.color\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"t('scene3d.binding.column_placeholder', 'Colonna...')\"\n appendTo=\"body\"\n styleClass=\"scene3d-binding-select\"\n (onChange)=\"onBindingDetailChange()\">\n </p-select>\n </div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-map-visible\">{{ t('scene3d.binding.map_visible', 'Visibile') }}</label>\n <p-select\n inputId=\"scene3d-binding-map-visible\"\n [options]=\"bindingColumnOptions\"\n [(ngModel)]=\"bindingFieldMap.visible\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"t('scene3d.binding.column_placeholder', 'Colonna...')\"\n appendTo=\"body\"\n styleClass=\"scene3d-binding-select\"\n (onChange)=\"onBindingDetailChange()\">\n </p-select>\n </div>\n </div>\n\n <p-button\n icon=\"pi pi-external-link\"\n [label]=\"t('scene3d.binding.open_record', 'Apri record')\"\n severity=\"secondary\"\n size=\"small\"\n styleClass=\"scene3d-open-record-btn\"\n (onClick)=\"openBoundRecord()\">\n </p-button>\n\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.binding.open_hint', 'Doppio click su un oggetto bindato per aprire la CRUD della rotta') }}\n </div>\n }\n </div>\n }\n\n <!-- Iter2 F2: animazioni / timeline (solo se il selezionato ha clip) -->\n @if (selectedHasAnimations) {\n <div class=\"scene3d-anim-section\">\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.anim.title', 'Animazioni') }}</div>\n\n @if (animClipOptions.length > 1) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-anim-clip\">{{ t('scene3d.anim.clip', 'Clip') }}</label>\n <p-select\n inputId=\"scene3d-anim-clip\"\n [options]=\"animClipOptions\"\n [(ngModel)]=\"selectedAnimClipIndex\"\n optionLabel=\"label\"\n optionValue=\"value\"\n appendTo=\"body\"\n styleClass=\"scene3d-binding-select\"\n (onChange)=\"onAnimClipChange()\">\n </p-select>\n </div>\n }\n\n <div class=\"scene3d-anim-transport\">\n <p-button\n icon=\"pi pi-play\"\n [title]=\"t('scene3d.anim.play', 'Play')\"\n severity=\"secondary\"\n size=\"small\"\n [disabled]=\"animPlaying\"\n (onClick)=\"animPlay()\">\n </p-button>\n <p-button\n icon=\"pi pi-pause\"\n [title]=\"t('scene3d.anim.pause', 'Pausa')\"\n severity=\"secondary\"\n size=\"small\"\n [disabled]=\"!animPlaying\"\n (onClick)=\"animPause()\">\n </p-button>\n <p-button\n icon=\"pi pi-stop\"\n [title]=\"t('scene3d.anim.stop', 'Stop')\"\n severity=\"secondary\"\n size=\"small\"\n (onClick)=\"animStop()\">\n </p-button>\n <p-button\n icon=\"pi pi-trash\"\n [title]=\"t('scene3d.anim.delete_clip', 'Elimina clip')\"\n severity=\"danger\"\n size=\"small\"\n (onClick)=\"deleteSelectedAnimClip()\">\n </p-button>\n </div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-anim-time\">{{ t('scene3d.anim.time', 'Tempo (s)') }}</label>\n <input\n id=\"scene3d-anim-time\"\n type=\"range\"\n class=\"scene3d-anim-scrubber\"\n min=\"0\"\n [max]=\"animDuration\"\n step=\"0.01\"\n [(ngModel)]=\"animScrubTime\"\n (ngModelChange)=\"onAnimScrub()\" />\n <div class=\"scene3d-anim-time\">{{ animScrubTime }} / {{ animDuration }}</div>\n </div>\n </div>\n }\n\n <!-- Mesh Repeater (DATAREPEATER 3D): una mesh per record, propriet\u00E0 bindate -->\n @if (selectedIsRepeater) {\n <div class=\"scene3d-material-section\">\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.repeater.title', 'Mesh Repeater') }}</div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-route\">{{ t('scene3d.repeater.route', 'Rotta') }}</label>\n <p-select\n inputId=\"scene3d-repeater-route\"\n [options]=\"routeOptions\" [(ngModel)]=\"repeaterCfg.route\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.route_ph', 'Seleziona rotta...')\"\n appendTo=\"body\" styleClass=\"scene3d-binding-select\"\n (onChange)=\"onRepeaterRouteChange()\">\n </p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-max\">{{ t('scene3d.repeater.max', 'Max record') }}</label>\n <input id=\"scene3d-repeater-max\" type=\"number\" min=\"1\" step=\"10\" [(ngModel)]=\"repeaterCfg.maxRecords\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-mesh-source\">{{ t('scene3d.repeater.mesh_source', 'Sorgente mesh') }}</label>\n <select id=\"scene3d-repeater-mesh-source\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.meshSource.mode\" (ngModelChange)=\"onRepeaterConfigChange()\">\n <option value=\"fixed\">{{ t('scene3d.repeater.fixed', 'Fissa') }}</option>\n <option value=\"column\">{{ t('scene3d.repeater.from_column', 'Da colonna') }}</option>\n <option value=\"asset\">{{ t('scene3d.repeater.from_asset', 'Asset (colonna)') }}</option>\n </select>\n </div>\n @if (repeaterCfg.meshSource.mode === 'fixed') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-mesh-type\">{{ t('scene3d.repeater.mesh_type', 'Tipo mesh') }}</label>\n <select id=\"scene3d-repeater-mesh-type\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.meshSource.type\" (ngModelChange)=\"onRepeaterConfigChange()\">\n @for (o of repeaterMeshTypeOptions; track o.value) { <option [value]=\"o.value\">{{ o.label }}</option> }\n </select>\n </div>\n } @else {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-mesh-column\">{{ repeaterCfg.meshSource.mode === 'asset'\n ? t('scene3d.repeater.asset_column', 'Colonna asset (upload GLB/OBJ/STL)')\n : t('scene3d.repeater.mesh_column', 'Colonna tipo') }}</label>\n <p-select inputId=\"scene3d-repeater-mesh-column\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"repeaterCfg.meshSource.column\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n appendTo=\"body\" styleClass=\"scene3d-binding-select\" (onChange)=\"onRepeaterConfigChange()\"></p-select>\n </div>\n }\n\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.repeater.field_map', 'Mappa campi \u2192 colonne') }}</div>\n @for (f of repeaterFields; track f.key) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" [attr.for]=\"'scene3d-rep-field-' + f.key\">{{ f.label }}</label>\n <p-select [inputId]=\"'scene3d-rep-field-' + f.key\" [options]=\"bindingColumnOptions\"\n [ngModel]=\"getFieldMapCol(f.key)\" (ngModelChange)=\"setFieldMapCol(f.key, $event)\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\"\n appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n }\n\n <!-- Opzioni texture (visibili solo con la colonna `texture` mappata):\n testo\u2192texture testuale, upload/img\u2192immagine, + modo di applicazione. -->\n @if (getFieldMapCol('texture')) {\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.repeater.texture_opts', 'Texture (colonna)') }}</div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-source\">{{ t('scene3d.repeater.texture_source', 'Sorgente') }}</label>\n <select id=\"scene3d-texture-source\" class=\"scene3d-material-select\" [(ngModel)]=\"textureApply.sourceKind\" (ngModelChange)=\"onTextureApplyChange()\">\n <option value=\"auto\">{{ t('scene3d.repeater.texture_auto', 'Auto (dal tipo colonna)') }}</option>\n <option value=\"text\">{{ t('scene3d.repeater.texture_text', 'Testo \u2192 texture') }}</option>\n <option value=\"image\">{{ t('scene3d.repeater.texture_image', 'Immagine / upload') }}</option>\n </select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-apply\">{{ t('scene3d.repeater.texture_apply', 'Applicazione') }}</label>\n <select id=\"scene3d-texture-apply\" class=\"scene3d-material-select\" [(ngModel)]=\"textureApply.wrap\" (ngModelChange)=\"onTextureApplyChange()\">\n <option value=\"stretch\">{{ t('scene3d.repeater.texture_stretch', 'Stretch (adatta)') }}</option>\n <option value=\"repeat\">{{ t('scene3d.repeater.texture_repeat', 'Ripeti') }}</option>\n <option value=\"mirror\">{{ t('scene3d.repeater.texture_mirror', 'Ripeti a specchio') }}</option>\n </select>\n </div>\n @if (textureApply.wrap !== 'stretch') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-repeat-x\">{{ t('scene3d.repeater.texture_repeat_xy', 'Ripetizioni X / Y') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-texture-repeat-x\" type=\"number\" step=\"0.5\" min=\"0.1\" [(ngModel)]=\"textureApply.repeatX\" (ngModelChange)=\"onTextureApplyChange()\" />\n <input type=\"number\" step=\"0.5\" min=\"0.1\" [(ngModel)]=\"textureApply.repeatY\" (ngModelChange)=\"onTextureApplyChange()\" />\n </div>\n </div>\n }\n @if (textureApply.sourceKind !== 'image') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-text-bg\">{{ t('scene3d.repeater.texture_text_colors', 'Colori testo (sfondo / testo)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-texture-text-bg\" type=\"color\" [(ngModel)]=\"textureApply.textBg\" (ngModelChange)=\"onTextureApplyChange()\" />\n <input type=\"color\" [(ngModel)]=\"textureApply.textFg\" (ngModelChange)=\"onTextureApplyChange()\" />\n </div>\n </div>\n }\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.repeater.texture_hint', 'Dopo aver cambiato le opzioni premi Rigenera per riapplicare le texture.') }}\n </div>\n }\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-layout\">{{ t('scene3d.repeater.layout', 'Layout (senza colonne posizione)') }}</label>\n <select id=\"scene3d-repeater-layout\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.layout.mode\" (ngModelChange)=\"onRepeaterLayoutModeChange()\">\n <optgroup [label]=\"t('scene3d.repeater.layout_simple', 'Semplici')\">\n <option value=\"grid\">Grid</option>\n <option value=\"cube\">{{ t('scene3d.repeater.cube', 'Cubo') }}</option>\n <option value=\"line\">{{ t('scene3d.repeater.line', 'Linea') }}</option>\n <option value=\"circle\">{{ t('scene3d.repeater.circle', 'Cerchio') }}</option>\n </optgroup>\n <optgroup [label]=\"t('scene3d.repeater.layout_archetypes', 'Archetipi')\">\n <option value=\"carousel\">{{ t('scene3d.repeater.carousel', 'Carousel 3D') }}</option>\n <option value=\"chart\">{{ t('scene3d.repeater.chart', 'Chart 3D') }}</option>\n <option value=\"surface\">{{ t('scene3d.repeater.surface', 'Surface / Heatmap') }}</option>\n <option value=\"gauge\">{{ t('scene3d.repeater.gauge', 'Gauge / KPI') }}</option>\n <option value=\"nodegraph\">{{ t('scene3d.repeater.nodegraph', 'Node graph') }}</option>\n <option value=\"tree\">{{ t('scene3d.repeater.tree', 'Albero (tree)') }}</option>\n <option value=\"map\">{{ t('scene3d.repeater.map', 'Mappa (pin)') }}</option>\n <option value=\"warehouse\">{{ t('scene3d.repeater.warehouse', 'Magazzino (scaffali)') }}</option>\n <option value=\"kanban\">{{ t('scene3d.repeater.kanban', 'Kanban 3D') }}</option>\n <option value=\"calendar\">{{ t('scene3d.repeater.calendar', 'Calendario') }}</option>\n <option value=\"stacked\">{{ t('scene3d.repeater.stacked', 'Barre impilate') }}</option>\n </optgroup>\n </select>\n </div>\n\n <!-- Archetipo Carousel 3D: sezioni collassabili Carousel / Disco / Frecce. -->\n @if (repeaterCfg.layout.mode === 'carousel') {\n <!-- Sezione Carousel (generale) -->\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('carousel')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('carousel')\" [class.pi-chevron-down]=\"!isSectionCollapsed('carousel')\"></i>\n {{ t('scene3d.repeater.carousel', 'Carousel 3D') }}\n </div>\n @if (!isSectionCollapsed('carousel')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-visible\">{{ t('scene3d.repeater.carousel_visible', 'Item visibili') }} ({{ carouselCfg.visible }})</label>\n <input id=\"scene3d-carousel-visible\" type=\"range\" min=\"1\" max=\"9\" step=\"1\" [(ngModel)]=\"carouselCfg.visible\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-radius\">{{ t('scene3d.repeater.carousel_radius', 'Curvatura (raggio)') }} ({{ carouselCfg.radius }})</label>\n <input id=\"scene3d-carousel-radius\" type=\"range\" min=\"2\" max=\"30\" step=\"0.5\" [(ngModel)]=\"carouselCfg.radius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-slot-angle\">{{ t('scene3d.repeater.carousel_slot_angle', 'Angolo tra slot') }} ({{ carouselCfg.slotAngleDeg }}\u00B0)</label>\n <input id=\"scene3d-carousel-slot-angle\" type=\"range\" min=\"8\" max=\"60\" step=\"1\" [(ngModel)]=\"carouselCfg.slotAngleDeg\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-focus-scale\">{{ t('scene3d.repeater.carousel_scales', 'Scala centro / lati') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-carousel-focus-scale\" type=\"number\" step=\"0.05\" min=\"0.1\" [(ngModel)]=\"carouselCfg.focusScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n <input type=\"number\" step=\"0.05\" min=\"0.1\" [(ngModel)]=\"carouselCfg.sideScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-mesh-y-offset\">{{ t('scene3d.repeater.carousel_mesh_y_offset', 'Offset verticale mesh') }} ({{ carouselCfg.meshYOffset ?? 0 }})</label>\n <input id=\"scene3d-carousel-mesh-y-offset\" type=\"range\" min=\"-5\" max=\"5\" step=\"0.1\" [(ngModel)]=\"carouselCfg.meshYOffset\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-mesh-rot-x\">{{ t('scene3d.repeater.carousel_mesh_rot', 'Rotazione mesh X / Y / Z (\u00B0)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-carousel-mesh-rot-x\" type=\"number\" step=\"15\" [(ngModel)]=\"carouselCfg.meshRotX\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n <input type=\"number\" step=\"15\" [(ngModel)]=\"carouselCfg.meshRotY\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n <input type=\"number\" step=\"15\" [(ngModel)]=\"carouselCfg.meshRotZ\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"carouselCfg.showBase\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.carousel_base', 'Base') }}\n </label>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"carouselCfg.showArrows\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.carousel_arrows', 'Frecce nav') }}\n </label>\n </div>\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.repeater.carousel_hint', 'Nel viewer le frecce 3D scorrono i record (item centrale a fuoco). Premi Rigenera dopo le modifiche.') }}\n </div>\n }\n\n <!-- Sezione Disco -->\n @if (carouselCfg.showBase) {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('carousel_disc')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('carousel_disc')\" [class.pi-chevron-down]=\"!isSectionCollapsed('carousel_disc')\"></i>\n {{ t('scene3d.repeater.carousel_disc', 'Disco') }}\n </div>\n @if (!isSectionCollapsed('carousel_disc')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-disc-size\">{{ t('scene3d.repeater.carousel_disc_size', 'Dimensione disco') }} ({{ carouselCfg.baseSize }})</label>\n <input id=\"scene3d-carousel-disc-size\" type=\"range\" min=\"0.2\" max=\"1.5\" step=\"0.02\" [(ngModel)]=\"carouselCfg.baseSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-color\">{{ t('scene3d.repeater.carousel_base_color', 'Colore base') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-carousel-base-color\" type=\"color\" [(ngModel)]=\"carouselCfg.baseColor\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-metalness\">{{ t('scene3d.repeater.carousel_metalness', 'Metalness') }} ({{ carouselCfg.baseMetalness }})</label>\n <input id=\"scene3d-carousel-base-metalness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.baseMetalness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-roughness\">{{ t('scene3d.repeater.carousel_roughness', 'Roughness') }} ({{ carouselCfg.baseRoughness }})</label>\n <input id=\"scene3d-carousel-base-roughness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.baseRoughness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-clearcoat\">{{ t('scene3d.repeater.carousel_clearcoat', 'Clearcoat') }} ({{ carouselCfg.baseClearcoat }})</label>\n <input id=\"scene3d-carousel-base-clearcoat\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.baseClearcoat\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n }\n }\n\n <!-- Sezione Frecce -->\n @if (carouselCfg.showArrows) {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('carousel_arrows')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('carousel_arrows')\" [class.pi-chevron-down]=\"!isSectionCollapsed('carousel_arrows')\"></i>\n {{ t('scene3d.repeater.carousel_arrows', 'Frecce nav') }}\n </div>\n @if (!isSectionCollapsed('carousel_arrows')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-size\">{{ t('scene3d.repeater.carousel_arrow_size', 'Dimensione frecce') }} ({{ carouselCfg.arrowSize }})</label>\n <input id=\"scene3d-carousel-arrow-size\" type=\"range\" min=\"0.3\" max=\"2.5\" step=\"0.05\" [(ngModel)]=\"carouselCfg.arrowSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-color\">{{ t('scene3d.repeater.carousel_arrow_color', 'Colore frecce') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-carousel-arrow-color\" type=\"color\" [(ngModel)]=\"carouselCfg.arrowColor\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-emissive\">{{ t('scene3d.repeater.carousel_arrow_glow', 'Bagliore (emissive)') }} ({{ carouselCfg.arrowEmissive }})</label>\n <input id=\"scene3d-carousel-arrow-emissive\" type=\"range\" min=\"0\" max=\"3\" step=\"0.05\" [(ngModel)]=\"carouselCfg.arrowEmissive\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-metalness\">{{ t('scene3d.repeater.carousel_metalness', 'Metalness') }} ({{ carouselCfg.arrowMetalness }})</label>\n <input id=\"scene3d-carousel-arrow-metalness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.arrowMetalness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-roughness\">{{ t('scene3d.repeater.carousel_roughness', 'Roughness') }} ({{ carouselCfg.arrowRoughness }})</label>\n <input id=\"scene3d-carousel-arrow-roughness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.arrowRoughness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-clearcoat\">{{ t('scene3d.repeater.carousel_clearcoat', 'Clearcoat') }} ({{ carouselCfg.arrowClearcoat }})</label>\n <input id=\"scene3d-carousel-arrow-clearcoat\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.arrowClearcoat\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n }\n }\n }\n\n <!-- Archetipo Chart 3D: bar->cilindri, pie->donut(torus), area->ribbon. -->\n @if (repeaterCfg.layout.mode === 'chart') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('chart')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('chart')\" [class.pi-chevron-down]=\"!isSectionCollapsed('chart')\"></i>\n {{ t('scene3d.repeater.chart', 'Chart 3D') }}\n </div>\n @if (!isSectionCollapsed('chart')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-type\">{{ t('scene3d.repeater.chart_type', 'Tipo chart') }}</label>\n <select id=\"scene3d-chart-type\" class=\"scene3d-material-select\" [(ngModel)]=\"chartCfg.type\" (ngModelChange)=\"onRepeaterConfigChange()\">\n <option value=\"bar\">{{ t('scene3d.repeater.chart_bar', 'Barre (cilindri)') }}</option>\n <option value=\"pie\">{{ t('scene3d.repeater.chart_pie', 'Torta (donut)') }}</option>\n <option value=\"area\">{{ t('scene3d.repeater.chart_area', 'Area (line/spline)') }}</option>\n <option value=\"funnel\">{{ t('scene3d.repeater.chart_funnel', 'Funnel (coni)') }}</option>\n <option value=\"radar\">{{ t('scene3d.repeater.chart_radar', 'Radar (stella)') }}</option>\n <option value=\"scatter\">{{ t('scene3d.repeater.chart_scatter', 'Scatter / bolle') }}</option>\n </select>\n </div>\n @if (chartCfg.type !== 'scatter') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select inputId=\"scene3d-chart-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n }\n @if (chartCfg.type === 'scatter') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-x\">{{ t('scene3d.repeater.chart_x', 'Colonna X') }}</label>\n <p-select inputId=\"scene3d-chart-x\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.xColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-y\">{{ t('scene3d.repeater.chart_y', 'Colonna Y (opz.)') }}</label>\n <p-select inputId=\"scene3d-chart-y\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.yColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-z\">{{ t('scene3d.repeater.chart_z', 'Colonna Z (opz.)') }}</label>\n <p-select inputId=\"scene3d-chart-z\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.zColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-size\">{{ t('scene3d.repeater.chart_size', 'Colonna dimensione (opz.)') }}</label>\n <p-select inputId=\"scene3d-chart-size\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.sizeColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.chart_size_ph', 'Costante')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n }\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-color-col\">{{ t('scene3d.repeater.chart_color_col', 'Colonna colore (opz.)') }}</label>\n <p-select inputId=\"scene3d-chart-color-col\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.colorColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.chart_palette', 'Palette automatica')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n @if (chartCfg.type === 'bar' || chartCfg.type === 'area' || chartCfg.type === 'funnel' || chartCfg.type === 'scatter') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-height\">{{ chartCfg.type === 'scatter' ? t('scene3d.repeater.chart_span', 'Estensione') : t('scene3d.repeater.chart_height', 'Altezza') }} ({{ chartCfg.height }})</label>\n <input id=\"scene3d-chart-height\" type=\"range\" min=\"1\" max=\"20\" step=\"0.5\" [(ngModel)]=\"chartCfg.height\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n }\n @if (chartCfg.type === 'bar' || chartCfg.type === 'area') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-spacing\">{{ t('scene3d.repeater.chart_spacing', 'Passo') }} ({{ chartCfg.spacing }})</label>\n <input id=\"scene3d-chart-spacing\" type=\"range\" min=\"0.5\" max=\"5\" step=\"0.1\" [(ngModel)]=\"chartCfg.spacing\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n }\n @if (chartCfg.type === 'bar' || chartCfg.type === 'scatter') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-bar-radius\">{{ chartCfg.type === 'scatter' ? t('scene3d.repeater.chart_bubble', 'Raggio bolla') : t('scene3d.repeater.chart_bar_radius', 'Raggio barra') }} ({{ chartCfg.barRadius }})</label>\n <input id=\"scene3d-chart-bar-radius\" type=\"range\" min=\"0.1\" max=\"2\" step=\"0.05\" [(ngModel)]=\"chartCfg.barRadius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n }\n @if (chartCfg.type === 'pie' || chartCfg.type === 'funnel' || chartCfg.type === 'radar') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-radius\">{{ t('scene3d.repeater.chart_radius', 'Raggio') }} ({{ chartCfg.radius }})</label>\n <input id=\"scene3d-chart-radius\" type=\"range\" min=\"1\" max=\"20\" step=\"0.5\" [(ngModel)]=\"chartCfg.radius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n }\n @if (chartCfg.type === 'pie' || chartCfg.type === 'area') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-thickness\">{{ chartCfg.type === 'pie' ? t('scene3d.repeater.chart_tube', 'Spessore donut') : t('scene3d.repeater.chart_depth', 'Profondita\\' area') }} ({{ chartCfg.thickness }})</label>\n <input id=\"scene3d-chart-thickness\" type=\"range\" min=\"0.1\" max=\"5\" step=\"0.1\" [(ngModel)]=\"chartCfg.thickness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n }\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.chart_labels', 'Etichette') }}\n </label>\n @if (chartCfg.type === 'bar' || chartCfg.type === 'area') {\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.showAxes\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.chart_axes', 'Assi') }}\n </label>\n }\n @if (chartCfg.type === 'bar' || chartCfg.type === 'pie') {\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.showLegend\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.chart_legend', 'Legenda') }}\n </label>\n }\n @if (chartCfg.type === 'area') {\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.smooth\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.chart_smooth', 'Smoothing spline') }}\n </label>\n }\n </div>\n @if (chartCfg.showLabels) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-label-col\">{{ t('scene3d.repeater.chart_label_col', 'Colonna etichetta (opz.)') }}</label>\n <p-select inputId=\"scene3d-chart-label-col\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.chart_label_val', 'Usa il valore')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n }\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.repeater.chart_hint', 'La colonna valore guida l\\'altezza (barre/area) o la quota (torta). Premi Rigenera dopo le modifiche.') }}\n </div>\n }\n }\n\n <!-- Archetipo Surface / Heatmap (matrice riga\u00D7colonna\u00D7valore) -->\n @if (repeaterCfg.layout.mode === 'surface') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('surface')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('surface')\" [class.pi-chevron-down]=\"!isSectionCollapsed('surface')\"></i>\n {{ t('scene3d.repeater.surface', 'Surface / Heatmap') }}\n </div>\n @if (!isSectionCollapsed('surface')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-row\">{{ t('scene3d.repeater.surf_row', 'Colonna riga') }}</label>\n <p-select inputId=\"scene3d-surf-row\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"surfaceCfg.rowColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-col\">{{ t('scene3d.repeater.surf_col', 'Colonna colonna') }}</label>\n <p-select inputId=\"scene3d-surf-col\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"surfaceCfg.colColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select inputId=\"scene3d-surf-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"surfaceCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-height\">{{ t('scene3d.repeater.surf_height', 'Scala altezza') }} ({{ surfaceCfg.heightScale }})</label>\n <input id=\"scene3d-surf-height\" type=\"range\" min=\"1\" max=\"20\" step=\"0.5\" [(ngModel)]=\"surfaceCfg.heightScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-cell\">{{ t('scene3d.repeater.surf_cell', 'Dimensione cella') }} ({{ surfaceCfg.cellSize }})</label>\n <input id=\"scene3d-surf-cell\" type=\"range\" min=\"0.4\" max=\"4\" step=\"0.1\" [(ngModel)]=\"surfaceCfg.cellSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-color-low\">{{ t('scene3d.repeater.surf_colors', 'Colori (basso / alto)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-surf-color-low\" type=\"color\" [(ngModel)]=\"surfaceCfg.colorLow\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n <input type=\"color\" [(ngModel)]=\"surfaceCfg.colorHigh\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"surfaceCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.chart_labels', 'Etichette') }}\n </label>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.surf_hint', 'Riga \u00D7 colonna = celle; valore \u2192 altezza e colore. Premi Rigenera.') }}</div>\n }\n }\n\n <!-- Archetipo Gauge / KPI (valore singolo dal primo record) -->\n @if (repeaterCfg.layout.mode === 'gauge') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('gauge')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('gauge')\" [class.pi-chevron-down]=\"!isSectionCollapsed('gauge')\"></i>\n {{ t('scene3d.repeater.gauge', 'Gauge / KPI') }}\n </div>\n @if (!isSectionCollapsed('gauge')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select inputId=\"scene3d-gauge-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"gaugeCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-label\">{{ t('scene3d.repeater.gauge_label', 'Colonna etichetta (opz.)') }}</label>\n <p-select inputId=\"scene3d-gauge-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"gaugeCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_label_val', 'Usa il valore')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-min\">{{ t('scene3d.repeater.gauge_range', 'Min / Max') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-gauge-min\" type=\"number\" step=\"1\" [(ngModel)]=\"gaugeCfg.min\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n <input type=\"number\" step=\"1\" [(ngModel)]=\"gaugeCfg.max\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-radius\">{{ t('scene3d.repeater.chart_radius', 'Raggio') }} ({{ gaugeCfg.radius }})</label>\n <input id=\"scene3d-gauge-radius\" type=\"range\" min=\"1\" max=\"12\" step=\"0.5\" [(ngModel)]=\"gaugeCfg.radius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.gauge_hint', 'Usa il valore del PRIMO record. Premi Rigenera.') }}</div>\n }\n }\n\n <!-- Archetipi Node-graph / Tree (gerarchia id \u2192 parent_id) -->\n @if (repeaterCfg.layout.mode === 'nodegraph' || repeaterCfg.layout.mode === 'tree') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('graph')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('graph')\" [class.pi-chevron-down]=\"!isSectionCollapsed('graph')\"></i>\n {{ repeaterCfg.layout.mode === 'tree' ? t('scene3d.repeater.tree', 'Albero (tree)') : t('scene3d.repeater.nodegraph', 'Node graph') }}\n </div>\n @if (!isSectionCollapsed('graph')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-id\">{{ t('scene3d.repeater.graph_id', 'Colonna ID') }}</label>\n <p-select inputId=\"scene3d-graph-id\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.idColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-parent\">{{ t('scene3d.repeater.graph_parent', 'Colonna padre') }}</label>\n <p-select inputId=\"scene3d-graph-parent\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.parentColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-label\">{{ t('scene3d.repeater.graph_label', 'Colonna etichetta') }}</label>\n <p-select inputId=\"scene3d-graph-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-value\">{{ t('scene3d.repeater.graph_value', 'Colonna valore (dim. nodo)') }}</label>\n <p-select inputId=\"scene3d-graph-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_palette', 'Palette automatica')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-nodesize\">{{ t('scene3d.repeater.graph_nodesize', 'Dimensione nodo') }} ({{ graphCfg.nodeSize }})</label>\n <input id=\"scene3d-graph-nodesize\" type=\"range\" min=\"0.2\" max=\"2\" step=\"0.05\" [(ngModel)]=\"graphCfg.nodeSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-spacing\">{{ t('scene3d.repeater.graph_spacing', 'Spaziatura') }} ({{ graphCfg.spacing }})</label>\n <input id=\"scene3d-graph-spacing\" type=\"range\" min=\"1\" max=\"10\" step=\"0.5\" [(ngModel)]=\"graphCfg.spacing\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-linkcolor\">{{ t('scene3d.repeater.graph_linkcolor', 'Colore archi') }}</label>\n <div class=\"scene3d-vector-row\"><input id=\"scene3d-graph-linkcolor\" type=\"color\" [(ngModel)]=\"graphCfg.linkColor\" (ngModelChange)=\"onRepeaterConfigChange()\" /></div>\n </div>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"graphCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.chart_labels', 'Etichette') }}\n </label>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.graph_hint', 'ID + padre definiscono la gerarchia; valore \u2192 dimensione nodo. Premi Rigenera.') }}</div>\n }\n }\n\n <!-- Archetipo Map-pins (lat/lon \u2192 pin su piano) -->\n @if (repeaterCfg.layout.mode === 'map') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('map')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('map')\" [class.pi-chevron-down]=\"!isSectionCollapsed('map')\"></i>\n {{ t('scene3d.repeater.map', 'Mappa (pin)') }}\n </div>\n @if (!isSectionCollapsed('map')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-lat\">{{ t('scene3d.repeater.map_lat', 'Colonna latitudine') }}</label>\n <p-select inputId=\"scene3d-map-lat\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.latColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-lon\">{{ t('scene3d.repeater.map_lon', 'Colonna longitudine') }}</label>\n <p-select inputId=\"scene3d-map-lon\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.lonColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select inputId=\"scene3d-map-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_size_ph', 'Costante')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-label\">{{ t('scene3d.repeater.map_label', 'Colonna etichetta (opz.)') }}</label>\n <p-select inputId=\"scene3d-map-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-size\">{{ t('scene3d.repeater.map_size', 'Dimensione mappa') }} ({{ mapCfg.planeSize }})</label>\n <input id=\"scene3d-map-size\" type=\"range\" min=\"6\" max=\"50\" step=\"1\" [(ngModel)]=\"mapCfg.planeSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-pincolor\">{{ t('scene3d.repeater.map_pincolor', 'Colore pin (senza valore)') }}</label>\n <div class=\"scene3d-vector-row\"><input id=\"scene3d-map-pincolor\" type=\"color\" [(ngModel)]=\"mapCfg.pinColor\" (ngModelChange)=\"onRepeaterConfigChange()\" /></div>\n </div>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"mapCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.map_hint', 'Lat/lon \u2192 pin sul piano; valore \u2192 altezza/colore. Premi Rigenera.') }}</div>\n }\n }\n\n <!-- Archetipo Warehouse-shelf (corsia \u00D7 ripiano \u00D7 quantita') -->\n @if (repeaterCfg.layout.mode === 'warehouse') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('warehouse')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('warehouse')\" [class.pi-chevron-down]=\"!isSectionCollapsed('warehouse')\"></i>\n {{ t('scene3d.repeater.warehouse', 'Magazzino (scaffali)') }}\n </div>\n @if (!isSectionCollapsed('warehouse')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-aisle\">{{ t('scene3d.repeater.wh_aisle', 'Colonna corsia') }}</label>\n <p-select inputId=\"scene3d-wh-aisle\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.aisleColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-shelf\">{{ t('scene3d.repeater.wh_shelf', 'Colonna ripiano') }}</label>\n <p-select inputId=\"scene3d-wh-shelf\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.shelfColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-qty\">{{ t('scene3d.repeater.wh_qty', 'Colonna quantit\u00E0') }}</label>\n <p-select inputId=\"scene3d-wh-qty\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.qtyColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-label\">{{ t('scene3d.repeater.wh_label', 'Colonna articolo (opz.)') }}</label>\n <p-select inputId=\"scene3d-wh-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-box\">{{ t('scene3d.repeater.wh_box', 'Dimensione scatola') }} ({{ warehouseCfg.boxSize }})</label>\n <input id=\"scene3d-wh-box\" type=\"range\" min=\"0.4\" max=\"3\" step=\"0.1\" [(ngModel)]=\"warehouseCfg.boxSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"warehouseCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.wh_hint', 'Corsia \u00D7 ripiano = posizione; quantit\u00E0 \u2192 altezza scatola. Premi Rigenera.') }}</div>\n }\n }\n\n <!-- Archetipo Kanban 3D (stato \u2192 colonne di card) -->\n @if (repeaterCfg.layout.mode === 'kanban') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('kanban')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('kanban')\" [class.pi-chevron-down]=\"!isSectionCollapsed('kanban')\"></i>\n {{ t('scene3d.repeater.kanban', 'Kanban 3D') }}\n </div>\n @if (!isSectionCollapsed('kanban')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-kb-status\">{{ t('scene3d.repeater.kb_status', 'Colonna stato') }}</label>\n <p-select inputId=\"scene3d-kb-status\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"kanbanCfg.statusColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-kb-label\">{{ t('scene3d.repeater.kb_label', 'Colonna titolo (opz.)') }}</label>\n <p-select inputId=\"scene3d-kb-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"kanbanCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-kb-card\">{{ t('scene3d.repeater.kb_card', 'Larghezza card') }} ({{ kanbanCfg.cardWidth }})</label>\n <input id=\"scene3d-kb-card\" type=\"range\" min=\"0.8\" max=\"4\" step=\"0.1\" [(ngModel)]=\"kanbanCfg.cardWidth\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"kanbanCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.kb_hint', 'Ogni stato = una colonna; le card si impilano. Premi Rigenera.') }}</div>\n }\n }\n\n <!-- Archetipo Calendar-wall (data \u2192 griglia settimane\u00D7giorni) -->\n @if (repeaterCfg.layout.mode === 'calendar') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('calendar')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('calendar')\" [class.pi-chevron-down]=\"!isSectionCollapsed('calendar')\"></i>\n {{ t('scene3d.repeater.calendar', 'Calendario') }}\n </div>\n @if (!isSectionCollapsed('calendar')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-date\">{{ t('scene3d.repeater.cal_date', 'Colonna data') }}</label>\n <p-select inputId=\"scene3d-cal-date\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"calendarCfg.dateColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select inputId=\"scene3d-cal-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"calendarCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_size_ph', 'Costante')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-height\">{{ t('scene3d.repeater.surf_height', 'Scala altezza') }} ({{ calendarCfg.heightScale }})</label>\n <input id=\"scene3d-cal-height\" type=\"range\" min=\"1\" max=\"15\" step=\"0.5\" [(ngModel)]=\"calendarCfg.heightScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-cell\">{{ t('scene3d.repeater.surf_cell', 'Dimensione cella') }} ({{ calendarCfg.cellSize }})</label>\n <input id=\"scene3d-cal-cell\" type=\"range\" min=\"0.4\" max=\"3\" step=\"0.1\" [(ngModel)]=\"calendarCfg.cellSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"calendarCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.cal_hint', 'Le date riempiono una griglia settimane\u00D7giorni; valore \u2192 altezza/colore. Premi Rigenera.') }}</div>\n }\n }\n\n <!-- Archetipo Stacked bars (categoria \u00D7 serie) -->\n @if (repeaterCfg.layout.mode === 'stacked') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('stacked')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('stacked')\" [class.pi-chevron-down]=\"!isSectionCollapsed('stacked')\"></i>\n {{ t('scene3d.repeater.stacked', 'Barre impilate') }}\n </div>\n @if (!isSectionCollapsed('stacked')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-category\">{{ t('scene3d.repeater.st_category', 'Colonna categoria') }}</label>\n <p-select inputId=\"scene3d-st-category\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"stackedCfg.categoryColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-series\">{{ t('scene3d.repeater.st_series', 'Colonna serie') }}</label>\n <p-select inputId=\"scene3d-st-series\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"stackedCfg.seriesColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select inputId=\"scene3d-st-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"stackedCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-height\">{{ t('scene3d.repeater.chart_height', 'Altezza') }} ({{ stackedCfg.height }})</label>\n <input id=\"scene3d-st-height\" type=\"range\" min=\"2\" max=\"20\" step=\"0.5\" [(ngModel)]=\"stackedCfg.height\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-bar-width\">{{ t('scene3d.repeater.chart_bar_radius', 'Larghezza barra') }} ({{ stackedCfg.barWidth }})</label>\n <input id=\"scene3d-st-bar-width\" type=\"range\" min=\"0.4\" max=\"3\" step=\"0.1\" [(ngModel)]=\"stackedCfg.barWidth\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"stackedCfg.showLegend\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_legend', 'Legenda') }}</label>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"stackedCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.st_hint', 'Categoria = colonna; serie = segmenti impilati. Premi Rigenera.') }}</div>\n }\n }\n\n @if (repeaterCfg.layout.mode === 'grid' || repeaterCfg.layout.mode === 'cube') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-order\">{{ t('scene3d.repeater.order', 'Ordine di flusso') }}</label>\n <select id=\"scene3d-repeater-order\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.layout.order\" (ngModelChange)=\"onRepeaterConfigChange()\">\n @for (o of layoutOrderOptions; track o.value) { <option [value]=\"o.value\">{{ o.label }}</option> }\n </select>\n </div>\n }\n <!-- Verso di riempimento per-asse (o senso di rotazione per il cerchio). -->\n @for (ax of layoutAxes; track ax) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" [attr.for]=\"'scene3d-axis-dir-' + ax\">{{ axisDirLabel(ax) }}</label>\n <select class=\"scene3d-material-select\" [id]=\"'scene3d-axis-dir-' + ax\" [ngModel]=\"getAxisDir(ax)\" (ngModelChange)=\"setAxisDir(ax, $event)\">\n @for (o of axisDirOptions(ax); track o.value) { <option [ngValue]=\"o.value\">{{ o.label }}</option> }\n </select>\n </div>\n }\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-pos-range\">{{ t('scene3d.repeater.pos_range', 'Range posizione') }} ({{ repeaterCfg.normalize.posRange }})</label>\n <input id=\"scene3d-repeater-pos-range\" type=\"range\" min=\"4\" max=\"200\" step=\"2\" [(ngModel)]=\"repeaterCfg.normalize.posRange\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-scale-min\">{{ t('scene3d.repeater.scale_range', 'Scala min/max') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-repeater-scale-min\" type=\"number\" step=\"0.1\" min=\"0.01\" [(ngModel)]=\"repeaterCfg.normalize.scaleMin\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n <input type=\"number\" step=\"0.1\" min=\"0.01\" [(ngModel)]=\"repeaterCfg.normalize.scaleMax\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n\n <p-button\n icon=\"pi pi-refresh\"\n [label]=\"t('scene3d.repeater.regenerate', 'Rigenera') + (repeaterCount ? ' (' + repeaterCount + ')' : '')\"\n size=\"small\" [loading]=\"repeaterRegenerating\"\n (onClick)=\"regenerateSelectedRepeater()\">\n </p-button>\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.repeater.hint', 'Le mesh sono rigenerate dai dati (non salvate nel JSON). Sposta il gruppo coi gizmo; click su una mesh apre la CRUD del record nel viewer.') }}\n </div>\n\n <!-- Interazioni: callback JS su click/dblclick/rightclick di un item.\n Sezione collassabile in fondo (collassata di default). -->\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"callbacksSectionOpen = !callbacksSectionOpen\">\n <i class=\"pi\" [class.pi-chevron-right]=\"!callbacksSectionOpen\" [class.pi-chevron-down]=\"callbacksSectionOpen\"></i>\n {{ t('scene3d.repeater.interactions', 'Interazioni (callback JS)') }}\n </div>\n @if (callbacksSectionOpen) {\n <div class=\"scene3d-prop-group\">\n <p-button size=\"small\" [text]=\"true\"\n [severity]=\"repeaterCallbackIsSet('click') ? 'primary' : 'secondary'\"\n icon=\"pi pi-code\"\n [label]=\"(repeaterCallbackIsSet('click') ? '\u25CF ' : '') + t('scene3d.repeater.cb_click', 'Click')\"\n (onClick)=\"editRepeaterCallback('click')\"></p-button>\n <p-button size=\"small\" [text]=\"true\"\n [severity]=\"repeaterCallbackIsSet('dblclick') ? 'primary' : 'secondary'\"\n icon=\"pi pi-code\"\n [label]=\"(repeaterCallbackIsSet('dblclick') ? '\u25CF ' : '') + t('scene3d.repeater.cb_dblclick', 'Doppio-click')\"\n (onClick)=\"editRepeaterCallback('dblclick')\"></p-button>\n <p-button size=\"small\" [text]=\"true\"\n [severity]=\"repeaterCallbackIsSet('rightclick') ? 'primary' : 'secondary'\"\n icon=\"pi pi-code\"\n [label]=\"(repeaterCallbackIsSet('rightclick') ? '\u25CF ' : '') + t('scene3d.repeater.cb_rightclick', 'Tasto destro')\"\n (onClick)=\"editRepeaterCallback('rightclick')\"></p-button>\n </div>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.cb_hint', 'Nel callback: record (item), mesh, wuic.showItemPopup({mode,fields,title}). Vale in designer e viewer.') }}</div>\n }\n </div>\n }\n\n <!-- Fisica per-oggetto (Rapier): corpo rigido + parametri -->\n @if (selectedIsMesh) {\n <div class=\"scene3d-material-section\">\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.physics.title', 'Fisica') }}</div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-body\">{{ t('scene3d.physics.body', 'Corpo') }}</label>\n <select id=\"scene3d-physics-body\" class=\"scene3d-material-select\" [(ngModel)]=\"propPhysics.type\" (ngModelChange)=\"onPhysicsInputChange()\">\n <option value=\"none\">{{ t('scene3d.physics.none', 'Nessuno') }}</option>\n <option value=\"static\">{{ t('scene3d.physics.static', 'Statico') }}</option>\n <option value=\"dynamic\">{{ t('scene3d.physics.dynamic', 'Dinamico') }}</option>\n <option value=\"kinematic\">{{ t('scene3d.physics.kinematic', 'Cinematico') }}</option>\n </select>\n </div>\n @if (propPhysics.type !== 'none') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-shape\">{{ t('scene3d.physics.shape', 'Forma collider') }}</label>\n <select id=\"scene3d-physics-shape\" class=\"scene3d-material-select\" [(ngModel)]=\"propPhysics.shape\" (ngModelChange)=\"onPhysicsInputChange()\">\n <option value=\"auto\">{{ t('scene3d.physics.shape_auto', 'Auto (dalla geometria)') }}</option>\n <option value=\"box\">Box</option>\n <option value=\"sphere\">{{ t('scene3d.physics.shape_sphere', 'Sfera') }}</option>\n <option value=\"hull\">{{ t('scene3d.physics.shape_hull', 'Convex hull') }}</option>\n </select>\n </div>\n @if (propPhysics.type === 'dynamic') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-mass\">{{ t('scene3d.physics.mass', 'Massa (kg)') }}</label>\n <input id=\"scene3d-physics-mass\" type=\"number\" step=\"0.1\" min=\"0.001\" [(ngModel)]=\"propPhysics.mass\" (ngModelChange)=\"onPhysicsInputChange()\" />\n </div>\n }\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-friction\">{{ t('scene3d.physics.friction', 'Attrito') }} ({{ propPhysics.friction }})</label>\n <input id=\"scene3d-physics-friction\" type=\"range\" min=\"0\" max=\"1\" step=\"0.05\" [(ngModel)]=\"propPhysics.friction\" (ngModelChange)=\"onPhysicsInputChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-restitution\">{{ t('scene3d.physics.restitution', 'Rimbalzo') }} ({{ propPhysics.restitution }})</label>\n <input id=\"scene3d-physics-restitution\" type=\"range\" min=\"0\" max=\"1\" step=\"0.05\" [(ngModel)]=\"propPhysics.restitution\" (ngModelChange)=\"onPhysicsInputChange()\" />\n </div>\n }\n </div>\n }\n\n <!-- Registrazione animazione: keyframe pos/rot/scala -> AnimationClip -->\n <div class=\"scene3d-anim-section\">\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"animRecSectionOpen = !animRecSectionOpen\">\n <i class=\"pi\" [class.pi-chevron-right]=\"!animRecSectionOpen\" [class.pi-chevron-down]=\"animRecSectionOpen\"></i>\n {{ t('scene3d.animrec.title', 'Registra animazione') }}\n </div>\n @if (animRecSectionOpen) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-animrec-clip-name\">{{ t('scene3d.animrec.clip_name', 'Nome clip') }}</label>\n <input id=\"scene3d-animrec-clip-name\" type=\"text\" [(ngModel)]=\"animRecClipName\" [placeholder]=\"t('scene3d.animrec.clip_name_ph', 'Clip 1')\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-animrec-time\">{{ t('scene3d.animrec.time', 'Tempo T (s)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-animrec-time\" type=\"number\" step=\"0.1\" min=\"0\" [(ngModel)]=\"animRecTime\" />\n <p-button\n icon=\"pi pi-plus\"\n [label]=\"t('scene3d.animrec.add', 'Aggiungi keyframe')\"\n size=\"small\"\n (onClick)=\"addRecKeyframe()\">\n </p-button>\n </div>\n </div>\n\n @if (animRecKeyframes.length > 0) {\n <div class=\"scene3d-kf-list\">\n @for (kf of animRecKeyframes; track kf.time; let i = $index) {\n <div class=\"scene3d-kf-row\">\n <button type=\"button\" class=\"scene3d-kf-apply\" (click)=\"applyRecKeyframe(i)\"\n [title]=\"t('scene3d.animrec.apply', 'Applica questa posa all\\'oggetto')\">\n <i class=\"pi pi-eye\"></i> t = {{ kf.time }}s\n </button>\n <button type=\"button\" class=\"scene3d-kf-delete\" (click)=\"removeRecKeyframe(i)\"\n [title]=\"t('designer.remove', 'Rimuovi')\">\n <i class=\"pi pi-trash\"></i>\n </button>\n </div>\n }\n </div>\n }\n\n <p-button\n icon=\"pi pi-video\"\n [label]=\"t('scene3d.animrec.generate', 'Genera clip')\"\n size=\"small\"\n [disabled]=\"animRecKeyframes.length < 2\"\n (onClick)=\"generateRecClip()\">\n </p-button>\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.animrec.hint', 'Posa l\\'oggetto coi gizmo, aggiungi un keyframe al tempo T, ripeti. La clip generata usa il transport qui sopra e persiste col salvataggio (autoplay nel viewer).') }}\n </div>\n }\n </div>\n\n <div class=\"scene3d-gizmo-hint\">\n {{ t('scene3d.props.gizmo_hint', 'g = sposta, r = ruota, s = scala') }}\n </div>\n } @else {\n <div class=\"scene3d-props-empty\">\n {{ t('scene3d.props.empty', 'Nessun oggetto selezionato') }}\n </div>\n }\n </div>\n\n </div>\n\n <!-- Footer componenti scena (mirror del footer del designer 2D):\n click = seleziona, right-click = ctx menu con Rimuovi. -->\n <div class=\"scene3d-footer\">\n <span class=\"scene3d-footer-label\">{{ t('scene3d.footer.title', 'Scena') }}:</span>\n <div class=\"scene3d-footer-list\">\n @for (item of sceneObjects; track item.uuid) {\n <button\n type=\"button\"\n class=\"scene3d-footer-item\"\n [class.active]=\"item.uuid === selectedObjectUuid\"\n [title]=\"item.label\"\n (click)=\"selectFromFooter(item.uuid)\"\n (contextmenu)=\"showFooterContextMenu($event, item.uuid, sceneFooterCtxMenu)\">\n {{ item.label }}\n </button>\n } @empty {\n <span class=\"scene3d-footer-empty\">{{ t('scene3d.footer.empty', 'Nessun componente nella scena') }}</span>\n }\n </div>\n <p-contextMenu #sceneFooterCtxMenu [model]=\"footerCtxItems\" appendTo=\"body\" />\n </div>\n</div>\n", styles: [":host{display:block;width:100%;height:100%;min-height:0}.scene3d-designer-shell{display:flex;flex-direction:column;width:100%;height:100%;min-height:480px}.scene3d-toolbar{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid var(--surface-border, #dee2e6);flex:0 0 auto;flex-wrap:wrap}.scene3d-load-select{min-width:14rem}.scene3d-body{display:flex;flex:1 1 auto;min-height:0}.scene3d-palette{flex:0 0 12rem;display:flex;flex-direction:column;gap:.25rem;padding:.5rem;overflow-y:auto;border-right:1px solid var(--surface-border, #dee2e6)}.scene3d-panel-title{font-weight:600;font-size:.85rem;text-transform:uppercase;opacity:.7;margin-bottom:.25rem}.scene3d-palette-item{display:flex;align-items:center;gap:.5rem;padding:.4rem .5rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;background:var(--surface-card, #fff);cursor:pointer;text-align:left;font-size:.85rem;color:inherit}.scene3d-palette-item:hover:not(:disabled){background:var(--surface-hover, #f5f5f5)}.scene3d-palette-item:disabled{opacity:.45;cursor:not-allowed}.scene3d-palette-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.scene3d-viewport{flex:1 1 auto;min-width:0;position:relative;overflow:hidden;background:#1e1e28}.scene3d-viewport canvas{display:block;width:100%;height:100%}.scene3d-properties{flex:0 0 16rem;display:flex;flex-direction:column;gap:.6rem;padding:.75rem;overflow-y:auto;border-left:1px solid var(--surface-border, #dee2e6)}.scene3d-prop-group{display:flex;flex-direction:column;gap:.25rem}.scene3d-prop-label{font-size:.8rem;font-weight:600;opacity:.8}.scene3d-prop-readonly{font-size:.85rem;opacity:.7;word-break:break-all}.scene3d-vector-row{display:flex;gap:.25rem}.scene3d-vector-row input{width:100%;min-width:0;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem}.scene3d-props-empty,.scene3d-gizmo-hint{font-size:.8rem;opacity:.6}.scene3d-prop-checkbox{display:inline-flex;align-items:center;gap:.4rem;cursor:pointer}.scene3d-kf-list{display:flex;flex-direction:column;gap:3px;max-height:160px;overflow-y:auto;border:1px dashed var(--surface-border, #dee2e6);border-radius:4px;padding:4px}.scene3d-kf-row{display:flex;align-items:center;justify-content:space-between;gap:6px}.scene3d-kf-apply,.scene3d-kf-delete{border:1px solid var(--surface-border, #dee2e6);background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.78rem;cursor:pointer;display:inline-flex;align-items:center;gap:4px}.scene3d-kf-apply{flex:1 1 auto;justify-content:flex-start}.scene3d-kf-apply:hover{border-color:#3b82f6}.scene3d-kf-delete:hover{border-color:#ef4444;color:#ef4444}.scene3d-anim-section input[type=text],.scene3d-anim-section input[type=number]{width:100%;min-width:0;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem}.scene3d-material-section{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6)}.scene3d-material-section input[type=range]{width:100%}.scene3d-material-select{width:100%;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem;background:var(--surface-card, #fff);color:inherit}.scene3d-shader-toggle{cursor:pointer;-webkit-user-select:none;user-select:none}.scene3d-shader-src{width:100%;min-width:0;resize:vertical;padding:.35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-family:Consolas,Monaco,monospace;font-size:.72rem;line-height:1.35;background:#14141c;color:#d1d5db}.scene3d-pt-hud{position:absolute;top:8px;left:50%;transform:translate(-50%);z-index:25;display:flex;align-items:center;gap:12px;padding:4px 12px;border-radius:6px;background:#14141cd9;color:#e5e7eb;font-size:.78rem;border:1px solid #3b82f6}.scene3d-pt-scale{display:inline-flex;align-items:center;gap:6px}.scene3d-pt-scale input[type=range]{width:90px}.scene3d-pt-exit{border:1px solid #3b82f6;background:transparent;color:#93c5fd;border-radius:4px;padding:2px 8px;font-size:.75rem;cursor:pointer}.scene3d-cam-preview{position:absolute;z-index:20;width:320px;border:1px solid #3b82f6;border-radius:6px;background:#14141c;box-shadow:0 6px 24px #00000073;overflow:hidden}.scene3d-cam-preview-header{display:flex;align-items:center;justify-content:space-between;padding:3px 8px;background:#1f2937;color:#e5e7eb;font-size:.75rem;cursor:move;-webkit-user-select:none;user-select:none}.scene3d-cam-preview-close{border:none;background:transparent;color:#e5e7eb;font-size:1rem;line-height:1;cursor:pointer;padding:0 2px}.scene3d-cam-preview-body{width:320px;height:192px}.scene3d-cam-preview-body canvas{display:block}.scene3d-gizmo-hint{margin-top:.5rem;padding-top:.5rem;border-top:1px dashed var(--surface-border, #dee2e6)}.scene3d-binding-section{display:flex;flex-direction:column;gap:.6rem;margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6)}.scene3d-panel-subtitle{font-weight:600;font-size:.78rem;text-transform:uppercase;opacity:.7}.scene3d-panel-subtitle.scene3d-collapsible{display:flex;align-items:center;gap:.4rem;cursor:pointer;-webkit-user-select:none;user-select:none;padding:.15rem 0;opacity:.85}.scene3d-panel-subtitle.scene3d-collapsible:hover{opacity:1}.scene3d-panel-subtitle.scene3d-collapsible>.pi{font-size:.7rem;opacity:.8}.scene3d-field-map{display:flex;flex-direction:column;gap:.5rem;padding:.5rem;border:1px dashed var(--surface-border, #dee2e6);border-radius:4px}.scene3d-binding-select{width:100%}.scene3d-binding-section input[type=text]{width:100%;min-width:0;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem}.scene3d-open-record-btn{align-self:flex-start}.scene3d-binding-hint{font-size:.78rem;opacity:.6}.scene3d-anim-section{display:flex;flex-direction:column;gap:.6rem;margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6)}.scene3d-anim-transport{display:flex;gap:.35rem}.scene3d-anim-scrubber{width:100%;min-width:0}.scene3d-anim-time{font-size:.78rem;opacity:.6;text-align:right}.scene3d-footer{display:flex;align-items:center;gap:8px;padding:6px 8px;border-top:1px solid var(--surface-border, #dee2e6);min-height:36px;flex:0 0 auto;width:100%;overflow:hidden}.scene3d-footer-label{font-size:.85rem;font-weight:600;white-space:nowrap}.scene3d-footer-list{display:flex;align-items:center;gap:6px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;width:100%}.scene3d-footer-item{border:1px solid var(--surface-border, #dee2e6);background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.8rem;line-height:1.25rem;cursor:pointer;white-space:nowrap;display:inline-flex;align-items:center;gap:4px}.scene3d-footer-item.active{border-color:#3b82f6;color:#1d4ed8}.scene3d-footer-empty{font-size:.8rem;opacity:.6}.scene3d-scene-option{display:flex;align-items:center;gap:10px;min-width:0}.scene3d-scene-option-thumb{width:72px;height:44px;flex:0 0 auto;border-radius:4px;object-fit:cover;border:1px solid #cbd5e1;background:#1e1e28}.scene3d-scene-option-thumb-empty{display:inline-flex;align-items:center;justify-content:center;color:#64748b}.scene3d-scene-option-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.RangeValueAccessor, selector: "input[type=range][formControlName],input[type=range][formControl],input[type=range][ngModel]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i4.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "directive", type: i3.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i5.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: SplitButtonModule }, { kind: "component", type: i12.SplitButton, selector: "p-splitbutton, p-splitButton, p-split-button", inputs: ["model", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "icon", "iconPos", "label", "tooltip", "tooltipOptions", "styleClass", "menuStyle", "menuStyleClass", "dropdownIcon", "appendTo", "dir", "expandAriaLabel", "showTransitionOptions", "hideTransitionOptions", "motionOptions", "buttonProps", "menuButtonProps", "autofocus", "disabled", "tabindex", "menuButtonDisabled", "buttonDisabled"], outputs: ["onClick", "onMenuHide", "onMenuShow", "onDropdownClick"] }, { kind: "ngmodule", type: ContextMenuModule }, { kind: "component", type: i14.ContextMenu, selector: "p-contextMenu, p-contextmenu, p-context-menu", inputs: ["model", "triggerEvent", "target", "global", "style", "styleClass", "autoZIndex", "baseZIndex", "id", "breakpoint", "ariaLabel", "ariaLabelledBy", "pressDelay", "appendTo", "motionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: TranslateModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4794
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: Scene3dDesignerComponent, isStandalone: true, selector: "wuic-scene3d-designer", viewQueries: [{ propertyName: "viewportContainer", first: true, predicate: ["viewportContainer"], descendants: true }, { propertyName: "previewCanvasHost", first: true, predicate: ["previewCanvasHost"], descendants: true }], ngImport: i0, template: "<!-- F2/F3: viewport WebGL (canvas three.js) + palette oggetti a sinistra +\r\n property panel a destra + toolbar (save/load/new/delete). -->\r\n<div class=\"scene3d-designer-shell\">\r\n\r\n <!-- Toolbar -->\r\n <div class=\"scene3d-toolbar\">\r\n <p-button\r\n icon=\"pi pi-file\"\r\n [label]=\"t('scene3d.toolbar.new', 'Nuova')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n (onClick)=\"newScene()\">\r\n </p-button>\r\n\r\n <p-button\r\n icon=\"pi pi-save\"\r\n [label]=\"t('scene3d.toolbar.save', 'Salva')\"\r\n size=\"small\"\r\n [loading]=\"savingScene\"\r\n (onClick)=\"saveScene()\">\r\n </p-button>\r\n\r\n <p-select\r\n [options]=\"savedSceneOptions\"\r\n [(ngModel)]=\"selectedSavedSceneKey\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n [filter]=\"true\"\r\n [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.toolbar.load_placeholder', 'Carica scena...')\"\r\n appendTo=\"body\"\r\n styleClass=\"scene3d-load-select\"\r\n (onChange)=\"onSavedSceneSelectionChange()\">\r\n <!-- Anteprima scena (thumbnail dal worker server-side GPU); placeholder cubo se assente. -->\r\n <ng-template pTemplate=\"item\" let-opt>\r\n <div class=\"scene3d-scene-option\">\r\n @if (opt.thumbnail) {\r\n <img class=\"scene3d-scene-option-thumb\" [src]=\"opt.thumbnail\" [alt]=\"opt.label\" loading=\"lazy\" />\r\n } @else {\r\n <span class=\"scene3d-scene-option-thumb scene3d-scene-option-thumb-empty\">\r\n <i class=\"pi pi-box\"></i>\r\n </span>\r\n }\r\n <span class=\"scene3d-scene-option-label\">{{ opt.label }}</span>\r\n </div>\r\n </ng-template>\r\n </p-select>\r\n\r\n <p-button\r\n icon=\"pi pi-folder-open\"\r\n [label]=\"t('scene3d.toolbar.load', 'Carica')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n [loading]=\"loadingScene\"\r\n [disabled]=\"!selectedSavedSceneKey\"\r\n (onClick)=\"loadScene()\">\r\n </p-button>\r\n\r\n <p-button\r\n icon=\"pi pi-trash\"\r\n [label]=\"t('scene3d.toolbar.delete_selected', 'Elimina selezionato')\"\r\n severity=\"danger\"\r\n size=\"small\"\r\n [disabled]=\"!selectedObjectUuid\"\r\n (onClick)=\"deleteSelectedObject()\">\r\n </p-button>\r\n\r\n <!-- Toggle luci default del viewport: OFF = si vede solo l'effetto delle luci droppate -->\r\n <p-button\r\n icon=\"pi pi-sun\"\r\n [label]=\"t('scene3d.toolbar.default_lights', 'Luci default')\"\r\n [severity]=\"defaultLightsEnabled ? 'primary' : 'secondary'\"\r\n [outlined]=\"!defaultLightsEnabled\"\r\n size=\"small\"\r\n [title]=\"t('scene3d.toolbar.default_lights_hint', 'Attiva/disattiva le luci di default del viewport')\"\r\n (onClick)=\"toggleDefaultLights()\">\r\n </p-button>\r\n\r\n <!-- Toggle shadow map del renderer (default ON) -->\r\n <p-button\r\n icon=\"pi pi-moon\"\r\n [label]=\"t('scene3d.toolbar.shadows', 'Ombre')\"\r\n [severity]=\"shadowsEnabled ? 'primary' : 'secondary'\"\r\n [outlined]=\"!shadowsEnabled\"\r\n size=\"small\"\r\n [title]=\"t('scene3d.toolbar.shadows_hint', 'Attiva/disattiva le ombre proiettate')\"\r\n (onClick)=\"toggleShadows()\">\r\n </p-button>\r\n\r\n <!-- Renderer WebGPU (opt-in): fallback WebGL se non supportato; shader GLSL e PT restano WebGL-only -->\r\n <p-button\r\n icon=\"pi pi-bolt\"\r\n label=\"WebGPU\"\r\n [severity]=\"rendererMode === 'webgpu' ? 'primary' : 'secondary'\"\r\n [outlined]=\"rendererMode !== 'webgpu'\"\r\n size=\"small\"\r\n [loading]=\"rendererSwitching\"\r\n [disabled]=\"!webGpuSupported\"\r\n [title]=\"webGpuSupported\r\n ? t('scene3d.renderer.toggle_hint', 'Renderer WebGPU (node-materials). Shader GLSL custom e path tracing restano disponibili solo in WebGL')\r\n : t('scene3d.renderer.webgpu_unavailable', 'WebGPU non disponibile in questo browser: resto su WebGL')\"\r\n (onClick)=\"toggleWebGpuRenderer()\">\r\n </p-button>\r\n\r\n <!-- Toggle guide di scena: proxy luci/camera + linee helper/frustum -->\r\n <p-button\r\n icon=\"pi pi-eye\"\r\n [label]=\"t('scene3d.toolbar.helpers', 'Guide')\"\r\n [severity]=\"helpersVisible ? 'primary' : 'secondary'\"\r\n [outlined]=\"!helpersVisible\"\r\n size=\"small\"\r\n [title]=\"t('scene3d.toolbar.helpers_hint', 'Mostra/nasconde i placeholder delle luci e le linee helper/frustum')\"\r\n (onClick)=\"toggleHelpers()\">\r\n </p-button>\r\n\r\n <!-- Auto-rigenera (toggle globale): se ON, ogni cambio di proprieta' rigenera\r\n automaticamente il repeater selezionato (con debounce), senza cliccare\r\n \"Rigenera\". Per gli altri oggetti palette i cambi si applicano gia' live. -->\r\n <p-button\r\n icon=\"pi pi-sync\"\r\n [label]=\"t('scene3d.toolbar.auto_regen', 'Auto-rigenera')\"\r\n [severity]=\"autoRegen ? 'primary' : 'secondary'\"\r\n [outlined]=\"!autoRegen\"\r\n size=\"small\"\r\n [title]=\"t('scene3d.toolbar.auto_regen_hint', 'Se attivo, ogni modifica di proprieta rigenera automaticamente (repeater), senza premere Rigenera')\"\r\n (onClick)=\"autoRegen = !autoRegen\">\r\n </p-button>\r\n\r\n <!-- Render qualita' (path tracing progressivo, lazy): GI + ombre colorate dai vetri -->\r\n <p-button\r\n icon=\"pi pi-sparkles\"\r\n [label]=\"pathTracingEnabled ? t('scene3d.toolbar.pt_exit', 'Realtime') : t('scene3d.toolbar.pt', 'Render PT')\"\r\n [severity]=\"pathTracingEnabled ? 'primary' : 'secondary'\"\r\n [outlined]=\"!pathTracingEnabled\"\r\n size=\"small\"\r\n [loading]=\"ptBuilding\"\r\n [title]=\"t('scene3d.toolbar.pt_hint', 'Render path-traced progressivo (GI, ombre colorate dai vetri). Modalita\\' preview: editing sospeso')\"\r\n (onClick)=\"togglePathTracing()\">\r\n </p-button>\r\n\r\n <!-- Fisica: Play/Stop simulazione + menu (Applica pose / Impostazioni scena) -->\r\n <p-splitButton\r\n [icon]=\"physicsSimulating ? 'pi pi-stop' : 'pi pi-play'\"\r\n [label]=\"physicsSimulating ? t('scene3d.toolbar.physics_stop', 'Stop fisica') : t('scene3d.toolbar.physics_play', 'Fisica')\"\r\n [severity]=\"physicsSimulating ? 'primary' : 'secondary'\"\r\n [outlined]=\"!physicsSimulating\"\r\n size=\"small\"\r\n [model]=\"physicsMenuItems\"\r\n appendTo=\"body\"\r\n [attr.title]=\"t('scene3d.toolbar.physics_hint', 'Simula i corpi rigidi configurati (Stop = ripristina le pose)')\"\r\n (onClick)=\"togglePhysicsSimulation()\">\r\n </p-splitButton>\r\n\r\n <!-- Bake luci per-vertice: cuoce l'illuminazione nei vertex colors (v1 lightmap) -->\r\n <p-button\r\n icon=\"pi pi-bolt\"\r\n [label]=\"t('scene3d.toolbar.bake', 'Bake luci')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n [title]=\"t('scene3d.toolbar.bake_hint', 'Cuoce l\\'illuminazione corrente nei vertex colors (materiale statico unlit)')\"\r\n (onClick)=\"bakeLightsAction()\">\r\n </p-button>\r\n\r\n <!-- Crea una camera di scena che replica il punto di vista attuale del designer -->\r\n <p-button\r\n icon=\"pi pi-camera\"\r\n [label]=\"t('scene3d.toolbar.camera_from_view', 'Camera da vista attuale')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n [title]=\"t('scene3d.toolbar.camera_from_view_hint', 'Crea una camera nella scena con posizione e inquadratura identiche alla vista corrente')\"\r\n (onClick)=\"createCameraFromView()\">\r\n </p-button>\r\n\r\n <!-- Vista camera: render dalla camera utente (selezionata o prima in scena).\r\n Con piu' camere in scena diventa uno split-button col picker. -->\r\n @if (sceneCameraCount > 1) {\r\n <p-splitButton\r\n icon=\"pi pi-video\"\r\n [label]=\"viewingThroughCamera ? t('scene3d.toolbar.designer_view', 'Vista designer') : t('scene3d.toolbar.camera_view', 'Vista camera')\"\r\n [severity]=\"viewingThroughCamera ? 'primary' : 'secondary'\"\r\n [outlined]=\"!viewingThroughCamera\"\r\n size=\"small\"\r\n [model]=\"cameraViewMenuItems\"\r\n appendTo=\"body\"\r\n [attr.title]=\"t('scene3d.toolbar.camera_view_hint', 'Alterna tra la vista di editing e la vista dalla camera della scena')\"\r\n (onClick)=\"toggleCameraView()\">\r\n </p-splitButton>\r\n } @else {\r\n <p-button\r\n icon=\"pi pi-video\"\r\n [label]=\"viewingThroughCamera ? t('scene3d.toolbar.designer_view', 'Vista designer') : t('scene3d.toolbar.camera_view', 'Vista camera')\"\r\n [severity]=\"viewingThroughCamera ? 'primary' : 'secondary'\"\r\n [outlined]=\"!viewingThroughCamera\"\r\n size=\"small\"\r\n [title]=\"t('scene3d.toolbar.camera_view_hint', 'Alterna tra la vista di editing e la vista dalla camera della scena')\"\r\n (onClick)=\"toggleCameraView()\">\r\n </p-button>\r\n }\r\n </div>\r\n\r\n <div class=\"scene3d-body\">\r\n\r\n <!-- Palette (sinistra) -->\r\n <div class=\"scene3d-palette\">\r\n <div class=\"scene3d-panel-title\">{{ t('scene3d.palette.title', 'Oggetti') }}</div>\r\n @for (item of paletteItems; track item.type) {\r\n <button\r\n type=\"button\"\r\n class=\"scene3d-palette-item\"\r\n [disabled]=\"!item.fromPalette\"\r\n [title]=\"item.label\"\r\n (click)=\"addFromPalette(item.type)\">\r\n <i [class]=\"item.icon\"></i>\r\n <span class=\"scene3d-palette-label\">{{ item.label }}</span>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Viewport (centro) -->\r\n <div #viewportContainer class=\"scene3d-viewport\">\r\n <!-- HUD path tracing: build, sample progressivi, scala risoluzione -->\r\n @if (ptBuilding || pathTracingEnabled) {\r\n <div class=\"scene3d-pt-hud\">\r\n @if (ptBuilding) {\r\n <span>{{ t('scene3d.pt.building', 'Costruzione BVH...') }}</span>\r\n } @else {\r\n <span>{{ ptSamples }} {{ t('scene3d.pt.samples_suffix', 'sample') }}</span>\r\n <label class=\"scene3d-pt-scale\">\r\n {{ t('scene3d.pt.scale', 'Risoluzione') }}\r\n <input type=\"range\" min=\"0.25\" max=\"1\" step=\"0.25\"\r\n [(ngModel)]=\"ptResolutionScale\" (ngModelChange)=\"onPtResolutionScaleChange()\" />\r\n </label>\r\n <button type=\"button\" class=\"scene3d-pt-exit\" (click)=\"togglePathTracing()\">\r\n {{ t('scene3d.pt.exit', 'Torna al realtime') }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Popup anteprima camera: real-time, trascinabile dall'header -->\r\n @if (cameraPreviewEnabled) {\r\n <div class=\"scene3d-cam-preview\" [style.left.px]=\"previewPos.x\" [style.top.px]=\"previewPos.y\">\r\n <div class=\"scene3d-cam-preview-header\" (mousedown)=\"startPreviewDrag($event)\">\r\n <span class=\"scene3d-cam-preview-title\">{{ t('scene3d.preview.title', 'Anteprima camera') }}</span>\r\n <button type=\"button\" class=\"scene3d-cam-preview-close\" (click)=\"toggleCameraPreview()\" (mousedown)=\"$event.stopPropagation()\">\u00D7</button>\r\n </div>\r\n <div #previewCanvasHost class=\"scene3d-cam-preview-body\"></div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Property panel (destra) -->\r\n <div class=\"scene3d-properties\">\r\n <div class=\"scene3d-panel-title\">{{ t('scene3d.props.title', 'Proprieta') }}</div>\r\n\r\n @if (selectedObjectUuid) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-prop-name\">{{ t('scene3d.props.name', 'Nome') }}</label>\r\n <input id=\"scene3d-prop-name\" type=\"text\" [(ngModel)]=\"selectedObjectName\"\r\n (ngModelChange)=\"renameSelectedObject($event)\"\r\n [placeholder]=\"t('scene3d.props.name_placeholder', 'Nome oggetto')\" />\r\n </div>\r\n\r\n <!-- ID della camera: e' il camera_id per l'URL del viewer\r\n (#/scene3d_viewer/<scene_key>/<camera_id>). Persiste nel salvataggio. -->\r\n @if (selectedIsCamera) {\r\n <div class=\"scene3d-prop-group\">\r\n <span class=\"scene3d-prop-label\">{{ t('scene3d.props.camera_id', 'ID camera (viewer)') }}</span>\r\n <div class=\"scene3d-prop-readonly\">{{ selectedObjectUuid }}</div>\r\n </div>\r\n\r\n <!-- Anteprima real-time in popup trascinabile dal punto di vista della camera -->\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input\r\n type=\"checkbox\"\r\n [ngModel]=\"cameraPreviewEnabled && previewCameraUuid === selectedObjectUuid\"\r\n (ngModelChange)=\"toggleCameraPreview()\" />\r\n {{ t('scene3d.props.camera_preview', 'Anteprima camera') }}\r\n </label>\r\n </div>\r\n }\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-pos-x\">{{ t('scene3d.props.position', 'Posizione') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-pos-x\" type=\"number\" step=\"0.1\" [(ngModel)]=\"propPosition.x\" (ngModelChange)=\"onTransformInputChange()\" />\r\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propPosition.y\" (ngModelChange)=\"onTransformInputChange()\" />\r\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propPosition.z\" (ngModelChange)=\"onTransformInputChange()\" />\r\n </div>\r\n </div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-rot-x\">{{ t('scene3d.props.rotation', 'Rotazione (deg)') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-rot-x\" type=\"number\" step=\"1\" [(ngModel)]=\"propRotation.x\" (ngModelChange)=\"onTransformInputChange()\" />\r\n <input type=\"number\" step=\"1\" [(ngModel)]=\"propRotation.y\" (ngModelChange)=\"onTransformInputChange()\" />\r\n <input type=\"number\" step=\"1\" [(ngModel)]=\"propRotation.z\" (ngModelChange)=\"onTransformInputChange()\" />\r\n </div>\r\n </div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-scale-x\">{{ t('scene3d.props.scale', 'Scala') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-scale-x\" type=\"number\" step=\"0.1\" [(ngModel)]=\"propScale.x\" (ngModelChange)=\"onTransformInputChange()\" />\r\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propScale.y\" (ngModelChange)=\"onTransformInputChange()\" />\r\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propScale.z\" (ngModelChange)=\"onTransformInputChange()\" />\r\n </div>\r\n </div>\r\n\r\n @if (selectedHasMaterialColor) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-prop-color\">{{ t('scene3d.props.color', 'Colore') }}</label>\r\n <input id=\"scene3d-prop-color\" type=\"color\" [(ngModel)]=\"propColor\" (ngModelChange)=\"onColorInputChange()\" />\r\n </div>\r\n }\r\n\r\n <!-- castShadow per-oggetto (default ON alla creazione, disattivabile) -->\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"propCastShadow\" (ngModelChange)=\"onCastShadowInputChange()\" />\r\n {{ t('scene3d.props.cast_shadow', 'Proietta ombra') }}\r\n </label>\r\n </div>\r\n\r\n <!-- Editor materiale PBR (MeshStandardMaterial) -->\r\n @if (selectedHasStandardMaterial) {\r\n <div class=\"scene3d-material-section\">\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.material.title', 'Materiale') }}</div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-metalness\">{{ t('scene3d.material.metalness', 'Metalness') }} ({{ propMaterial.metalness }})</label>\r\n <input id=\"scene3d-mat-metalness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.metalness\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-roughness\">{{ t('scene3d.material.roughness', 'Roughness') }} ({{ propMaterial.roughness }})</label>\r\n <input id=\"scene3d-mat-roughness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.roughness\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-opacity\">{{ t('scene3d.material.opacity', 'Opacita\\'') }} ({{ propMaterial.opacity }})</label>\r\n <input id=\"scene3d-mat-opacity\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.opacity\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-emissive\">{{ t('scene3d.material.emissive', 'Emissivo') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-mat-emissive\" type=\"color\" [(ngModel)]=\"propMaterial.emissive\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n <input type=\"number\" step=\"0.1\" min=\"0\" [(ngModel)]=\"propMaterial.emissiveIntensity\" (ngModelChange)=\"onMaterialInputChange()\"\r\n [title]=\"t('scene3d.material.emissive_intensity', 'Intensita\\' emissiva')\" />\r\n </div>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-side\">{{ t('scene3d.material.side', 'Facce') }}</label>\r\n <select id=\"scene3d-mat-side\" class=\"scene3d-material-select\" [(ngModel)]=\"propMaterial.side\" (ngModelChange)=\"onMaterialInputChange()\">\r\n <option value=\"front\">{{ t('scene3d.material.side_front', 'Fronte') }}</option>\r\n <option value=\"back\">{{ t('scene3d.material.side_back', 'Retro') }}</option>\r\n <option value=\"double\">{{ t('scene3d.material.side_double', 'Doppia') }}</option>\r\n </select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"propMaterial.wireframe\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n {{ t('scene3d.material.wireframe', 'Wireframe') }}\r\n </label>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"propMaterial.flatShading\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n {{ t('scene3d.material.flat', 'Flat shading') }}\r\n </label>\r\n </div>\r\n\r\n <!-- Vetro fisico (transmission): rifrazione reale di cio' che sta dietro.\r\n In Render PT l'assorbimento (colore+distanza) tinge anche la LUCE\r\n che attraversa il vetro sugli altri oggetti (Beer-Lambert). -->\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.material.glass', 'Vetro (transmission)') }}</div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-transmission\">{{ t('scene3d.material.transmission', 'Trasmissione') }} ({{ propMaterial.transmission }})</label>\r\n <input id=\"scene3d-mat-transmission\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.transmission\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n @if (propMaterial.transmission > 0) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-ior\">{{ t('scene3d.material.ior', 'IOR') }} ({{ propMaterial.ior }})</label>\r\n <input id=\"scene3d-mat-ior\" type=\"range\" min=\"1\" max=\"2.33\" step=\"0.01\" [(ngModel)]=\"propMaterial.ior\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-thickness\">{{ t('scene3d.material.thickness', 'Spessore') }} ({{ propMaterial.thickness }})</label>\r\n <input id=\"scene3d-mat-thickness\" type=\"range\" min=\"0\" max=\"5\" step=\"0.1\" [(ngModel)]=\"propMaterial.thickness\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n <!-- Assorbimento volumetrico: colora la luce trasmessa (visibile in PT). -->\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-attenuation-color\">{{ t('scene3d.material.attenuation_color', 'Colore assorbimento') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-mat-attenuation-color\" type=\"color\" [(ngModel)]=\"propMaterial.attenuationColor\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-attenuation-distance\">{{ t('scene3d.material.attenuation_distance', 'Distanza assorbimento') }} ({{ propMaterial.attenuationDistance }})</label>\r\n <input id=\"scene3d-mat-attenuation-distance\" type=\"range\" min=\"0.05\" max=\"10\" step=\"0.05\" [(ngModel)]=\"propMaterial.attenuationDistance\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.material.glass_hint', 'Con transmission tieni Opacita\\' = 1 (la trasparenza la gestisce il vetro). Il colore assorbimento tinge la luce filtrata: visibile in Render PT.') }}\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Shader GLSL custom -->\r\n @if (selectedIsMesh) {\r\n <div class=\"scene3d-material-section\">\r\n <div class=\"scene3d-panel-subtitle scene3d-shader-toggle\" (click)=\"shaderSectionOpen = !shaderSectionOpen\">\r\n {{ t('scene3d.shader.title', 'Shader custom (GLSL)') }} {{ shaderSectionOpen ? '\u25BE' : '\u25B8' }}\r\n </div>\r\n @if (shaderSectionOpen) {\r\n <!-- Effetto JSON (dual-backend): editor Monaco con schema, compilato\r\n in GLSL su WebGL e in node-material su WebGPU -->\r\n <div class=\"scene3d-anim-transport\">\r\n <p-button\r\n icon=\"pi pi-sliders-h\"\r\n [label]=\"t('scene3d.shader.effect_edit', 'Effetto JSON')\"\r\n size=\"small\"\r\n [severity]=\"selectedHasShaderEffect ? 'primary' : 'secondary'\"\r\n [title]=\"t('scene3d.shader.effect_tooltip', 'JSON con schema (completion/validazione in editor). Compilato in GLSL su WebGL e in node-material su WebGPU.')\"\r\n (onClick)=\"editShaderEffect()\">\r\n </p-button>\r\n @if (selectedHasShaderEffect) {\r\n <p-button\r\n icon=\"pi pi-undo\"\r\n [label]=\"t('scene3d.shader.effect_remove', 'Rimuovi effetto')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n (onClick)=\"removeShaderEffect()\">\r\n </p-button>\r\n }\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-shader-vertex\">{{ t('scene3d.shader.vertex', 'Vertex shader') }}</label>\r\n <textarea id=\"scene3d-shader-vertex\" class=\"scene3d-shader-src\" rows=\"6\" spellcheck=\"false\" [(ngModel)]=\"shaderVertexSrc\"></textarea>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-shader-fragment\">{{ t('scene3d.shader.fragment', 'Fragment shader') }}</label>\r\n <textarea id=\"scene3d-shader-fragment\" class=\"scene3d-shader-src\" rows=\"8\" spellcheck=\"false\" [(ngModel)]=\"shaderFragmentSrc\"></textarea>\r\n </div>\r\n <div class=\"scene3d-anim-transport\">\r\n <p-button\r\n icon=\"pi pi-check\"\r\n [label]=\"t('scene3d.shader.apply', 'Applica shader')\"\r\n size=\"small\"\r\n (onClick)=\"applyCustomShader()\">\r\n </p-button>\r\n @if (selectedHasCustomShader) {\r\n <p-button\r\n icon=\"pi pi-undo\"\r\n [label]=\"t('scene3d.shader.restore', 'Ripristina')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n (onClick)=\"restoreCustomShader()\">\r\n </p-button>\r\n }\r\n </div>\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.shader.hint', 'Uniform disponibile: uTime (secondi, animata). Errori GLSL in console.') }}\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- F5: Binding rotta scaffoldata + field-map + open-record.\r\n Nascosto per il Mesh Repeater: il repeater binda per-record (config sua). -->\r\n @if (!selectedIsRepeater) {\r\n <div class=\"scene3d-binding-section\">\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.binding.title', 'Binding rotta') }}</div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-route\">{{ t('scene3d.binding.route', 'Rotta') }}</label>\r\n <p-select\r\n inputId=\"scene3d-binding-route\"\r\n [options]=\"routeOptions\"\r\n [(ngModel)]=\"bindingRoute\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n [filter]=\"true\"\r\n [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.binding.route_placeholder', 'Seleziona rotta...')\"\r\n appendTo=\"body\"\r\n styleClass=\"scene3d-binding-select\"\r\n (onChange)=\"onBindingRouteChange()\">\r\n </p-select>\r\n </div>\r\n\r\n @if (selectedHasBinding) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-record-id\">{{ t('scene3d.binding.record_id', 'Record ID (opzionale)') }}</label>\r\n <input\r\n id=\"scene3d-binding-record-id\"\r\n type=\"text\"\r\n [(ngModel)]=\"bindingRecordId\"\r\n (ngModelChange)=\"onBindingDetailChange()\"\r\n [placeholder]=\"t('scene3d.binding.record_id_placeholder', 'Vuoto = binding di rotta')\" />\r\n </div>\r\n\r\n <div class=\"scene3d-field-map\">\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.binding.field_map', 'Mappa campi') }}</div>\r\n\r\n @if (bindingColumnsLoading) {\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.binding.columns_loading', 'Caricamento colonne...') }}</div>\r\n }\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-map-label\">{{ t('scene3d.binding.map_label', 'Etichetta') }}</label>\r\n <p-select\r\n inputId=\"scene3d-binding-map-label\"\r\n [options]=\"bindingColumnOptions\"\r\n [(ngModel)]=\"bindingFieldMap.label\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n [filter]=\"true\"\r\n [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.binding.column_placeholder', 'Colonna...')\"\r\n appendTo=\"body\"\r\n styleClass=\"scene3d-binding-select\"\r\n (onChange)=\"onBindingDetailChange()\">\r\n </p-select>\r\n </div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-map-color\">{{ t('scene3d.binding.map_color', 'Colore') }}</label>\r\n <p-select\r\n inputId=\"scene3d-binding-map-color\"\r\n [options]=\"bindingColumnOptions\"\r\n [(ngModel)]=\"bindingFieldMap.color\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n [filter]=\"true\"\r\n [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.binding.column_placeholder', 'Colonna...')\"\r\n appendTo=\"body\"\r\n styleClass=\"scene3d-binding-select\"\r\n (onChange)=\"onBindingDetailChange()\">\r\n </p-select>\r\n </div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-map-visible\">{{ t('scene3d.binding.map_visible', 'Visibile') }}</label>\r\n <p-select\r\n inputId=\"scene3d-binding-map-visible\"\r\n [options]=\"bindingColumnOptions\"\r\n [(ngModel)]=\"bindingFieldMap.visible\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n [filter]=\"true\"\r\n [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.binding.column_placeholder', 'Colonna...')\"\r\n appendTo=\"body\"\r\n styleClass=\"scene3d-binding-select\"\r\n (onChange)=\"onBindingDetailChange()\">\r\n </p-select>\r\n </div>\r\n </div>\r\n\r\n <p-button\r\n icon=\"pi pi-external-link\"\r\n [label]=\"t('scene3d.binding.open_record', 'Apri record')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n styleClass=\"scene3d-open-record-btn\"\r\n (onClick)=\"openBoundRecord()\">\r\n </p-button>\r\n\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.binding.open_hint', 'Doppio click su un oggetto bindato per aprire la CRUD della rotta') }}\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Iter2 F2: animazioni / timeline (solo se il selezionato ha clip) -->\r\n @if (selectedHasAnimations) {\r\n <div class=\"scene3d-anim-section\">\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.anim.title', 'Animazioni') }}</div>\r\n\r\n @if (animClipOptions.length > 1) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-anim-clip\">{{ t('scene3d.anim.clip', 'Clip') }}</label>\r\n <p-select\r\n inputId=\"scene3d-anim-clip\"\r\n [options]=\"animClipOptions\"\r\n [(ngModel)]=\"selectedAnimClipIndex\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n appendTo=\"body\"\r\n styleClass=\"scene3d-binding-select\"\r\n (onChange)=\"onAnimClipChange()\">\r\n </p-select>\r\n </div>\r\n }\r\n\r\n <div class=\"scene3d-anim-transport\">\r\n <p-button\r\n icon=\"pi pi-play\"\r\n [title]=\"t('scene3d.anim.play', 'Play')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n [disabled]=\"animPlaying\"\r\n (onClick)=\"animPlay()\">\r\n </p-button>\r\n <p-button\r\n icon=\"pi pi-pause\"\r\n [title]=\"t('scene3d.anim.pause', 'Pausa')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n [disabled]=\"!animPlaying\"\r\n (onClick)=\"animPause()\">\r\n </p-button>\r\n <p-button\r\n icon=\"pi pi-stop\"\r\n [title]=\"t('scene3d.anim.stop', 'Stop')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n (onClick)=\"animStop()\">\r\n </p-button>\r\n <p-button\r\n icon=\"pi pi-trash\"\r\n [title]=\"t('scene3d.anim.delete_clip', 'Elimina clip')\"\r\n severity=\"danger\"\r\n size=\"small\"\r\n (onClick)=\"deleteSelectedAnimClip()\">\r\n </p-button>\r\n </div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-anim-time\">{{ t('scene3d.anim.time', 'Tempo (s)') }}</label>\r\n <input\r\n id=\"scene3d-anim-time\"\r\n type=\"range\"\r\n class=\"scene3d-anim-scrubber\"\r\n min=\"0\"\r\n [max]=\"animDuration\"\r\n step=\"0.01\"\r\n [(ngModel)]=\"animScrubTime\"\r\n (ngModelChange)=\"onAnimScrub()\" />\r\n <div class=\"scene3d-anim-time\">{{ animScrubTime }} / {{ animDuration }}</div>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Mesh Repeater (DATAREPEATER 3D): una mesh per record, propriet\u00E0 bindate -->\r\n @if (selectedIsRepeater) {\r\n <div class=\"scene3d-material-section\">\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.repeater.title', 'Mesh Repeater') }}</div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-route\">{{ t('scene3d.repeater.route', 'Rotta') }}</label>\r\n <p-select\r\n inputId=\"scene3d-repeater-route\"\r\n [options]=\"routeOptions\" [(ngModel)]=\"repeaterCfg.route\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.route_ph', 'Seleziona rotta...')\"\r\n appendTo=\"body\" styleClass=\"scene3d-binding-select\"\r\n (onChange)=\"onRepeaterRouteChange()\">\r\n </p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-max\">{{ t('scene3d.repeater.max', 'Max record') }}</label>\r\n <input id=\"scene3d-repeater-max\" type=\"number\" min=\"1\" step=\"10\" [(ngModel)]=\"repeaterCfg.maxRecords\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-mesh-source\">{{ t('scene3d.repeater.mesh_source', 'Sorgente mesh') }}</label>\r\n <select id=\"scene3d-repeater-mesh-source\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.meshSource.mode\" (ngModelChange)=\"onRepeaterConfigChange()\">\r\n <option value=\"fixed\">{{ t('scene3d.repeater.fixed', 'Fissa') }}</option>\r\n <option value=\"column\">{{ t('scene3d.repeater.from_column', 'Da colonna') }}</option>\r\n <option value=\"asset\">{{ t('scene3d.repeater.from_asset', 'Asset (colonna)') }}</option>\r\n </select>\r\n </div>\r\n @if (repeaterCfg.meshSource.mode === 'fixed') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-mesh-type\">{{ t('scene3d.repeater.mesh_type', 'Tipo mesh') }}</label>\r\n <select id=\"scene3d-repeater-mesh-type\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.meshSource.type\" (ngModelChange)=\"onRepeaterConfigChange()\">\r\n @for (o of repeaterMeshTypeOptions; track o.value) { <option [value]=\"o.value\">{{ o.label }}</option> }\r\n </select>\r\n </div>\r\n } @else {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-mesh-column\">{{ repeaterCfg.meshSource.mode === 'asset'\r\n ? t('scene3d.repeater.asset_column', 'Colonna asset (upload GLB/OBJ/STL)')\r\n : t('scene3d.repeater.mesh_column', 'Colonna tipo') }}</label>\r\n <p-select inputId=\"scene3d-repeater-mesh-column\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"repeaterCfg.meshSource.column\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n appendTo=\"body\" styleClass=\"scene3d-binding-select\" (onChange)=\"onRepeaterConfigChange()\"></p-select>\r\n </div>\r\n }\r\n\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.repeater.field_map', 'Mappa campi \u2192 colonne') }}</div>\r\n @for (f of repeaterFields; track f.key) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" [attr.for]=\"'scene3d-rep-field-' + f.key\">{{ f.label }}</label>\r\n <p-select [inputId]=\"'scene3d-rep-field-' + f.key\" [options]=\"bindingColumnOptions\"\r\n [ngModel]=\"getFieldMapCol(f.key)\" (ngModelChange)=\"setFieldMapCol(f.key, $event)\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\"\r\n appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n }\r\n\r\n <!-- Opzioni texture (visibili solo con la colonna `texture` mappata):\r\n testo\u2192texture testuale, upload/img\u2192immagine, + modo di applicazione. -->\r\n @if (getFieldMapCol('texture')) {\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.repeater.texture_opts', 'Texture (colonna)') }}</div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-source\">{{ t('scene3d.repeater.texture_source', 'Sorgente') }}</label>\r\n <select id=\"scene3d-texture-source\" class=\"scene3d-material-select\" [(ngModel)]=\"textureApply.sourceKind\" (ngModelChange)=\"onTextureApplyChange()\">\r\n <option value=\"auto\">{{ t('scene3d.repeater.texture_auto', 'Auto (dal tipo colonna)') }}</option>\r\n <option value=\"text\">{{ t('scene3d.repeater.texture_text', 'Testo \u2192 texture') }}</option>\r\n <option value=\"image\">{{ t('scene3d.repeater.texture_image', 'Immagine / upload') }}</option>\r\n </select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-apply\">{{ t('scene3d.repeater.texture_apply', 'Applicazione') }}</label>\r\n <select id=\"scene3d-texture-apply\" class=\"scene3d-material-select\" [(ngModel)]=\"textureApply.wrap\" (ngModelChange)=\"onTextureApplyChange()\">\r\n <option value=\"stretch\">{{ t('scene3d.repeater.texture_stretch', 'Stretch (adatta)') }}</option>\r\n <option value=\"repeat\">{{ t('scene3d.repeater.texture_repeat', 'Ripeti') }}</option>\r\n <option value=\"mirror\">{{ t('scene3d.repeater.texture_mirror', 'Ripeti a specchio') }}</option>\r\n </select>\r\n </div>\r\n @if (textureApply.wrap !== 'stretch') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-repeat-x\">{{ t('scene3d.repeater.texture_repeat_xy', 'Ripetizioni X / Y') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-texture-repeat-x\" type=\"number\" step=\"0.5\" min=\"0.1\" [(ngModel)]=\"textureApply.repeatX\" (ngModelChange)=\"onTextureApplyChange()\" />\r\n <input type=\"number\" step=\"0.5\" min=\"0.1\" [(ngModel)]=\"textureApply.repeatY\" (ngModelChange)=\"onTextureApplyChange()\" />\r\n </div>\r\n </div>\r\n }\r\n @if (textureApply.sourceKind !== 'image') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-text-bg\">{{ t('scene3d.repeater.texture_text_colors', 'Colori testo (sfondo / testo)') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-texture-text-bg\" type=\"color\" [(ngModel)]=\"textureApply.textBg\" (ngModelChange)=\"onTextureApplyChange()\" />\r\n <input type=\"color\" [(ngModel)]=\"textureApply.textFg\" (ngModelChange)=\"onTextureApplyChange()\" />\r\n </div>\r\n </div>\r\n }\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.repeater.texture_hint', 'Dopo aver cambiato le opzioni premi Rigenera per riapplicare le texture.') }}\r\n </div>\r\n }\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-layout\">{{ t('scene3d.repeater.layout', 'Layout (senza colonne posizione)') }}</label>\r\n <select id=\"scene3d-repeater-layout\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.layout.mode\" (ngModelChange)=\"onRepeaterLayoutModeChange()\">\r\n <optgroup [label]=\"t('scene3d.repeater.layout_simple', 'Semplici')\">\r\n <option value=\"grid\">Grid</option>\r\n <option value=\"cube\">{{ t('scene3d.repeater.cube', 'Cubo') }}</option>\r\n <option value=\"line\">{{ t('scene3d.repeater.line', 'Linea') }}</option>\r\n <option value=\"circle\">{{ t('scene3d.repeater.circle', 'Cerchio') }}</option>\r\n </optgroup>\r\n <optgroup [label]=\"t('scene3d.repeater.layout_archetypes', 'Archetipi')\">\r\n <option value=\"carousel\">{{ t('scene3d.repeater.carousel', 'Carousel 3D') }}</option>\r\n <option value=\"chart\">{{ t('scene3d.repeater.chart', 'Chart 3D') }}</option>\r\n <option value=\"surface\">{{ t('scene3d.repeater.surface', 'Surface / Heatmap') }}</option>\r\n <option value=\"gauge\">{{ t('scene3d.repeater.gauge', 'Gauge / KPI') }}</option>\r\n <option value=\"nodegraph\">{{ t('scene3d.repeater.nodegraph', 'Node graph') }}</option>\r\n <option value=\"tree\">{{ t('scene3d.repeater.tree', 'Albero (tree)') }}</option>\r\n <option value=\"map\">{{ t('scene3d.repeater.map', 'Mappa (pin)') }}</option>\r\n <option value=\"warehouse\">{{ t('scene3d.repeater.warehouse', 'Magazzino (scaffali)') }}</option>\r\n <option value=\"kanban\">{{ t('scene3d.repeater.kanban', 'Kanban 3D') }}</option>\r\n <option value=\"calendar\">{{ t('scene3d.repeater.calendar', 'Calendario') }}</option>\r\n <option value=\"stacked\">{{ t('scene3d.repeater.stacked', 'Barre impilate') }}</option>\r\n </optgroup>\r\n </select>\r\n </div>\r\n\r\n <!-- Archetipo Carousel 3D: sezioni collassabili Carousel / Disco / Frecce. -->\r\n @if (repeaterCfg.layout.mode === 'carousel') {\r\n <!-- Sezione Carousel (generale) -->\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('carousel')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('carousel')\" [class.pi-chevron-down]=\"!isSectionCollapsed('carousel')\"></i>\r\n {{ t('scene3d.repeater.carousel', 'Carousel 3D') }}\r\n </div>\r\n @if (!isSectionCollapsed('carousel')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-visible\">{{ t('scene3d.repeater.carousel_visible', 'Item visibili') }} ({{ carouselCfg.visible }})</label>\r\n <input id=\"scene3d-carousel-visible\" type=\"range\" min=\"1\" max=\"9\" step=\"1\" [(ngModel)]=\"carouselCfg.visible\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-radius\">{{ t('scene3d.repeater.carousel_radius', 'Curvatura (raggio)') }} ({{ carouselCfg.radius }})</label>\r\n <input id=\"scene3d-carousel-radius\" type=\"range\" min=\"2\" max=\"30\" step=\"0.5\" [(ngModel)]=\"carouselCfg.radius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-slot-angle\">{{ t('scene3d.repeater.carousel_slot_angle', 'Angolo tra slot') }} ({{ carouselCfg.slotAngleDeg }}\u00B0)</label>\r\n <input id=\"scene3d-carousel-slot-angle\" type=\"range\" min=\"8\" max=\"60\" step=\"1\" [(ngModel)]=\"carouselCfg.slotAngleDeg\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-focus-scale\">{{ t('scene3d.repeater.carousel_scales', 'Scala centro / lati') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-carousel-focus-scale\" type=\"number\" step=\"0.05\" min=\"0.1\" [(ngModel)]=\"carouselCfg.focusScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n <input type=\"number\" step=\"0.05\" min=\"0.1\" [(ngModel)]=\"carouselCfg.sideScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-mesh-y-offset\">{{ t('scene3d.repeater.carousel_mesh_y_offset', 'Offset verticale mesh') }} ({{ carouselCfg.meshYOffset ?? 0 }})</label>\r\n <input id=\"scene3d-carousel-mesh-y-offset\" type=\"range\" min=\"-5\" max=\"5\" step=\"0.1\" [(ngModel)]=\"carouselCfg.meshYOffset\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-mesh-rot-x\">{{ t('scene3d.repeater.carousel_mesh_rot', 'Rotazione mesh X / Y / Z (\u00B0)') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-carousel-mesh-rot-x\" type=\"number\" step=\"15\" [(ngModel)]=\"carouselCfg.meshRotX\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n <input type=\"number\" step=\"15\" [(ngModel)]=\"carouselCfg.meshRotY\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n <input type=\"number\" step=\"15\" [(ngModel)]=\"carouselCfg.meshRotZ\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"carouselCfg.showBase\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.carousel_base', 'Base') }}\r\n </label>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"carouselCfg.showArrows\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.carousel_arrows', 'Frecce nav') }}\r\n </label>\r\n </div>\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.repeater.carousel_hint', 'Nel viewer le frecce 3D scorrono i record (item centrale a fuoco). Premi Rigenera dopo le modifiche.') }}\r\n </div>\r\n }\r\n\r\n <!-- Sezione Disco -->\r\n @if (carouselCfg.showBase) {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('carousel_disc')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('carousel_disc')\" [class.pi-chevron-down]=\"!isSectionCollapsed('carousel_disc')\"></i>\r\n {{ t('scene3d.repeater.carousel_disc', 'Disco') }}\r\n </div>\r\n @if (!isSectionCollapsed('carousel_disc')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-disc-size\">{{ t('scene3d.repeater.carousel_disc_size', 'Dimensione disco') }} ({{ carouselCfg.baseSize }})</label>\r\n <input id=\"scene3d-carousel-disc-size\" type=\"range\" min=\"0.2\" max=\"1.5\" step=\"0.02\" [(ngModel)]=\"carouselCfg.baseSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-color\">{{ t('scene3d.repeater.carousel_base_color', 'Colore base') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-carousel-base-color\" type=\"color\" [(ngModel)]=\"carouselCfg.baseColor\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-metalness\">{{ t('scene3d.repeater.carousel_metalness', 'Metalness') }} ({{ carouselCfg.baseMetalness }})</label>\r\n <input id=\"scene3d-carousel-base-metalness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.baseMetalness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-roughness\">{{ t('scene3d.repeater.carousel_roughness', 'Roughness') }} ({{ carouselCfg.baseRoughness }})</label>\r\n <input id=\"scene3d-carousel-base-roughness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.baseRoughness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-clearcoat\">{{ t('scene3d.repeater.carousel_clearcoat', 'Clearcoat') }} ({{ carouselCfg.baseClearcoat }})</label>\r\n <input id=\"scene3d-carousel-base-clearcoat\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.baseClearcoat\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n }\r\n }\r\n\r\n <!-- Sezione Frecce -->\r\n @if (carouselCfg.showArrows) {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('carousel_arrows')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('carousel_arrows')\" [class.pi-chevron-down]=\"!isSectionCollapsed('carousel_arrows')\"></i>\r\n {{ t('scene3d.repeater.carousel_arrows', 'Frecce nav') }}\r\n </div>\r\n @if (!isSectionCollapsed('carousel_arrows')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-size\">{{ t('scene3d.repeater.carousel_arrow_size', 'Dimensione frecce') }} ({{ carouselCfg.arrowSize }})</label>\r\n <input id=\"scene3d-carousel-arrow-size\" type=\"range\" min=\"0.3\" max=\"2.5\" step=\"0.05\" [(ngModel)]=\"carouselCfg.arrowSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-color\">{{ t('scene3d.repeater.carousel_arrow_color', 'Colore frecce') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-carousel-arrow-color\" type=\"color\" [(ngModel)]=\"carouselCfg.arrowColor\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-emissive\">{{ t('scene3d.repeater.carousel_arrow_glow', 'Bagliore (emissive)') }} ({{ carouselCfg.arrowEmissive }})</label>\r\n <input id=\"scene3d-carousel-arrow-emissive\" type=\"range\" min=\"0\" max=\"3\" step=\"0.05\" [(ngModel)]=\"carouselCfg.arrowEmissive\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-metalness\">{{ t('scene3d.repeater.carousel_metalness', 'Metalness') }} ({{ carouselCfg.arrowMetalness }})</label>\r\n <input id=\"scene3d-carousel-arrow-metalness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.arrowMetalness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-roughness\">{{ t('scene3d.repeater.carousel_roughness', 'Roughness') }} ({{ carouselCfg.arrowRoughness }})</label>\r\n <input id=\"scene3d-carousel-arrow-roughness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.arrowRoughness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-clearcoat\">{{ t('scene3d.repeater.carousel_clearcoat', 'Clearcoat') }} ({{ carouselCfg.arrowClearcoat }})</label>\r\n <input id=\"scene3d-carousel-arrow-clearcoat\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.arrowClearcoat\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n }\r\n }\r\n }\r\n\r\n <!-- Archetipo Chart 3D: bar->cilindri, pie->donut(torus), area->ribbon. -->\r\n @if (repeaterCfg.layout.mode === 'chart') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('chart')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('chart')\" [class.pi-chevron-down]=\"!isSectionCollapsed('chart')\"></i>\r\n {{ t('scene3d.repeater.chart', 'Chart 3D') }}\r\n </div>\r\n @if (!isSectionCollapsed('chart')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-type\">{{ t('scene3d.repeater.chart_type', 'Tipo chart') }}</label>\r\n <select id=\"scene3d-chart-type\" class=\"scene3d-material-select\" [(ngModel)]=\"chartCfg.type\" (ngModelChange)=\"onRepeaterConfigChange()\">\r\n <option value=\"bar\">{{ t('scene3d.repeater.chart_bar', 'Barre (cilindri)') }}</option>\r\n <option value=\"pie\">{{ t('scene3d.repeater.chart_pie', 'Torta (donut)') }}</option>\r\n <option value=\"area\">{{ t('scene3d.repeater.chart_area', 'Area (line/spline)') }}</option>\r\n <option value=\"funnel\">{{ t('scene3d.repeater.chart_funnel', 'Funnel (coni)') }}</option>\r\n <option value=\"radar\">{{ t('scene3d.repeater.chart_radar', 'Radar (stella)') }}</option>\r\n <option value=\"scatter\">{{ t('scene3d.repeater.chart_scatter', 'Scatter / bolle') }}</option>\r\n </select>\r\n </div>\r\n @if (chartCfg.type !== 'scatter') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\r\n <p-select inputId=\"scene3d-chart-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n }\r\n @if (chartCfg.type === 'scatter') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-x\">{{ t('scene3d.repeater.chart_x', 'Colonna X') }}</label>\r\n <p-select inputId=\"scene3d-chart-x\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.xColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-y\">{{ t('scene3d.repeater.chart_y', 'Colonna Y (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-chart-y\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.yColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-z\">{{ t('scene3d.repeater.chart_z', 'Colonna Z (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-chart-z\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.zColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-size\">{{ t('scene3d.repeater.chart_size', 'Colonna dimensione (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-chart-size\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.sizeColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.chart_size_ph', 'Costante')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n }\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-color-col\">{{ t('scene3d.repeater.chart_color_col', 'Colonna colore (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-chart-color-col\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.colorColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.chart_palette', 'Palette automatica')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n @if (chartCfg.type === 'bar' || chartCfg.type === 'area' || chartCfg.type === 'funnel' || chartCfg.type === 'scatter') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-height\">{{ chartCfg.type === 'scatter' ? t('scene3d.repeater.chart_span', 'Estensione') : t('scene3d.repeater.chart_height', 'Altezza') }} ({{ chartCfg.height }})</label>\r\n <input id=\"scene3d-chart-height\" type=\"range\" min=\"1\" max=\"20\" step=\"0.5\" [(ngModel)]=\"chartCfg.height\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n }\r\n @if (chartCfg.type === 'bar' || chartCfg.type === 'area') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-spacing\">{{ t('scene3d.repeater.chart_spacing', 'Passo') }} ({{ chartCfg.spacing }})</label>\r\n <input id=\"scene3d-chart-spacing\" type=\"range\" min=\"0.5\" max=\"5\" step=\"0.1\" [(ngModel)]=\"chartCfg.spacing\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n }\r\n @if (chartCfg.type === 'bar' || chartCfg.type === 'scatter') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-bar-radius\">{{ chartCfg.type === 'scatter' ? t('scene3d.repeater.chart_bubble', 'Raggio bolla') : t('scene3d.repeater.chart_bar_radius', 'Raggio barra') }} ({{ chartCfg.barRadius }})</label>\r\n <input id=\"scene3d-chart-bar-radius\" type=\"range\" min=\"0.1\" max=\"2\" step=\"0.05\" [(ngModel)]=\"chartCfg.barRadius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n }\r\n @if (chartCfg.type === 'pie' || chartCfg.type === 'funnel' || chartCfg.type === 'radar') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-radius\">{{ t('scene3d.repeater.chart_radius', 'Raggio') }} ({{ chartCfg.radius }})</label>\r\n <input id=\"scene3d-chart-radius\" type=\"range\" min=\"1\" max=\"20\" step=\"0.5\" [(ngModel)]=\"chartCfg.radius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n }\r\n @if (chartCfg.type === 'pie' || chartCfg.type === 'area') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-thickness\">{{ chartCfg.type === 'pie' ? t('scene3d.repeater.chart_tube', 'Spessore donut') : t('scene3d.repeater.chart_depth', 'Profondita\\' area') }} ({{ chartCfg.thickness }})</label>\r\n <input id=\"scene3d-chart-thickness\" type=\"range\" min=\"0.1\" max=\"5\" step=\"0.1\" [(ngModel)]=\"chartCfg.thickness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n }\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.chart_labels', 'Etichette') }}\r\n </label>\r\n @if (chartCfg.type === 'bar' || chartCfg.type === 'area') {\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.showAxes\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.chart_axes', 'Assi') }}\r\n </label>\r\n }\r\n @if (chartCfg.type === 'bar' || chartCfg.type === 'pie') {\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.showLegend\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.chart_legend', 'Legenda') }}\r\n </label>\r\n }\r\n @if (chartCfg.type === 'area') {\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.smooth\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.chart_smooth', 'Smoothing spline') }}\r\n </label>\r\n }\r\n </div>\r\n @if (chartCfg.showLabels) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-label-col\">{{ t('scene3d.repeater.chart_label_col', 'Colonna etichetta (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-chart-label-col\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.chart_label_val', 'Usa il valore')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n }\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.repeater.chart_hint', 'La colonna valore guida l\\'altezza (barre/area) o la quota (torta). Premi Rigenera dopo le modifiche.') }}\r\n </div>\r\n }\r\n }\r\n\r\n <!-- Archetipo Surface / Heatmap (matrice riga\u00D7colonna\u00D7valore) -->\r\n @if (repeaterCfg.layout.mode === 'surface') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('surface')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('surface')\" [class.pi-chevron-down]=\"!isSectionCollapsed('surface')\"></i>\r\n {{ t('scene3d.repeater.surface', 'Surface / Heatmap') }}\r\n </div>\r\n @if (!isSectionCollapsed('surface')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-row\">{{ t('scene3d.repeater.surf_row', 'Colonna riga') }}</label>\r\n <p-select inputId=\"scene3d-surf-row\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"surfaceCfg.rowColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-col\">{{ t('scene3d.repeater.surf_col', 'Colonna colonna') }}</label>\r\n <p-select inputId=\"scene3d-surf-col\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"surfaceCfg.colColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\r\n <p-select inputId=\"scene3d-surf-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"surfaceCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-height\">{{ t('scene3d.repeater.surf_height', 'Scala altezza') }} ({{ surfaceCfg.heightScale }})</label>\r\n <input id=\"scene3d-surf-height\" type=\"range\" min=\"1\" max=\"20\" step=\"0.5\" [(ngModel)]=\"surfaceCfg.heightScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-cell\">{{ t('scene3d.repeater.surf_cell', 'Dimensione cella') }} ({{ surfaceCfg.cellSize }})</label>\r\n <input id=\"scene3d-surf-cell\" type=\"range\" min=\"0.4\" max=\"4\" step=\"0.1\" [(ngModel)]=\"surfaceCfg.cellSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-color-low\">{{ t('scene3d.repeater.surf_colors', 'Colori (basso / alto)') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-surf-color-low\" type=\"color\" [(ngModel)]=\"surfaceCfg.colorLow\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n <input type=\"color\" [(ngModel)]=\"surfaceCfg.colorHigh\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n </div>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"surfaceCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.chart_labels', 'Etichette') }}\r\n </label>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.surf_hint', 'Riga \u00D7 colonna = celle; valore \u2192 altezza e colore. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n <!-- Archetipo Gauge / KPI (valore singolo dal primo record) -->\r\n @if (repeaterCfg.layout.mode === 'gauge') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('gauge')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('gauge')\" [class.pi-chevron-down]=\"!isSectionCollapsed('gauge')\"></i>\r\n {{ t('scene3d.repeater.gauge', 'Gauge / KPI') }}\r\n </div>\r\n @if (!isSectionCollapsed('gauge')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\r\n <p-select inputId=\"scene3d-gauge-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"gaugeCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-label\">{{ t('scene3d.repeater.gauge_label', 'Colonna etichetta (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-gauge-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"gaugeCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_label_val', 'Usa il valore')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-min\">{{ t('scene3d.repeater.gauge_range', 'Min / Max') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-gauge-min\" type=\"number\" step=\"1\" [(ngModel)]=\"gaugeCfg.min\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n <input type=\"number\" step=\"1\" [(ngModel)]=\"gaugeCfg.max\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-radius\">{{ t('scene3d.repeater.chart_radius', 'Raggio') }} ({{ gaugeCfg.radius }})</label>\r\n <input id=\"scene3d-gauge-radius\" type=\"range\" min=\"1\" max=\"12\" step=\"0.5\" [(ngModel)]=\"gaugeCfg.radius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.gauge_hint', 'Usa il valore del PRIMO record. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n <!-- Archetipi Node-graph / Tree (gerarchia id \u2192 parent_id) -->\r\n @if (repeaterCfg.layout.mode === 'nodegraph' || repeaterCfg.layout.mode === 'tree') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('graph')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('graph')\" [class.pi-chevron-down]=\"!isSectionCollapsed('graph')\"></i>\r\n {{ repeaterCfg.layout.mode === 'tree' ? t('scene3d.repeater.tree', 'Albero (tree)') : t('scene3d.repeater.nodegraph', 'Node graph') }}\r\n </div>\r\n @if (!isSectionCollapsed('graph')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-id\">{{ t('scene3d.repeater.graph_id', 'Colonna ID') }}</label>\r\n <p-select inputId=\"scene3d-graph-id\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.idColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-parent\">{{ t('scene3d.repeater.graph_parent', 'Colonna padre') }}</label>\r\n <p-select inputId=\"scene3d-graph-parent\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.parentColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-label\">{{ t('scene3d.repeater.graph_label', 'Colonna etichetta') }}</label>\r\n <p-select inputId=\"scene3d-graph-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-value\">{{ t('scene3d.repeater.graph_value', 'Colonna valore (dim. nodo)') }}</label>\r\n <p-select inputId=\"scene3d-graph-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_palette', 'Palette automatica')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-nodesize\">{{ t('scene3d.repeater.graph_nodesize', 'Dimensione nodo') }} ({{ graphCfg.nodeSize }})</label>\r\n <input id=\"scene3d-graph-nodesize\" type=\"range\" min=\"0.2\" max=\"2\" step=\"0.05\" [(ngModel)]=\"graphCfg.nodeSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-spacing\">{{ t('scene3d.repeater.graph_spacing', 'Spaziatura') }} ({{ graphCfg.spacing }})</label>\r\n <input id=\"scene3d-graph-spacing\" type=\"range\" min=\"1\" max=\"10\" step=\"0.5\" [(ngModel)]=\"graphCfg.spacing\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-linkcolor\">{{ t('scene3d.repeater.graph_linkcolor', 'Colore archi') }}</label>\r\n <div class=\"scene3d-vector-row\"><input id=\"scene3d-graph-linkcolor\" type=\"color\" [(ngModel)]=\"graphCfg.linkColor\" (ngModelChange)=\"onRepeaterConfigChange()\" /></div>\r\n </div>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"graphCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.chart_labels', 'Etichette') }}\r\n </label>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.graph_hint', 'ID + padre definiscono la gerarchia; valore \u2192 dimensione nodo. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n <!-- Archetipo Map-pins (lat/lon \u2192 pin su piano) -->\r\n @if (repeaterCfg.layout.mode === 'map') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('map')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('map')\" [class.pi-chevron-down]=\"!isSectionCollapsed('map')\"></i>\r\n {{ t('scene3d.repeater.map', 'Mappa (pin)') }}\r\n </div>\r\n @if (!isSectionCollapsed('map')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-lat\">{{ t('scene3d.repeater.map_lat', 'Colonna latitudine') }}</label>\r\n <p-select inputId=\"scene3d-map-lat\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.latColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-lon\">{{ t('scene3d.repeater.map_lon', 'Colonna longitudine') }}</label>\r\n <p-select inputId=\"scene3d-map-lon\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.lonColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\r\n <p-select inputId=\"scene3d-map-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_size_ph', 'Costante')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-label\">{{ t('scene3d.repeater.map_label', 'Colonna etichetta (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-map-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-size\">{{ t('scene3d.repeater.map_size', 'Dimensione mappa') }} ({{ mapCfg.planeSize }})</label>\r\n <input id=\"scene3d-map-size\" type=\"range\" min=\"6\" max=\"50\" step=\"1\" [(ngModel)]=\"mapCfg.planeSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-pincolor\">{{ t('scene3d.repeater.map_pincolor', 'Colore pin (senza valore)') }}</label>\r\n <div class=\"scene3d-vector-row\"><input id=\"scene3d-map-pincolor\" type=\"color\" [(ngModel)]=\"mapCfg.pinColor\" (ngModelChange)=\"onRepeaterConfigChange()\" /></div>\r\n </div>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"mapCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.map_hint', 'Lat/lon \u2192 pin sul piano; valore \u2192 altezza/colore. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n <!-- Archetipo Warehouse-shelf (corsia \u00D7 ripiano \u00D7 quantita') -->\r\n @if (repeaterCfg.layout.mode === 'warehouse') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('warehouse')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('warehouse')\" [class.pi-chevron-down]=\"!isSectionCollapsed('warehouse')\"></i>\r\n {{ t('scene3d.repeater.warehouse', 'Magazzino (scaffali)') }}\r\n </div>\r\n @if (!isSectionCollapsed('warehouse')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-aisle\">{{ t('scene3d.repeater.wh_aisle', 'Colonna corsia') }}</label>\r\n <p-select inputId=\"scene3d-wh-aisle\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.aisleColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-shelf\">{{ t('scene3d.repeater.wh_shelf', 'Colonna ripiano') }}</label>\r\n <p-select inputId=\"scene3d-wh-shelf\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.shelfColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-qty\">{{ t('scene3d.repeater.wh_qty', 'Colonna quantit\u00E0') }}</label>\r\n <p-select inputId=\"scene3d-wh-qty\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.qtyColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-label\">{{ t('scene3d.repeater.wh_label', 'Colonna articolo (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-wh-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-box\">{{ t('scene3d.repeater.wh_box', 'Dimensione scatola') }} ({{ warehouseCfg.boxSize }})</label>\r\n <input id=\"scene3d-wh-box\" type=\"range\" min=\"0.4\" max=\"3\" step=\"0.1\" [(ngModel)]=\"warehouseCfg.boxSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"warehouseCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.wh_hint', 'Corsia \u00D7 ripiano = posizione; quantit\u00E0 \u2192 altezza scatola. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n <!-- Archetipo Kanban 3D (stato \u2192 colonne di card) -->\r\n @if (repeaterCfg.layout.mode === 'kanban') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('kanban')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('kanban')\" [class.pi-chevron-down]=\"!isSectionCollapsed('kanban')\"></i>\r\n {{ t('scene3d.repeater.kanban', 'Kanban 3D') }}\r\n </div>\r\n @if (!isSectionCollapsed('kanban')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-kb-status\">{{ t('scene3d.repeater.kb_status', 'Colonna stato') }}</label>\r\n <p-select inputId=\"scene3d-kb-status\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"kanbanCfg.statusColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-kb-label\">{{ t('scene3d.repeater.kb_label', 'Colonna titolo (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-kb-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"kanbanCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-kb-card\">{{ t('scene3d.repeater.kb_card', 'Larghezza card') }} ({{ kanbanCfg.cardWidth }})</label>\r\n <input id=\"scene3d-kb-card\" type=\"range\" min=\"0.8\" max=\"4\" step=\"0.1\" [(ngModel)]=\"kanbanCfg.cardWidth\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"kanbanCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.kb_hint', 'Ogni stato = una colonna; le card si impilano. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n <!-- Archetipo Calendar-wall (data \u2192 griglia settimane\u00D7giorni) -->\r\n @if (repeaterCfg.layout.mode === 'calendar') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('calendar')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('calendar')\" [class.pi-chevron-down]=\"!isSectionCollapsed('calendar')\"></i>\r\n {{ t('scene3d.repeater.calendar', 'Calendario') }}\r\n </div>\r\n @if (!isSectionCollapsed('calendar')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-date\">{{ t('scene3d.repeater.cal_date', 'Colonna data') }}</label>\r\n <p-select inputId=\"scene3d-cal-date\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"calendarCfg.dateColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\r\n <p-select inputId=\"scene3d-cal-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"calendarCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_size_ph', 'Costante')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-height\">{{ t('scene3d.repeater.surf_height', 'Scala altezza') }} ({{ calendarCfg.heightScale }})</label>\r\n <input id=\"scene3d-cal-height\" type=\"range\" min=\"1\" max=\"15\" step=\"0.5\" [(ngModel)]=\"calendarCfg.heightScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-cell\">{{ t('scene3d.repeater.surf_cell', 'Dimensione cella') }} ({{ calendarCfg.cellSize }})</label>\r\n <input id=\"scene3d-cal-cell\" type=\"range\" min=\"0.4\" max=\"3\" step=\"0.1\" [(ngModel)]=\"calendarCfg.cellSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"calendarCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.cal_hint', 'Le date riempiono una griglia settimane\u00D7giorni; valore \u2192 altezza/colore. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n <!-- Archetipo Stacked bars (categoria \u00D7 serie) -->\r\n @if (repeaterCfg.layout.mode === 'stacked') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('stacked')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('stacked')\" [class.pi-chevron-down]=\"!isSectionCollapsed('stacked')\"></i>\r\n {{ t('scene3d.repeater.stacked', 'Barre impilate') }}\r\n </div>\r\n @if (!isSectionCollapsed('stacked')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-category\">{{ t('scene3d.repeater.st_category', 'Colonna categoria') }}</label>\r\n <p-select inputId=\"scene3d-st-category\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"stackedCfg.categoryColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-series\">{{ t('scene3d.repeater.st_series', 'Colonna serie') }}</label>\r\n <p-select inputId=\"scene3d-st-series\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"stackedCfg.seriesColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\r\n <p-select inputId=\"scene3d-st-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"stackedCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-height\">{{ t('scene3d.repeater.chart_height', 'Altezza') }} ({{ stackedCfg.height }})</label>\r\n <input id=\"scene3d-st-height\" type=\"range\" min=\"2\" max=\"20\" step=\"0.5\" [(ngModel)]=\"stackedCfg.height\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-bar-width\">{{ t('scene3d.repeater.chart_bar_radius', 'Larghezza barra') }} ({{ stackedCfg.barWidth }})</label>\r\n <input id=\"scene3d-st-bar-width\" type=\"range\" min=\"0.4\" max=\"3\" step=\"0.1\" [(ngModel)]=\"stackedCfg.barWidth\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"stackedCfg.showLegend\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_legend', 'Legenda') }}</label>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"stackedCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.st_hint', 'Categoria = colonna; serie = segmenti impilati. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n @if (repeaterCfg.layout.mode === 'grid' || repeaterCfg.layout.mode === 'cube') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-order\">{{ t('scene3d.repeater.order', 'Ordine di flusso') }}</label>\r\n <select id=\"scene3d-repeater-order\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.layout.order\" (ngModelChange)=\"onRepeaterConfigChange()\">\r\n @for (o of layoutOrderOptions; track o.value) { <option [value]=\"o.value\">{{ o.label }}</option> }\r\n </select>\r\n </div>\r\n }\r\n <!-- Verso di riempimento per-asse (o senso di rotazione per il cerchio). -->\r\n @for (ax of layoutAxes; track ax) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" [attr.for]=\"'scene3d-axis-dir-' + ax\">{{ axisDirLabel(ax) }}</label>\r\n <select class=\"scene3d-material-select\" [id]=\"'scene3d-axis-dir-' + ax\" [ngModel]=\"getAxisDir(ax)\" (ngModelChange)=\"setAxisDir(ax, $event)\">\r\n @for (o of axisDirOptions(ax); track o.value) { <option [ngValue]=\"o.value\">{{ o.label }}</option> }\r\n </select>\r\n </div>\r\n }\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-pos-range\">{{ t('scene3d.repeater.pos_range', 'Range posizione') }} ({{ repeaterCfg.normalize.posRange }})</label>\r\n <input id=\"scene3d-repeater-pos-range\" type=\"range\" min=\"4\" max=\"200\" step=\"2\" [(ngModel)]=\"repeaterCfg.normalize.posRange\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-scale-min\">{{ t('scene3d.repeater.scale_range', 'Scala min/max') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-repeater-scale-min\" type=\"number\" step=\"0.1\" min=\"0.01\" [(ngModel)]=\"repeaterCfg.normalize.scaleMin\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n <input type=\"number\" step=\"0.1\" min=\"0.01\" [(ngModel)]=\"repeaterCfg.normalize.scaleMax\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n </div>\r\n\r\n <p-button\r\n icon=\"pi pi-refresh\"\r\n [label]=\"t('scene3d.repeater.regenerate', 'Rigenera') + (repeaterCount ? ' (' + repeaterCount + ')' : '')\"\r\n size=\"small\" [loading]=\"repeaterRegenerating\"\r\n (onClick)=\"regenerateSelectedRepeater()\">\r\n </p-button>\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.repeater.hint', 'Le mesh sono rigenerate dai dati (non salvate nel JSON). Sposta il gruppo coi gizmo; click su una mesh apre la CRUD del record nel viewer.') }}\r\n </div>\r\n\r\n <!-- Interazioni: callback JS su click/dblclick/rightclick di un item.\r\n Sezione collassabile in fondo (collassata di default). -->\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"callbacksSectionOpen = !callbacksSectionOpen\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"!callbacksSectionOpen\" [class.pi-chevron-down]=\"callbacksSectionOpen\"></i>\r\n {{ t('scene3d.repeater.interactions', 'Interazioni (callback JS)') }}\r\n </div>\r\n @if (callbacksSectionOpen) {\r\n <div class=\"scene3d-prop-group\">\r\n <p-button size=\"small\" [text]=\"true\"\r\n [severity]=\"repeaterCallbackIsSet('click') ? 'primary' : 'secondary'\"\r\n icon=\"pi pi-code\"\r\n [label]=\"(repeaterCallbackIsSet('click') ? '\u25CF ' : '') + t('scene3d.repeater.cb_click', 'Click')\"\r\n (onClick)=\"editRepeaterCallback('click')\"></p-button>\r\n <p-button size=\"small\" [text]=\"true\"\r\n [severity]=\"repeaterCallbackIsSet('dblclick') ? 'primary' : 'secondary'\"\r\n icon=\"pi pi-code\"\r\n [label]=\"(repeaterCallbackIsSet('dblclick') ? '\u25CF ' : '') + t('scene3d.repeater.cb_dblclick', 'Doppio-click')\"\r\n (onClick)=\"editRepeaterCallback('dblclick')\"></p-button>\r\n <p-button size=\"small\" [text]=\"true\"\r\n [severity]=\"repeaterCallbackIsSet('rightclick') ? 'primary' : 'secondary'\"\r\n icon=\"pi pi-code\"\r\n [label]=\"(repeaterCallbackIsSet('rightclick') ? '\u25CF ' : '') + t('scene3d.repeater.cb_rightclick', 'Tasto destro')\"\r\n (onClick)=\"editRepeaterCallback('rightclick')\"></p-button>\r\n </div>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.cb_hint', 'Nel callback: record (item), mesh, wuic.showItemPopup({mode,fields,title}). Vale in designer e viewer.') }}</div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Fisica per-oggetto (Rapier): corpo rigido + parametri -->\r\n @if (selectedIsMesh) {\r\n <div class=\"scene3d-material-section\">\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.physics.title', 'Fisica') }}</div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-body\">{{ t('scene3d.physics.body', 'Corpo') }}</label>\r\n <select id=\"scene3d-physics-body\" class=\"scene3d-material-select\" [(ngModel)]=\"propPhysics.type\" (ngModelChange)=\"onPhysicsInputChange()\">\r\n <option value=\"none\">{{ t('scene3d.physics.none', 'Nessuno') }}</option>\r\n <option value=\"static\">{{ t('scene3d.physics.static', 'Statico') }}</option>\r\n <option value=\"dynamic\">{{ t('scene3d.physics.dynamic', 'Dinamico') }}</option>\r\n <option value=\"kinematic\">{{ t('scene3d.physics.kinematic', 'Cinematico') }}</option>\r\n </select>\r\n </div>\r\n @if (propPhysics.type !== 'none') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-shape\">{{ t('scene3d.physics.shape', 'Forma collider') }}</label>\r\n <select id=\"scene3d-physics-shape\" class=\"scene3d-material-select\" [(ngModel)]=\"propPhysics.shape\" (ngModelChange)=\"onPhysicsInputChange()\">\r\n <option value=\"auto\">{{ t('scene3d.physics.shape_auto', 'Auto (dalla geometria)') }}</option>\r\n <option value=\"box\">Box</option>\r\n <option value=\"sphere\">{{ t('scene3d.physics.shape_sphere', 'Sfera') }}</option>\r\n <option value=\"hull\">{{ t('scene3d.physics.shape_hull', 'Convex hull') }}</option>\r\n </select>\r\n </div>\r\n @if (propPhysics.type === 'dynamic') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-mass\">{{ t('scene3d.physics.mass', 'Massa (kg)') }}</label>\r\n <input id=\"scene3d-physics-mass\" type=\"number\" step=\"0.1\" min=\"0.001\" [(ngModel)]=\"propPhysics.mass\" (ngModelChange)=\"onPhysicsInputChange()\" />\r\n </div>\r\n }\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-friction\">{{ t('scene3d.physics.friction', 'Attrito') }} ({{ propPhysics.friction }})</label>\r\n <input id=\"scene3d-physics-friction\" type=\"range\" min=\"0\" max=\"1\" step=\"0.05\" [(ngModel)]=\"propPhysics.friction\" (ngModelChange)=\"onPhysicsInputChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-restitution\">{{ t('scene3d.physics.restitution', 'Rimbalzo') }} ({{ propPhysics.restitution }})</label>\r\n <input id=\"scene3d-physics-restitution\" type=\"range\" min=\"0\" max=\"1\" step=\"0.05\" [(ngModel)]=\"propPhysics.restitution\" (ngModelChange)=\"onPhysicsInputChange()\" />\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Registrazione animazione: keyframe pos/rot/scala -> AnimationClip -->\r\n <div class=\"scene3d-anim-section\">\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"animRecSectionOpen = !animRecSectionOpen\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"!animRecSectionOpen\" [class.pi-chevron-down]=\"animRecSectionOpen\"></i>\r\n {{ t('scene3d.animrec.title', 'Registra animazione') }}\r\n </div>\r\n @if (animRecSectionOpen) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-animrec-clip-name\">{{ t('scene3d.animrec.clip_name', 'Nome clip') }}</label>\r\n <input id=\"scene3d-animrec-clip-name\" type=\"text\" [(ngModel)]=\"animRecClipName\" [placeholder]=\"t('scene3d.animrec.clip_name_ph', 'Clip 1')\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-animrec-time\">{{ t('scene3d.animrec.time', 'Tempo T (s)') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-animrec-time\" type=\"number\" step=\"0.1\" min=\"0\" [(ngModel)]=\"animRecTime\" />\r\n <p-button\r\n icon=\"pi pi-plus\"\r\n [label]=\"t('scene3d.animrec.add', 'Aggiungi keyframe')\"\r\n size=\"small\"\r\n (onClick)=\"addRecKeyframe()\">\r\n </p-button>\r\n </div>\r\n </div>\r\n\r\n @if (animRecKeyframes.length > 0) {\r\n <div class=\"scene3d-kf-list\">\r\n @for (kf of animRecKeyframes; track kf.time; let i = $index) {\r\n <div class=\"scene3d-kf-row\">\r\n <button type=\"button\" class=\"scene3d-kf-apply\" (click)=\"applyRecKeyframe(i)\"\r\n [title]=\"t('scene3d.animrec.apply', 'Applica questa posa all\\'oggetto')\">\r\n <i class=\"pi pi-eye\"></i> t = {{ kf.time }}s\r\n </button>\r\n <button type=\"button\" class=\"scene3d-kf-delete\" (click)=\"removeRecKeyframe(i)\"\r\n [title]=\"t('designer.remove', 'Rimuovi')\">\r\n <i class=\"pi pi-trash\"></i>\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <p-button\r\n icon=\"pi pi-video\"\r\n [label]=\"t('scene3d.animrec.generate', 'Genera clip')\"\r\n size=\"small\"\r\n [disabled]=\"animRecKeyframes.length < 2\"\r\n (onClick)=\"generateRecClip()\">\r\n </p-button>\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.animrec.hint', 'Posa l\\'oggetto coi gizmo, aggiungi un keyframe al tempo T, ripeti. La clip generata usa il transport qui sopra e persiste col salvataggio (autoplay nel viewer).') }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <div class=\"scene3d-gizmo-hint\">\r\n {{ t('scene3d.props.gizmo_hint', 'g = sposta, r = ruota, s = scala') }}\r\n </div>\r\n } @else {\r\n <div class=\"scene3d-props-empty\">\r\n {{ t('scene3d.props.empty', 'Nessun oggetto selezionato') }}\r\n </div>\r\n }\r\n </div>\r\n\r\n </div>\r\n\r\n <!-- Footer componenti scena (mirror del footer del designer 2D):\r\n click = seleziona, right-click = ctx menu con Rimuovi. -->\r\n <div class=\"scene3d-footer\">\r\n <span class=\"scene3d-footer-label\">{{ t('scene3d.footer.title', 'Scena') }}:</span>\r\n <div class=\"scene3d-footer-list\">\r\n @for (item of sceneObjects; track item.uuid) {\r\n <button\r\n type=\"button\"\r\n class=\"scene3d-footer-item\"\r\n [class.active]=\"item.uuid === selectedObjectUuid\"\r\n [title]=\"item.label\"\r\n (click)=\"selectFromFooter(item.uuid)\"\r\n (contextmenu)=\"showFooterContextMenu($event, item.uuid, sceneFooterCtxMenu)\">\r\n {{ item.label }}\r\n </button>\r\n } @empty {\r\n <span class=\"scene3d-footer-empty\">{{ t('scene3d.footer.empty', 'Nessun componente nella scena') }}</span>\r\n }\r\n </div>\r\n <p-contextMenu #sceneFooterCtxMenu [model]=\"footerCtxItems\" appendTo=\"body\" />\r\n </div>\r\n</div>\r\n", styles: [":host{display:block;width:100%;height:100%;min-height:0}.scene3d-designer-shell{display:flex;flex-direction:column;width:100%;height:100%;min-height:480px}.scene3d-toolbar{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid var(--surface-border, #dee2e6);flex:0 0 auto;flex-wrap:wrap}.scene3d-load-select{min-width:14rem}.scene3d-body{display:flex;flex:1 1 auto;min-height:0}.scene3d-palette{flex:0 0 12rem;display:flex;flex-direction:column;gap:.25rem;padding:.5rem;overflow-y:auto;border-right:1px solid var(--surface-border, #dee2e6)}.scene3d-panel-title{font-weight:600;font-size:.85rem;text-transform:uppercase;opacity:.7;margin-bottom:.25rem}.scene3d-palette-item{display:flex;align-items:center;gap:.5rem;padding:.4rem .5rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;background:var(--surface-card, #fff);cursor:pointer;text-align:left;font-size:.85rem;color:inherit}.scene3d-palette-item:hover:not(:disabled){background:var(--surface-hover, #f5f5f5)}.scene3d-palette-item:disabled{opacity:.45;cursor:not-allowed}.scene3d-palette-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.scene3d-viewport{flex:1 1 auto;min-width:0;position:relative;overflow:hidden;background:#1e1e28}.scene3d-viewport canvas{display:block;width:100%;height:100%}.scene3d-properties{flex:0 0 16rem;display:flex;flex-direction:column;gap:.6rem;padding:.75rem;overflow-y:auto;border-left:1px solid var(--surface-border, #dee2e6)}.scene3d-prop-group{display:flex;flex-direction:column;gap:.25rem}.scene3d-prop-label{font-size:.8rem;font-weight:600;opacity:.8}.scene3d-prop-readonly{font-size:.85rem;opacity:.7;word-break:break-all}.scene3d-vector-row{display:flex;gap:.25rem}.scene3d-vector-row input{width:100%;min-width:0;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem}.scene3d-props-empty,.scene3d-gizmo-hint{font-size:.8rem;opacity:.6}.scene3d-prop-checkbox{display:inline-flex;align-items:center;gap:.4rem;cursor:pointer}.scene3d-kf-list{display:flex;flex-direction:column;gap:3px;max-height:160px;overflow-y:auto;border:1px dashed var(--surface-border, #dee2e6);border-radius:4px;padding:4px}.scene3d-kf-row{display:flex;align-items:center;justify-content:space-between;gap:6px}.scene3d-kf-apply,.scene3d-kf-delete{border:1px solid var(--surface-border, #dee2e6);background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.78rem;cursor:pointer;display:inline-flex;align-items:center;gap:4px}.scene3d-kf-apply{flex:1 1 auto;justify-content:flex-start}.scene3d-kf-apply:hover{border-color:#3b82f6}.scene3d-kf-delete:hover{border-color:#ef4444;color:#ef4444}.scene3d-anim-section input[type=text],.scene3d-anim-section input[type=number]{width:100%;min-width:0;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem}.scene3d-material-section{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6)}.scene3d-material-section input[type=range]{width:100%}.scene3d-material-select{width:100%;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem;background:var(--surface-card, #fff);color:inherit}.scene3d-shader-toggle{cursor:pointer;-webkit-user-select:none;user-select:none}.scene3d-shader-src{width:100%;min-width:0;resize:vertical;padding:.35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-family:Consolas,Monaco,monospace;font-size:.72rem;line-height:1.35;background:#14141c;color:#d1d5db}.scene3d-pt-hud{position:absolute;top:8px;left:50%;transform:translate(-50%);z-index:25;display:flex;align-items:center;gap:12px;padding:4px 12px;border-radius:6px;background:#14141cd9;color:#e5e7eb;font-size:.78rem;border:1px solid #3b82f6}.scene3d-pt-scale{display:inline-flex;align-items:center;gap:6px}.scene3d-pt-scale input[type=range]{width:90px}.scene3d-pt-exit{border:1px solid #3b82f6;background:transparent;color:#93c5fd;border-radius:4px;padding:2px 8px;font-size:.75rem;cursor:pointer}.scene3d-cam-preview{position:absolute;z-index:20;width:320px;border:1px solid #3b82f6;border-radius:6px;background:#14141c;box-shadow:0 6px 24px #00000073;overflow:hidden}.scene3d-cam-preview-header{display:flex;align-items:center;justify-content:space-between;padding:3px 8px;background:#1f2937;color:#e5e7eb;font-size:.75rem;cursor:move;-webkit-user-select:none;user-select:none}.scene3d-cam-preview-close{border:none;background:transparent;color:#e5e7eb;font-size:1rem;line-height:1;cursor:pointer;padding:0 2px}.scene3d-cam-preview-body{width:320px;height:192px}.scene3d-cam-preview-body canvas{display:block}.scene3d-gizmo-hint{margin-top:.5rem;padding-top:.5rem;border-top:1px dashed var(--surface-border, #dee2e6)}.scene3d-binding-section{display:flex;flex-direction:column;gap:.6rem;margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6)}.scene3d-panel-subtitle{font-weight:600;font-size:.78rem;text-transform:uppercase;opacity:.7}.scene3d-panel-subtitle.scene3d-collapsible{display:flex;align-items:center;gap:.4rem;cursor:pointer;-webkit-user-select:none;user-select:none;padding:.15rem 0;opacity:.85}.scene3d-panel-subtitle.scene3d-collapsible:hover{opacity:1}.scene3d-panel-subtitle.scene3d-collapsible>.pi{font-size:.7rem;opacity:.8}.scene3d-field-map{display:flex;flex-direction:column;gap:.5rem;padding:.5rem;border:1px dashed var(--surface-border, #dee2e6);border-radius:4px}.scene3d-binding-select{width:100%}.scene3d-binding-section input[type=text]{width:100%;min-width:0;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem}.scene3d-open-record-btn{align-self:flex-start}.scene3d-binding-hint{font-size:.78rem;opacity:.6}.scene3d-anim-section{display:flex;flex-direction:column;gap:.6rem;margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6)}.scene3d-anim-transport{display:flex;gap:.35rem}.scene3d-anim-scrubber{width:100%;min-width:0}.scene3d-anim-time{font-size:.78rem;opacity:.6;text-align:right}.scene3d-footer{display:flex;align-items:center;gap:8px;padding:6px 8px;border-top:1px solid var(--surface-border, #dee2e6);min-height:36px;flex:0 0 auto;width:100%;overflow:hidden}.scene3d-footer-label{font-size:.85rem;font-weight:600;white-space:nowrap}.scene3d-footer-list{display:flex;align-items:center;gap:6px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;width:100%}.scene3d-footer-item{border:1px solid var(--surface-border, #dee2e6);background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.8rem;line-height:1.25rem;cursor:pointer;white-space:nowrap;display:inline-flex;align-items:center;gap:4px}.scene3d-footer-item.active{border-color:#3b82f6;color:#1d4ed8}.scene3d-footer-empty{font-size:.8rem;opacity:.6}.scene3d-scene-option{display:flex;align-items:center;gap:10px;min-width:0}.scene3d-scene-option-thumb{width:72px;height:44px;flex:0 0 auto;border-radius:4px;object-fit:cover;border:1px solid #cbd5e1;background:#1e1e28}.scene3d-scene-option-thumb-empty{display:inline-flex;align-items:center;justify-content:center;color:#64748b}.scene3d-scene-option-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.RangeValueAccessor, selector: "input[type=range][formControlName],input[type=range][formControl],input[type=range][ngModel]" }, { kind: "directive", type: i1$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i4.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "directive", type: i3.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i5.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: SplitButtonModule }, { kind: "component", type: i12.SplitButton, selector: "p-splitbutton, p-splitButton, p-split-button", inputs: ["model", "severity", "raised", "rounded", "text", "outlined", "size", "plain", "icon", "iconPos", "label", "tooltip", "tooltipOptions", "styleClass", "menuStyle", "menuStyleClass", "dropdownIcon", "appendTo", "dir", "expandAriaLabel", "showTransitionOptions", "hideTransitionOptions", "motionOptions", "buttonProps", "menuButtonProps", "autofocus", "disabled", "tabindex", "menuButtonDisabled", "buttonDisabled"], outputs: ["onClick", "onMenuHide", "onMenuShow", "onDropdownClick"] }, { kind: "ngmodule", type: ContextMenuModule }, { kind: "component", type: i14.ContextMenu, selector: "p-contextMenu, p-contextmenu, p-context-menu", inputs: ["model", "triggerEvent", "target", "global", "style", "styleClass", "autoZIndex", "baseZIndex", "id", "breakpoint", "ariaLabel", "ariaLabelledBy", "pressDelay", "appendTo", "motionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: TranslateModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4795
4795
|
}
|
|
4796
4796
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: Scene3dDesignerComponent, decorators: [{
|
|
4797
4797
|
type: Component,
|
|
4798
|
-
args: [{ selector: 'wuic-scene3d-designer', standalone: true, imports: [FormsModule, ButtonModule, SelectModule, SplitButtonModule, ContextMenuModule, TranslateModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- F2/F3: viewport WebGL (canvas three.js) + palette oggetti a sinistra +\n property panel a destra + toolbar (save/load/new/delete). -->\n<div class=\"scene3d-designer-shell\">\n\n <!-- Toolbar -->\n <div class=\"scene3d-toolbar\">\n <p-button\n icon=\"pi pi-file\"\n [label]=\"t('scene3d.toolbar.new', 'Nuova')\"\n severity=\"secondary\"\n size=\"small\"\n (onClick)=\"newScene()\">\n </p-button>\n\n <p-button\n icon=\"pi pi-save\"\n [label]=\"t('scene3d.toolbar.save', 'Salva')\"\n size=\"small\"\n [loading]=\"savingScene\"\n (onClick)=\"saveScene()\">\n </p-button>\n\n <p-select\n [options]=\"savedSceneOptions\"\n [(ngModel)]=\"selectedSavedSceneKey\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"t('scene3d.toolbar.load_placeholder', 'Carica scena...')\"\n appendTo=\"body\"\n styleClass=\"scene3d-load-select\"\n (onChange)=\"onSavedSceneSelectionChange()\">\n <!-- Anteprima scena (thumbnail dal worker server-side GPU); placeholder cubo se assente. -->\n <ng-template pTemplate=\"item\" let-opt>\n <div class=\"scene3d-scene-option\">\n @if (opt.thumbnail) {\n <img class=\"scene3d-scene-option-thumb\" [src]=\"opt.thumbnail\" [alt]=\"opt.label\" loading=\"lazy\" />\n } @else {\n <span class=\"scene3d-scene-option-thumb scene3d-scene-option-thumb-empty\">\n <i class=\"pi pi-box\"></i>\n </span>\n }\n <span class=\"scene3d-scene-option-label\">{{ opt.label }}</span>\n </div>\n </ng-template>\n </p-select>\n\n <p-button\n icon=\"pi pi-folder-open\"\n [label]=\"t('scene3d.toolbar.load', 'Carica')\"\n severity=\"secondary\"\n size=\"small\"\n [loading]=\"loadingScene\"\n [disabled]=\"!selectedSavedSceneKey\"\n (onClick)=\"loadScene()\">\n </p-button>\n\n <p-button\n icon=\"pi pi-trash\"\n [label]=\"t('scene3d.toolbar.delete_selected', 'Elimina selezionato')\"\n severity=\"danger\"\n size=\"small\"\n [disabled]=\"!selectedObjectUuid\"\n (onClick)=\"deleteSelectedObject()\">\n </p-button>\n\n <!-- Toggle luci default del viewport: OFF = si vede solo l'effetto delle luci droppate -->\n <p-button\n icon=\"pi pi-sun\"\n [label]=\"t('scene3d.toolbar.default_lights', 'Luci default')\"\n [severity]=\"defaultLightsEnabled ? 'primary' : 'secondary'\"\n [outlined]=\"!defaultLightsEnabled\"\n size=\"small\"\n [title]=\"t('scene3d.toolbar.default_lights_hint', 'Attiva/disattiva le luci di default del viewport')\"\n (onClick)=\"toggleDefaultLights()\">\n </p-button>\n\n <!-- Toggle shadow map del renderer (default ON) -->\n <p-button\n icon=\"pi pi-moon\"\n [label]=\"t('scene3d.toolbar.shadows', 'Ombre')\"\n [severity]=\"shadowsEnabled ? 'primary' : 'secondary'\"\n [outlined]=\"!shadowsEnabled\"\n size=\"small\"\n [title]=\"t('scene3d.toolbar.shadows_hint', 'Attiva/disattiva le ombre proiettate')\"\n (onClick)=\"toggleShadows()\">\n </p-button>\n\n <!-- Renderer WebGPU (opt-in): fallback WebGL se non supportato; shader GLSL e PT restano WebGL-only -->\n <p-button\n icon=\"pi pi-bolt\"\n label=\"WebGPU\"\n [severity]=\"rendererMode === 'webgpu' ? 'primary' : 'secondary'\"\n [outlined]=\"rendererMode !== 'webgpu'\"\n size=\"small\"\n [loading]=\"rendererSwitching\"\n [disabled]=\"!webGpuSupported\"\n [title]=\"webGpuSupported\n ? t('scene3d.renderer.toggle_hint', 'Renderer WebGPU (node-materials). Shader GLSL custom e path tracing restano disponibili solo in WebGL')\n : t('scene3d.renderer.webgpu_unavailable', 'WebGPU non disponibile in questo browser: resto su WebGL')\"\n (onClick)=\"toggleWebGpuRenderer()\">\n </p-button>\n\n <!-- Toggle guide di scena: proxy luci/camera + linee helper/frustum -->\n <p-button\n icon=\"pi pi-eye\"\n [label]=\"t('scene3d.toolbar.helpers', 'Guide')\"\n [severity]=\"helpersVisible ? 'primary' : 'secondary'\"\n [outlined]=\"!helpersVisible\"\n size=\"small\"\n [title]=\"t('scene3d.toolbar.helpers_hint', 'Mostra/nasconde i placeholder delle luci e le linee helper/frustum')\"\n (onClick)=\"toggleHelpers()\">\n </p-button>\n\n <!-- Auto-rigenera (toggle globale): se ON, ogni cambio di proprieta' rigenera\n automaticamente il repeater selezionato (con debounce), senza cliccare\n \"Rigenera\". Per gli altri oggetti palette i cambi si applicano gia' live. -->\n <p-button\n icon=\"pi pi-sync\"\n [label]=\"t('scene3d.toolbar.auto_regen', 'Auto-rigenera')\"\n [severity]=\"autoRegen ? 'primary' : 'secondary'\"\n [outlined]=\"!autoRegen\"\n size=\"small\"\n [title]=\"t('scene3d.toolbar.auto_regen_hint', 'Se attivo, ogni modifica di proprieta rigenera automaticamente (repeater), senza premere Rigenera')\"\n (onClick)=\"autoRegen = !autoRegen\">\n </p-button>\n\n <!-- Render qualita' (path tracing progressivo, lazy): GI + ombre colorate dai vetri -->\n <p-button\n icon=\"pi pi-sparkles\"\n [label]=\"pathTracingEnabled ? t('scene3d.toolbar.pt_exit', 'Realtime') : t('scene3d.toolbar.pt', 'Render PT')\"\n [severity]=\"pathTracingEnabled ? 'primary' : 'secondary'\"\n [outlined]=\"!pathTracingEnabled\"\n size=\"small\"\n [loading]=\"ptBuilding\"\n [title]=\"t('scene3d.toolbar.pt_hint', 'Render path-traced progressivo (GI, ombre colorate dai vetri). Modalita\\' preview: editing sospeso')\"\n (onClick)=\"togglePathTracing()\">\n </p-button>\n\n <!-- Fisica: Play/Stop simulazione + menu (Applica pose / Impostazioni scena) -->\n <p-splitButton\n [icon]=\"physicsSimulating ? 'pi pi-stop' : 'pi pi-play'\"\n [label]=\"physicsSimulating ? t('scene3d.toolbar.physics_stop', 'Stop fisica') : t('scene3d.toolbar.physics_play', 'Fisica')\"\n [severity]=\"physicsSimulating ? 'primary' : 'secondary'\"\n [outlined]=\"!physicsSimulating\"\n size=\"small\"\n [model]=\"physicsMenuItems\"\n appendTo=\"body\"\n [attr.title]=\"t('scene3d.toolbar.physics_hint', 'Simula i corpi rigidi configurati (Stop = ripristina le pose)')\"\n (onClick)=\"togglePhysicsSimulation()\">\n </p-splitButton>\n\n <!-- Bake luci per-vertice: cuoce l'illuminazione nei vertex colors (v1 lightmap) -->\n <p-button\n icon=\"pi pi-bolt\"\n [label]=\"t('scene3d.toolbar.bake', 'Bake luci')\"\n severity=\"secondary\"\n size=\"small\"\n [title]=\"t('scene3d.toolbar.bake_hint', 'Cuoce l\\'illuminazione corrente nei vertex colors (materiale statico unlit)')\"\n (onClick)=\"bakeLightsAction()\">\n </p-button>\n\n <!-- Crea una camera di scena che replica il punto di vista attuale del designer -->\n <p-button\n icon=\"pi pi-camera\"\n [label]=\"t('scene3d.toolbar.camera_from_view', 'Camera da vista attuale')\"\n severity=\"secondary\"\n size=\"small\"\n [title]=\"t('scene3d.toolbar.camera_from_view_hint', 'Crea una camera nella scena con posizione e inquadratura identiche alla vista corrente')\"\n (onClick)=\"createCameraFromView()\">\n </p-button>\n\n <!-- Vista camera: render dalla camera utente (selezionata o prima in scena).\n Con piu' camere in scena diventa uno split-button col picker. -->\n @if (sceneCameraCount > 1) {\n <p-splitButton\n icon=\"pi pi-video\"\n [label]=\"viewingThroughCamera ? t('scene3d.toolbar.designer_view', 'Vista designer') : t('scene3d.toolbar.camera_view', 'Vista camera')\"\n [severity]=\"viewingThroughCamera ? 'primary' : 'secondary'\"\n [outlined]=\"!viewingThroughCamera\"\n size=\"small\"\n [model]=\"cameraViewMenuItems\"\n appendTo=\"body\"\n [attr.title]=\"t('scene3d.toolbar.camera_view_hint', 'Alterna tra la vista di editing e la vista dalla camera della scena')\"\n (onClick)=\"toggleCameraView()\">\n </p-splitButton>\n } @else {\n <p-button\n icon=\"pi pi-video\"\n [label]=\"viewingThroughCamera ? t('scene3d.toolbar.designer_view', 'Vista designer') : t('scene3d.toolbar.camera_view', 'Vista camera')\"\n [severity]=\"viewingThroughCamera ? 'primary' : 'secondary'\"\n [outlined]=\"!viewingThroughCamera\"\n size=\"small\"\n [title]=\"t('scene3d.toolbar.camera_view_hint', 'Alterna tra la vista di editing e la vista dalla camera della scena')\"\n (onClick)=\"toggleCameraView()\">\n </p-button>\n }\n </div>\n\n <div class=\"scene3d-body\">\n\n <!-- Palette (sinistra) -->\n <div class=\"scene3d-palette\">\n <div class=\"scene3d-panel-title\">{{ t('scene3d.palette.title', 'Oggetti') }}</div>\n @for (item of paletteItems; track item.type) {\n <button\n type=\"button\"\n class=\"scene3d-palette-item\"\n [disabled]=\"!item.fromPalette\"\n [title]=\"item.label\"\n (click)=\"addFromPalette(item.type)\">\n <i [class]=\"item.icon\"></i>\n <span class=\"scene3d-palette-label\">{{ item.label }}</span>\n </button>\n }\n </div>\n\n <!-- Viewport (centro) -->\n <div #viewportContainer class=\"scene3d-viewport\">\n <!-- HUD path tracing: build, sample progressivi, scala risoluzione -->\n @if (ptBuilding || pathTracingEnabled) {\n <div class=\"scene3d-pt-hud\">\n @if (ptBuilding) {\n <span>{{ t('scene3d.pt.building', 'Costruzione BVH...') }}</span>\n } @else {\n <span>{{ ptSamples }} {{ t('scene3d.pt.samples_suffix', 'sample') }}</span>\n <label class=\"scene3d-pt-scale\">\n {{ t('scene3d.pt.scale', 'Risoluzione') }}\n <input type=\"range\" min=\"0.25\" max=\"1\" step=\"0.25\"\n [(ngModel)]=\"ptResolutionScale\" (ngModelChange)=\"onPtResolutionScaleChange()\" />\n </label>\n <button type=\"button\" class=\"scene3d-pt-exit\" (click)=\"togglePathTracing()\">\n {{ t('scene3d.pt.exit', 'Torna al realtime') }}\n </button>\n }\n </div>\n }\n\n <!-- Popup anteprima camera: real-time, trascinabile dall'header -->\n @if (cameraPreviewEnabled) {\n <div class=\"scene3d-cam-preview\" [style.left.px]=\"previewPos.x\" [style.top.px]=\"previewPos.y\">\n <div class=\"scene3d-cam-preview-header\" (mousedown)=\"startPreviewDrag($event)\">\n <span class=\"scene3d-cam-preview-title\">{{ t('scene3d.preview.title', 'Anteprima camera') }}</span>\n <button type=\"button\" class=\"scene3d-cam-preview-close\" (click)=\"toggleCameraPreview()\" (mousedown)=\"$event.stopPropagation()\">\u00D7</button>\n </div>\n <div #previewCanvasHost class=\"scene3d-cam-preview-body\"></div>\n </div>\n }\n </div>\n\n <!-- Property panel (destra) -->\n <div class=\"scene3d-properties\">\n <div class=\"scene3d-panel-title\">{{ t('scene3d.props.title', 'Proprieta') }}</div>\n\n @if (selectedObjectUuid) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-prop-name\">{{ t('scene3d.props.name', 'Nome') }}</label>\n <input id=\"scene3d-prop-name\" type=\"text\" [(ngModel)]=\"selectedObjectName\"\n (ngModelChange)=\"renameSelectedObject($event)\"\n [placeholder]=\"t('scene3d.props.name_placeholder', 'Nome oggetto')\" />\n </div>\n\n <!-- ID della camera: e' il camera_id per l'URL del viewer\n (#/scene3d_viewer/<scene_key>/<camera_id>). Persiste nel salvataggio. -->\n @if (selectedIsCamera) {\n <div class=\"scene3d-prop-group\">\n <span class=\"scene3d-prop-label\">{{ t('scene3d.props.camera_id', 'ID camera (viewer)') }}</span>\n <div class=\"scene3d-prop-readonly\">{{ selectedObjectUuid }}</div>\n </div>\n\n <!-- Anteprima real-time in popup trascinabile dal punto di vista della camera -->\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input\n type=\"checkbox\"\n [ngModel]=\"cameraPreviewEnabled && previewCameraUuid === selectedObjectUuid\"\n (ngModelChange)=\"toggleCameraPreview()\" />\n {{ t('scene3d.props.camera_preview', 'Anteprima camera') }}\n </label>\n </div>\n }\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-pos-x\">{{ t('scene3d.props.position', 'Posizione') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-pos-x\" type=\"number\" step=\"0.1\" [(ngModel)]=\"propPosition.x\" (ngModelChange)=\"onTransformInputChange()\" />\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propPosition.y\" (ngModelChange)=\"onTransformInputChange()\" />\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propPosition.z\" (ngModelChange)=\"onTransformInputChange()\" />\n </div>\n </div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-rot-x\">{{ t('scene3d.props.rotation', 'Rotazione (deg)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-rot-x\" type=\"number\" step=\"1\" [(ngModel)]=\"propRotation.x\" (ngModelChange)=\"onTransformInputChange()\" />\n <input type=\"number\" step=\"1\" [(ngModel)]=\"propRotation.y\" (ngModelChange)=\"onTransformInputChange()\" />\n <input type=\"number\" step=\"1\" [(ngModel)]=\"propRotation.z\" (ngModelChange)=\"onTransformInputChange()\" />\n </div>\n </div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-scale-x\">{{ t('scene3d.props.scale', 'Scala') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-scale-x\" type=\"number\" step=\"0.1\" [(ngModel)]=\"propScale.x\" (ngModelChange)=\"onTransformInputChange()\" />\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propScale.y\" (ngModelChange)=\"onTransformInputChange()\" />\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propScale.z\" (ngModelChange)=\"onTransformInputChange()\" />\n </div>\n </div>\n\n @if (selectedHasMaterialColor) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-prop-color\">{{ t('scene3d.props.color', 'Colore') }}</label>\n <input id=\"scene3d-prop-color\" type=\"color\" [(ngModel)]=\"propColor\" (ngModelChange)=\"onColorInputChange()\" />\n </div>\n }\n\n <!-- castShadow per-oggetto (default ON alla creazione, disattivabile) -->\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"propCastShadow\" (ngModelChange)=\"onCastShadowInputChange()\" />\n {{ t('scene3d.props.cast_shadow', 'Proietta ombra') }}\n </label>\n </div>\n\n <!-- Editor materiale PBR (MeshStandardMaterial) -->\n @if (selectedHasStandardMaterial) {\n <div class=\"scene3d-material-section\">\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.material.title', 'Materiale') }}</div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-metalness\">{{ t('scene3d.material.metalness', 'Metalness') }} ({{ propMaterial.metalness }})</label>\n <input id=\"scene3d-mat-metalness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.metalness\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-roughness\">{{ t('scene3d.material.roughness', 'Roughness') }} ({{ propMaterial.roughness }})</label>\n <input id=\"scene3d-mat-roughness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.roughness\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-opacity\">{{ t('scene3d.material.opacity', 'Opacita\\'') }} ({{ propMaterial.opacity }})</label>\n <input id=\"scene3d-mat-opacity\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.opacity\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-emissive\">{{ t('scene3d.material.emissive', 'Emissivo') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-mat-emissive\" type=\"color\" [(ngModel)]=\"propMaterial.emissive\" (ngModelChange)=\"onMaterialInputChange()\" />\n <input type=\"number\" step=\"0.1\" min=\"0\" [(ngModel)]=\"propMaterial.emissiveIntensity\" (ngModelChange)=\"onMaterialInputChange()\"\n [title]=\"t('scene3d.material.emissive_intensity', 'Intensita\\' emissiva')\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-side\">{{ t('scene3d.material.side', 'Facce') }}</label>\n <select id=\"scene3d-mat-side\" class=\"scene3d-material-select\" [(ngModel)]=\"propMaterial.side\" (ngModelChange)=\"onMaterialInputChange()\">\n <option value=\"front\">{{ t('scene3d.material.side_front', 'Fronte') }}</option>\n <option value=\"back\">{{ t('scene3d.material.side_back', 'Retro') }}</option>\n <option value=\"double\">{{ t('scene3d.material.side_double', 'Doppia') }}</option>\n </select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"propMaterial.wireframe\" (ngModelChange)=\"onMaterialInputChange()\" />\n {{ t('scene3d.material.wireframe', 'Wireframe') }}\n </label>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"propMaterial.flatShading\" (ngModelChange)=\"onMaterialInputChange()\" />\n {{ t('scene3d.material.flat', 'Flat shading') }}\n </label>\n </div>\n\n <!-- Vetro fisico (transmission): rifrazione reale di cio' che sta dietro.\n In Render PT l'assorbimento (colore+distanza) tinge anche la LUCE\n che attraversa il vetro sugli altri oggetti (Beer-Lambert). -->\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.material.glass', 'Vetro (transmission)') }}</div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-transmission\">{{ t('scene3d.material.transmission', 'Trasmissione') }} ({{ propMaterial.transmission }})</label>\n <input id=\"scene3d-mat-transmission\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.transmission\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n @if (propMaterial.transmission > 0) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-ior\">{{ t('scene3d.material.ior', 'IOR') }} ({{ propMaterial.ior }})</label>\n <input id=\"scene3d-mat-ior\" type=\"range\" min=\"1\" max=\"2.33\" step=\"0.01\" [(ngModel)]=\"propMaterial.ior\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-thickness\">{{ t('scene3d.material.thickness', 'Spessore') }} ({{ propMaterial.thickness }})</label>\n <input id=\"scene3d-mat-thickness\" type=\"range\" min=\"0\" max=\"5\" step=\"0.1\" [(ngModel)]=\"propMaterial.thickness\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n <!-- Assorbimento volumetrico: colora la luce trasmessa (visibile in PT). -->\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-attenuation-color\">{{ t('scene3d.material.attenuation_color', 'Colore assorbimento') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-mat-attenuation-color\" type=\"color\" [(ngModel)]=\"propMaterial.attenuationColor\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-attenuation-distance\">{{ t('scene3d.material.attenuation_distance', 'Distanza assorbimento') }} ({{ propMaterial.attenuationDistance }})</label>\n <input id=\"scene3d-mat-attenuation-distance\" type=\"range\" min=\"0.05\" max=\"10\" step=\"0.05\" [(ngModel)]=\"propMaterial.attenuationDistance\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.material.glass_hint', 'Con transmission tieni Opacita\\' = 1 (la trasparenza la gestisce il vetro). Il colore assorbimento tinge la luce filtrata: visibile in Render PT.') }}\n </div>\n }\n </div>\n }\n\n <!-- Shader GLSL custom -->\n @if (selectedIsMesh) {\n <div class=\"scene3d-material-section\">\n <div class=\"scene3d-panel-subtitle scene3d-shader-toggle\" (click)=\"shaderSectionOpen = !shaderSectionOpen\">\n {{ t('scene3d.shader.title', 'Shader custom (GLSL)') }} {{ shaderSectionOpen ? '\u25BE' : '\u25B8' }}\n </div>\n @if (shaderSectionOpen) {\n <!-- Effetto JSON (dual-backend): editor Monaco con schema, compilato\n in GLSL su WebGL e in node-material su WebGPU -->\n <div class=\"scene3d-anim-transport\">\n <p-button\n icon=\"pi pi-sliders-h\"\n [label]=\"t('scene3d.shader.effect_edit', 'Effetto JSON')\"\n size=\"small\"\n [severity]=\"selectedHasShaderEffect ? 'primary' : 'secondary'\"\n [title]=\"t('scene3d.shader.effect_tooltip', 'JSON con schema (completion/validazione in editor). Compilato in GLSL su WebGL e in node-material su WebGPU.')\"\n (onClick)=\"editShaderEffect()\">\n </p-button>\n @if (selectedHasShaderEffect) {\n <p-button\n icon=\"pi pi-undo\"\n [label]=\"t('scene3d.shader.effect_remove', 'Rimuovi effetto')\"\n severity=\"secondary\"\n size=\"small\"\n (onClick)=\"removeShaderEffect()\">\n </p-button>\n }\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-shader-vertex\">{{ t('scene3d.shader.vertex', 'Vertex shader') }}</label>\n <textarea id=\"scene3d-shader-vertex\" class=\"scene3d-shader-src\" rows=\"6\" spellcheck=\"false\" [(ngModel)]=\"shaderVertexSrc\"></textarea>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-shader-fragment\">{{ t('scene3d.shader.fragment', 'Fragment shader') }}</label>\n <textarea id=\"scene3d-shader-fragment\" class=\"scene3d-shader-src\" rows=\"8\" spellcheck=\"false\" [(ngModel)]=\"shaderFragmentSrc\"></textarea>\n </div>\n <div class=\"scene3d-anim-transport\">\n <p-button\n icon=\"pi pi-check\"\n [label]=\"t('scene3d.shader.apply', 'Applica shader')\"\n size=\"small\"\n (onClick)=\"applyCustomShader()\">\n </p-button>\n @if (selectedHasCustomShader) {\n <p-button\n icon=\"pi pi-undo\"\n [label]=\"t('scene3d.shader.restore', 'Ripristina')\"\n severity=\"secondary\"\n size=\"small\"\n (onClick)=\"restoreCustomShader()\">\n </p-button>\n }\n </div>\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.shader.hint', 'Uniform disponibile: uTime (secondi, animata). Errori GLSL in console.') }}\n </div>\n }\n </div>\n }\n\n <!-- F5: Binding rotta scaffoldata + field-map + open-record.\n Nascosto per il Mesh Repeater: il repeater binda per-record (config sua). -->\n @if (!selectedIsRepeater) {\n <div class=\"scene3d-binding-section\">\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.binding.title', 'Binding rotta') }}</div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-route\">{{ t('scene3d.binding.route', 'Rotta') }}</label>\n <p-select\n inputId=\"scene3d-binding-route\"\n [options]=\"routeOptions\"\n [(ngModel)]=\"bindingRoute\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"t('scene3d.binding.route_placeholder', 'Seleziona rotta...')\"\n appendTo=\"body\"\n styleClass=\"scene3d-binding-select\"\n (onChange)=\"onBindingRouteChange()\">\n </p-select>\n </div>\n\n @if (selectedHasBinding) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-record-id\">{{ t('scene3d.binding.record_id', 'Record ID (opzionale)') }}</label>\n <input\n id=\"scene3d-binding-record-id\"\n type=\"text\"\n [(ngModel)]=\"bindingRecordId\"\n (ngModelChange)=\"onBindingDetailChange()\"\n [placeholder]=\"t('scene3d.binding.record_id_placeholder', 'Vuoto = binding di rotta')\" />\n </div>\n\n <div class=\"scene3d-field-map\">\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.binding.field_map', 'Mappa campi') }}</div>\n\n @if (bindingColumnsLoading) {\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.binding.columns_loading', 'Caricamento colonne...') }}</div>\n }\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-map-label\">{{ t('scene3d.binding.map_label', 'Etichetta') }}</label>\n <p-select\n inputId=\"scene3d-binding-map-label\"\n [options]=\"bindingColumnOptions\"\n [(ngModel)]=\"bindingFieldMap.label\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"t('scene3d.binding.column_placeholder', 'Colonna...')\"\n appendTo=\"body\"\n styleClass=\"scene3d-binding-select\"\n (onChange)=\"onBindingDetailChange()\">\n </p-select>\n </div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-map-color\">{{ t('scene3d.binding.map_color', 'Colore') }}</label>\n <p-select\n inputId=\"scene3d-binding-map-color\"\n [options]=\"bindingColumnOptions\"\n [(ngModel)]=\"bindingFieldMap.color\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"t('scene3d.binding.column_placeholder', 'Colonna...')\"\n appendTo=\"body\"\n styleClass=\"scene3d-binding-select\"\n (onChange)=\"onBindingDetailChange()\">\n </p-select>\n </div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-map-visible\">{{ t('scene3d.binding.map_visible', 'Visibile') }}</label>\n <p-select\n inputId=\"scene3d-binding-map-visible\"\n [options]=\"bindingColumnOptions\"\n [(ngModel)]=\"bindingFieldMap.visible\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [filter]=\"true\"\n [showClear]=\"true\"\n [placeholder]=\"t('scene3d.binding.column_placeholder', 'Colonna...')\"\n appendTo=\"body\"\n styleClass=\"scene3d-binding-select\"\n (onChange)=\"onBindingDetailChange()\">\n </p-select>\n </div>\n </div>\n\n <p-button\n icon=\"pi pi-external-link\"\n [label]=\"t('scene3d.binding.open_record', 'Apri record')\"\n severity=\"secondary\"\n size=\"small\"\n styleClass=\"scene3d-open-record-btn\"\n (onClick)=\"openBoundRecord()\">\n </p-button>\n\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.binding.open_hint', 'Doppio click su un oggetto bindato per aprire la CRUD della rotta') }}\n </div>\n }\n </div>\n }\n\n <!-- Iter2 F2: animazioni / timeline (solo se il selezionato ha clip) -->\n @if (selectedHasAnimations) {\n <div class=\"scene3d-anim-section\">\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.anim.title', 'Animazioni') }}</div>\n\n @if (animClipOptions.length > 1) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-anim-clip\">{{ t('scene3d.anim.clip', 'Clip') }}</label>\n <p-select\n inputId=\"scene3d-anim-clip\"\n [options]=\"animClipOptions\"\n [(ngModel)]=\"selectedAnimClipIndex\"\n optionLabel=\"label\"\n optionValue=\"value\"\n appendTo=\"body\"\n styleClass=\"scene3d-binding-select\"\n (onChange)=\"onAnimClipChange()\">\n </p-select>\n </div>\n }\n\n <div class=\"scene3d-anim-transport\">\n <p-button\n icon=\"pi pi-play\"\n [title]=\"t('scene3d.anim.play', 'Play')\"\n severity=\"secondary\"\n size=\"small\"\n [disabled]=\"animPlaying\"\n (onClick)=\"animPlay()\">\n </p-button>\n <p-button\n icon=\"pi pi-pause\"\n [title]=\"t('scene3d.anim.pause', 'Pausa')\"\n severity=\"secondary\"\n size=\"small\"\n [disabled]=\"!animPlaying\"\n (onClick)=\"animPause()\">\n </p-button>\n <p-button\n icon=\"pi pi-stop\"\n [title]=\"t('scene3d.anim.stop', 'Stop')\"\n severity=\"secondary\"\n size=\"small\"\n (onClick)=\"animStop()\">\n </p-button>\n <p-button\n icon=\"pi pi-trash\"\n [title]=\"t('scene3d.anim.delete_clip', 'Elimina clip')\"\n severity=\"danger\"\n size=\"small\"\n (onClick)=\"deleteSelectedAnimClip()\">\n </p-button>\n </div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-anim-time\">{{ t('scene3d.anim.time', 'Tempo (s)') }}</label>\n <input\n id=\"scene3d-anim-time\"\n type=\"range\"\n class=\"scene3d-anim-scrubber\"\n min=\"0\"\n [max]=\"animDuration\"\n step=\"0.01\"\n [(ngModel)]=\"animScrubTime\"\n (ngModelChange)=\"onAnimScrub()\" />\n <div class=\"scene3d-anim-time\">{{ animScrubTime }} / {{ animDuration }}</div>\n </div>\n </div>\n }\n\n <!-- Mesh Repeater (DATAREPEATER 3D): una mesh per record, propriet\u00E0 bindate -->\n @if (selectedIsRepeater) {\n <div class=\"scene3d-material-section\">\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.repeater.title', 'Mesh Repeater') }}</div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-route\">{{ t('scene3d.repeater.route', 'Rotta') }}</label>\n <p-select\n inputId=\"scene3d-repeater-route\"\n [options]=\"routeOptions\" [(ngModel)]=\"repeaterCfg.route\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.route_ph', 'Seleziona rotta...')\"\n appendTo=\"body\" styleClass=\"scene3d-binding-select\"\n (onChange)=\"onRepeaterRouteChange()\">\n </p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-max\">{{ t('scene3d.repeater.max', 'Max record') }}</label>\n <input id=\"scene3d-repeater-max\" type=\"number\" min=\"1\" step=\"10\" [(ngModel)]=\"repeaterCfg.maxRecords\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-mesh-source\">{{ t('scene3d.repeater.mesh_source', 'Sorgente mesh') }}</label>\n <select id=\"scene3d-repeater-mesh-source\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.meshSource.mode\" (ngModelChange)=\"onRepeaterConfigChange()\">\n <option value=\"fixed\">{{ t('scene3d.repeater.fixed', 'Fissa') }}</option>\n <option value=\"column\">{{ t('scene3d.repeater.from_column', 'Da colonna') }}</option>\n <option value=\"asset\">{{ t('scene3d.repeater.from_asset', 'Asset (colonna)') }}</option>\n </select>\n </div>\n @if (repeaterCfg.meshSource.mode === 'fixed') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-mesh-type\">{{ t('scene3d.repeater.mesh_type', 'Tipo mesh') }}</label>\n <select id=\"scene3d-repeater-mesh-type\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.meshSource.type\" (ngModelChange)=\"onRepeaterConfigChange()\">\n @for (o of repeaterMeshTypeOptions; track o.value) { <option [value]=\"o.value\">{{ o.label }}</option> }\n </select>\n </div>\n } @else {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-mesh-column\">{{ repeaterCfg.meshSource.mode === 'asset'\n ? t('scene3d.repeater.asset_column', 'Colonna asset (upload GLB/OBJ/STL)')\n : t('scene3d.repeater.mesh_column', 'Colonna tipo') }}</label>\n <p-select inputId=\"scene3d-repeater-mesh-column\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"repeaterCfg.meshSource.column\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n appendTo=\"body\" styleClass=\"scene3d-binding-select\" (onChange)=\"onRepeaterConfigChange()\"></p-select>\n </div>\n }\n\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.repeater.field_map', 'Mappa campi \u2192 colonne') }}</div>\n @for (f of repeaterFields; track f.key) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" [attr.for]=\"'scene3d-rep-field-' + f.key\">{{ f.label }}</label>\n <p-select [inputId]=\"'scene3d-rep-field-' + f.key\" [options]=\"bindingColumnOptions\"\n [ngModel]=\"getFieldMapCol(f.key)\" (ngModelChange)=\"setFieldMapCol(f.key, $event)\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\"\n appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n }\n\n <!-- Opzioni texture (visibili solo con la colonna `texture` mappata):\n testo\u2192texture testuale, upload/img\u2192immagine, + modo di applicazione. -->\n @if (getFieldMapCol('texture')) {\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.repeater.texture_opts', 'Texture (colonna)') }}</div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-source\">{{ t('scene3d.repeater.texture_source', 'Sorgente') }}</label>\n <select id=\"scene3d-texture-source\" class=\"scene3d-material-select\" [(ngModel)]=\"textureApply.sourceKind\" (ngModelChange)=\"onTextureApplyChange()\">\n <option value=\"auto\">{{ t('scene3d.repeater.texture_auto', 'Auto (dal tipo colonna)') }}</option>\n <option value=\"text\">{{ t('scene3d.repeater.texture_text', 'Testo \u2192 texture') }}</option>\n <option value=\"image\">{{ t('scene3d.repeater.texture_image', 'Immagine / upload') }}</option>\n </select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-apply\">{{ t('scene3d.repeater.texture_apply', 'Applicazione') }}</label>\n <select id=\"scene3d-texture-apply\" class=\"scene3d-material-select\" [(ngModel)]=\"textureApply.wrap\" (ngModelChange)=\"onTextureApplyChange()\">\n <option value=\"stretch\">{{ t('scene3d.repeater.texture_stretch', 'Stretch (adatta)') }}</option>\n <option value=\"repeat\">{{ t('scene3d.repeater.texture_repeat', 'Ripeti') }}</option>\n <option value=\"mirror\">{{ t('scene3d.repeater.texture_mirror', 'Ripeti a specchio') }}</option>\n </select>\n </div>\n @if (textureApply.wrap !== 'stretch') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-repeat-x\">{{ t('scene3d.repeater.texture_repeat_xy', 'Ripetizioni X / Y') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-texture-repeat-x\" type=\"number\" step=\"0.5\" min=\"0.1\" [(ngModel)]=\"textureApply.repeatX\" (ngModelChange)=\"onTextureApplyChange()\" />\n <input type=\"number\" step=\"0.5\" min=\"0.1\" [(ngModel)]=\"textureApply.repeatY\" (ngModelChange)=\"onTextureApplyChange()\" />\n </div>\n </div>\n }\n @if (textureApply.sourceKind !== 'image') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-text-bg\">{{ t('scene3d.repeater.texture_text_colors', 'Colori testo (sfondo / testo)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-texture-text-bg\" type=\"color\" [(ngModel)]=\"textureApply.textBg\" (ngModelChange)=\"onTextureApplyChange()\" />\n <input type=\"color\" [(ngModel)]=\"textureApply.textFg\" (ngModelChange)=\"onTextureApplyChange()\" />\n </div>\n </div>\n }\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.repeater.texture_hint', 'Dopo aver cambiato le opzioni premi Rigenera per riapplicare le texture.') }}\n </div>\n }\n\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-layout\">{{ t('scene3d.repeater.layout', 'Layout (senza colonne posizione)') }}</label>\n <select id=\"scene3d-repeater-layout\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.layout.mode\" (ngModelChange)=\"onRepeaterLayoutModeChange()\">\n <optgroup [label]=\"t('scene3d.repeater.layout_simple', 'Semplici')\">\n <option value=\"grid\">Grid</option>\n <option value=\"cube\">{{ t('scene3d.repeater.cube', 'Cubo') }}</option>\n <option value=\"line\">{{ t('scene3d.repeater.line', 'Linea') }}</option>\n <option value=\"circle\">{{ t('scene3d.repeater.circle', 'Cerchio') }}</option>\n </optgroup>\n <optgroup [label]=\"t('scene3d.repeater.layout_archetypes', 'Archetipi')\">\n <option value=\"carousel\">{{ t('scene3d.repeater.carousel', 'Carousel 3D') }}</option>\n <option value=\"chart\">{{ t('scene3d.repeater.chart', 'Chart 3D') }}</option>\n <option value=\"surface\">{{ t('scene3d.repeater.surface', 'Surface / Heatmap') }}</option>\n <option value=\"gauge\">{{ t('scene3d.repeater.gauge', 'Gauge / KPI') }}</option>\n <option value=\"nodegraph\">{{ t('scene3d.repeater.nodegraph', 'Node graph') }}</option>\n <option value=\"tree\">{{ t('scene3d.repeater.tree', 'Albero (tree)') }}</option>\n <option value=\"map\">{{ t('scene3d.repeater.map', 'Mappa (pin)') }}</option>\n <option value=\"warehouse\">{{ t('scene3d.repeater.warehouse', 'Magazzino (scaffali)') }}</option>\n <option value=\"kanban\">{{ t('scene3d.repeater.kanban', 'Kanban 3D') }}</option>\n <option value=\"calendar\">{{ t('scene3d.repeater.calendar', 'Calendario') }}</option>\n <option value=\"stacked\">{{ t('scene3d.repeater.stacked', 'Barre impilate') }}</option>\n </optgroup>\n </select>\n </div>\n\n <!-- Archetipo Carousel 3D: sezioni collassabili Carousel / Disco / Frecce. -->\n @if (repeaterCfg.layout.mode === 'carousel') {\n <!-- Sezione Carousel (generale) -->\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('carousel')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('carousel')\" [class.pi-chevron-down]=\"!isSectionCollapsed('carousel')\"></i>\n {{ t('scene3d.repeater.carousel', 'Carousel 3D') }}\n </div>\n @if (!isSectionCollapsed('carousel')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-visible\">{{ t('scene3d.repeater.carousel_visible', 'Item visibili') }} ({{ carouselCfg.visible }})</label>\n <input id=\"scene3d-carousel-visible\" type=\"range\" min=\"1\" max=\"9\" step=\"1\" [(ngModel)]=\"carouselCfg.visible\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-radius\">{{ t('scene3d.repeater.carousel_radius', 'Curvatura (raggio)') }} ({{ carouselCfg.radius }})</label>\n <input id=\"scene3d-carousel-radius\" type=\"range\" min=\"2\" max=\"30\" step=\"0.5\" [(ngModel)]=\"carouselCfg.radius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-slot-angle\">{{ t('scene3d.repeater.carousel_slot_angle', 'Angolo tra slot') }} ({{ carouselCfg.slotAngleDeg }}\u00B0)</label>\n <input id=\"scene3d-carousel-slot-angle\" type=\"range\" min=\"8\" max=\"60\" step=\"1\" [(ngModel)]=\"carouselCfg.slotAngleDeg\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-focus-scale\">{{ t('scene3d.repeater.carousel_scales', 'Scala centro / lati') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-carousel-focus-scale\" type=\"number\" step=\"0.05\" min=\"0.1\" [(ngModel)]=\"carouselCfg.focusScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n <input type=\"number\" step=\"0.05\" min=\"0.1\" [(ngModel)]=\"carouselCfg.sideScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-mesh-y-offset\">{{ t('scene3d.repeater.carousel_mesh_y_offset', 'Offset verticale mesh') }} ({{ carouselCfg.meshYOffset ?? 0 }})</label>\n <input id=\"scene3d-carousel-mesh-y-offset\" type=\"range\" min=\"-5\" max=\"5\" step=\"0.1\" [(ngModel)]=\"carouselCfg.meshYOffset\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-mesh-rot-x\">{{ t('scene3d.repeater.carousel_mesh_rot', 'Rotazione mesh X / Y / Z (\u00B0)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-carousel-mesh-rot-x\" type=\"number\" step=\"15\" [(ngModel)]=\"carouselCfg.meshRotX\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n <input type=\"number\" step=\"15\" [(ngModel)]=\"carouselCfg.meshRotY\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n <input type=\"number\" step=\"15\" [(ngModel)]=\"carouselCfg.meshRotZ\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"carouselCfg.showBase\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.carousel_base', 'Base') }}\n </label>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"carouselCfg.showArrows\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.carousel_arrows', 'Frecce nav') }}\n </label>\n </div>\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.repeater.carousel_hint', 'Nel viewer le frecce 3D scorrono i record (item centrale a fuoco). Premi Rigenera dopo le modifiche.') }}\n </div>\n }\n\n <!-- Sezione Disco -->\n @if (carouselCfg.showBase) {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('carousel_disc')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('carousel_disc')\" [class.pi-chevron-down]=\"!isSectionCollapsed('carousel_disc')\"></i>\n {{ t('scene3d.repeater.carousel_disc', 'Disco') }}\n </div>\n @if (!isSectionCollapsed('carousel_disc')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-disc-size\">{{ t('scene3d.repeater.carousel_disc_size', 'Dimensione disco') }} ({{ carouselCfg.baseSize }})</label>\n <input id=\"scene3d-carousel-disc-size\" type=\"range\" min=\"0.2\" max=\"1.5\" step=\"0.02\" [(ngModel)]=\"carouselCfg.baseSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-color\">{{ t('scene3d.repeater.carousel_base_color', 'Colore base') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-carousel-base-color\" type=\"color\" [(ngModel)]=\"carouselCfg.baseColor\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-metalness\">{{ t('scene3d.repeater.carousel_metalness', 'Metalness') }} ({{ carouselCfg.baseMetalness }})</label>\n <input id=\"scene3d-carousel-base-metalness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.baseMetalness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-roughness\">{{ t('scene3d.repeater.carousel_roughness', 'Roughness') }} ({{ carouselCfg.baseRoughness }})</label>\n <input id=\"scene3d-carousel-base-roughness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.baseRoughness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-clearcoat\">{{ t('scene3d.repeater.carousel_clearcoat', 'Clearcoat') }} ({{ carouselCfg.baseClearcoat }})</label>\n <input id=\"scene3d-carousel-base-clearcoat\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.baseClearcoat\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n }\n }\n\n <!-- Sezione Frecce -->\n @if (carouselCfg.showArrows) {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('carousel_arrows')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('carousel_arrows')\" [class.pi-chevron-down]=\"!isSectionCollapsed('carousel_arrows')\"></i>\n {{ t('scene3d.repeater.carousel_arrows', 'Frecce nav') }}\n </div>\n @if (!isSectionCollapsed('carousel_arrows')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-size\">{{ t('scene3d.repeater.carousel_arrow_size', 'Dimensione frecce') }} ({{ carouselCfg.arrowSize }})</label>\n <input id=\"scene3d-carousel-arrow-size\" type=\"range\" min=\"0.3\" max=\"2.5\" step=\"0.05\" [(ngModel)]=\"carouselCfg.arrowSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-color\">{{ t('scene3d.repeater.carousel_arrow_color', 'Colore frecce') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-carousel-arrow-color\" type=\"color\" [(ngModel)]=\"carouselCfg.arrowColor\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-emissive\">{{ t('scene3d.repeater.carousel_arrow_glow', 'Bagliore (emissive)') }} ({{ carouselCfg.arrowEmissive }})</label>\n <input id=\"scene3d-carousel-arrow-emissive\" type=\"range\" min=\"0\" max=\"3\" step=\"0.05\" [(ngModel)]=\"carouselCfg.arrowEmissive\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-metalness\">{{ t('scene3d.repeater.carousel_metalness', 'Metalness') }} ({{ carouselCfg.arrowMetalness }})</label>\n <input id=\"scene3d-carousel-arrow-metalness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.arrowMetalness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-roughness\">{{ t('scene3d.repeater.carousel_roughness', 'Roughness') }} ({{ carouselCfg.arrowRoughness }})</label>\n <input id=\"scene3d-carousel-arrow-roughness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.arrowRoughness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-clearcoat\">{{ t('scene3d.repeater.carousel_clearcoat', 'Clearcoat') }} ({{ carouselCfg.arrowClearcoat }})</label>\n <input id=\"scene3d-carousel-arrow-clearcoat\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.arrowClearcoat\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n }\n }\n }\n\n <!-- Archetipo Chart 3D: bar->cilindri, pie->donut(torus), area->ribbon. -->\n @if (repeaterCfg.layout.mode === 'chart') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('chart')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('chart')\" [class.pi-chevron-down]=\"!isSectionCollapsed('chart')\"></i>\n {{ t('scene3d.repeater.chart', 'Chart 3D') }}\n </div>\n @if (!isSectionCollapsed('chart')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-type\">{{ t('scene3d.repeater.chart_type', 'Tipo chart') }}</label>\n <select id=\"scene3d-chart-type\" class=\"scene3d-material-select\" [(ngModel)]=\"chartCfg.type\" (ngModelChange)=\"onRepeaterConfigChange()\">\n <option value=\"bar\">{{ t('scene3d.repeater.chart_bar', 'Barre (cilindri)') }}</option>\n <option value=\"pie\">{{ t('scene3d.repeater.chart_pie', 'Torta (donut)') }}</option>\n <option value=\"area\">{{ t('scene3d.repeater.chart_area', 'Area (line/spline)') }}</option>\n <option value=\"funnel\">{{ t('scene3d.repeater.chart_funnel', 'Funnel (coni)') }}</option>\n <option value=\"radar\">{{ t('scene3d.repeater.chart_radar', 'Radar (stella)') }}</option>\n <option value=\"scatter\">{{ t('scene3d.repeater.chart_scatter', 'Scatter / bolle') }}</option>\n </select>\n </div>\n @if (chartCfg.type !== 'scatter') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select inputId=\"scene3d-chart-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n }\n @if (chartCfg.type === 'scatter') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-x\">{{ t('scene3d.repeater.chart_x', 'Colonna X') }}</label>\n <p-select inputId=\"scene3d-chart-x\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.xColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-y\">{{ t('scene3d.repeater.chart_y', 'Colonna Y (opz.)') }}</label>\n <p-select inputId=\"scene3d-chart-y\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.yColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-z\">{{ t('scene3d.repeater.chart_z', 'Colonna Z (opz.)') }}</label>\n <p-select inputId=\"scene3d-chart-z\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.zColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-size\">{{ t('scene3d.repeater.chart_size', 'Colonna dimensione (opz.)') }}</label>\n <p-select inputId=\"scene3d-chart-size\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.sizeColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.chart_size_ph', 'Costante')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n }\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-color-col\">{{ t('scene3d.repeater.chart_color_col', 'Colonna colore (opz.)') }}</label>\n <p-select inputId=\"scene3d-chart-color-col\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.colorColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.chart_palette', 'Palette automatica')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n @if (chartCfg.type === 'bar' || chartCfg.type === 'area' || chartCfg.type === 'funnel' || chartCfg.type === 'scatter') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-height\">{{ chartCfg.type === 'scatter' ? t('scene3d.repeater.chart_span', 'Estensione') : t('scene3d.repeater.chart_height', 'Altezza') }} ({{ chartCfg.height }})</label>\n <input id=\"scene3d-chart-height\" type=\"range\" min=\"1\" max=\"20\" step=\"0.5\" [(ngModel)]=\"chartCfg.height\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n }\n @if (chartCfg.type === 'bar' || chartCfg.type === 'area') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-spacing\">{{ t('scene3d.repeater.chart_spacing', 'Passo') }} ({{ chartCfg.spacing }})</label>\n <input id=\"scene3d-chart-spacing\" type=\"range\" min=\"0.5\" max=\"5\" step=\"0.1\" [(ngModel)]=\"chartCfg.spacing\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n }\n @if (chartCfg.type === 'bar' || chartCfg.type === 'scatter') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-bar-radius\">{{ chartCfg.type === 'scatter' ? t('scene3d.repeater.chart_bubble', 'Raggio bolla') : t('scene3d.repeater.chart_bar_radius', 'Raggio barra') }} ({{ chartCfg.barRadius }})</label>\n <input id=\"scene3d-chart-bar-radius\" type=\"range\" min=\"0.1\" max=\"2\" step=\"0.05\" [(ngModel)]=\"chartCfg.barRadius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n }\n @if (chartCfg.type === 'pie' || chartCfg.type === 'funnel' || chartCfg.type === 'radar') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-radius\">{{ t('scene3d.repeater.chart_radius', 'Raggio') }} ({{ chartCfg.radius }})</label>\n <input id=\"scene3d-chart-radius\" type=\"range\" min=\"1\" max=\"20\" step=\"0.5\" [(ngModel)]=\"chartCfg.radius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n }\n @if (chartCfg.type === 'pie' || chartCfg.type === 'area') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-thickness\">{{ chartCfg.type === 'pie' ? t('scene3d.repeater.chart_tube', 'Spessore donut') : t('scene3d.repeater.chart_depth', 'Profondita\\' area') }} ({{ chartCfg.thickness }})</label>\n <input id=\"scene3d-chart-thickness\" type=\"range\" min=\"0.1\" max=\"5\" step=\"0.1\" [(ngModel)]=\"chartCfg.thickness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n }\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.chart_labels', 'Etichette') }}\n </label>\n @if (chartCfg.type === 'bar' || chartCfg.type === 'area') {\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.showAxes\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.chart_axes', 'Assi') }}\n </label>\n }\n @if (chartCfg.type === 'bar' || chartCfg.type === 'pie') {\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.showLegend\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.chart_legend', 'Legenda') }}\n </label>\n }\n @if (chartCfg.type === 'area') {\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.smooth\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.chart_smooth', 'Smoothing spline') }}\n </label>\n }\n </div>\n @if (chartCfg.showLabels) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-label-col\">{{ t('scene3d.repeater.chart_label_col', 'Colonna etichetta (opz.)') }}</label>\n <p-select inputId=\"scene3d-chart-label-col\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\n [placeholder]=\"t('scene3d.repeater.chart_label_val', 'Usa il valore')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n }\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.repeater.chart_hint', 'La colonna valore guida l\\'altezza (barre/area) o la quota (torta). Premi Rigenera dopo le modifiche.') }}\n </div>\n }\n }\n\n <!-- Archetipo Surface / Heatmap (matrice riga\u00D7colonna\u00D7valore) -->\n @if (repeaterCfg.layout.mode === 'surface') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('surface')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('surface')\" [class.pi-chevron-down]=\"!isSectionCollapsed('surface')\"></i>\n {{ t('scene3d.repeater.surface', 'Surface / Heatmap') }}\n </div>\n @if (!isSectionCollapsed('surface')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-row\">{{ t('scene3d.repeater.surf_row', 'Colonna riga') }}</label>\n <p-select inputId=\"scene3d-surf-row\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"surfaceCfg.rowColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-col\">{{ t('scene3d.repeater.surf_col', 'Colonna colonna') }}</label>\n <p-select inputId=\"scene3d-surf-col\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"surfaceCfg.colColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select inputId=\"scene3d-surf-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"surfaceCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-height\">{{ t('scene3d.repeater.surf_height', 'Scala altezza') }} ({{ surfaceCfg.heightScale }})</label>\n <input id=\"scene3d-surf-height\" type=\"range\" min=\"1\" max=\"20\" step=\"0.5\" [(ngModel)]=\"surfaceCfg.heightScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-cell\">{{ t('scene3d.repeater.surf_cell', 'Dimensione cella') }} ({{ surfaceCfg.cellSize }})</label>\n <input id=\"scene3d-surf-cell\" type=\"range\" min=\"0.4\" max=\"4\" step=\"0.1\" [(ngModel)]=\"surfaceCfg.cellSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-color-low\">{{ t('scene3d.repeater.surf_colors', 'Colori (basso / alto)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-surf-color-low\" type=\"color\" [(ngModel)]=\"surfaceCfg.colorLow\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n <input type=\"color\" [(ngModel)]=\"surfaceCfg.colorHigh\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"surfaceCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.chart_labels', 'Etichette') }}\n </label>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.surf_hint', 'Riga \u00D7 colonna = celle; valore \u2192 altezza e colore. Premi Rigenera.') }}</div>\n }\n }\n\n <!-- Archetipo Gauge / KPI (valore singolo dal primo record) -->\n @if (repeaterCfg.layout.mode === 'gauge') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('gauge')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('gauge')\" [class.pi-chevron-down]=\"!isSectionCollapsed('gauge')\"></i>\n {{ t('scene3d.repeater.gauge', 'Gauge / KPI') }}\n </div>\n @if (!isSectionCollapsed('gauge')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select inputId=\"scene3d-gauge-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"gaugeCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-label\">{{ t('scene3d.repeater.gauge_label', 'Colonna etichetta (opz.)') }}</label>\n <p-select inputId=\"scene3d-gauge-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"gaugeCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_label_val', 'Usa il valore')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-min\">{{ t('scene3d.repeater.gauge_range', 'Min / Max') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-gauge-min\" type=\"number\" step=\"1\" [(ngModel)]=\"gaugeCfg.min\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n <input type=\"number\" step=\"1\" [(ngModel)]=\"gaugeCfg.max\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-radius\">{{ t('scene3d.repeater.chart_radius', 'Raggio') }} ({{ gaugeCfg.radius }})</label>\n <input id=\"scene3d-gauge-radius\" type=\"range\" min=\"1\" max=\"12\" step=\"0.5\" [(ngModel)]=\"gaugeCfg.radius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.gauge_hint', 'Usa il valore del PRIMO record. Premi Rigenera.') }}</div>\n }\n }\n\n <!-- Archetipi Node-graph / Tree (gerarchia id \u2192 parent_id) -->\n @if (repeaterCfg.layout.mode === 'nodegraph' || repeaterCfg.layout.mode === 'tree') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('graph')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('graph')\" [class.pi-chevron-down]=\"!isSectionCollapsed('graph')\"></i>\n {{ repeaterCfg.layout.mode === 'tree' ? t('scene3d.repeater.tree', 'Albero (tree)') : t('scene3d.repeater.nodegraph', 'Node graph') }}\n </div>\n @if (!isSectionCollapsed('graph')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-id\">{{ t('scene3d.repeater.graph_id', 'Colonna ID') }}</label>\n <p-select inputId=\"scene3d-graph-id\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.idColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-parent\">{{ t('scene3d.repeater.graph_parent', 'Colonna padre') }}</label>\n <p-select inputId=\"scene3d-graph-parent\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.parentColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-label\">{{ t('scene3d.repeater.graph_label', 'Colonna etichetta') }}</label>\n <p-select inputId=\"scene3d-graph-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-value\">{{ t('scene3d.repeater.graph_value', 'Colonna valore (dim. nodo)') }}</label>\n <p-select inputId=\"scene3d-graph-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_palette', 'Palette automatica')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-nodesize\">{{ t('scene3d.repeater.graph_nodesize', 'Dimensione nodo') }} ({{ graphCfg.nodeSize }})</label>\n <input id=\"scene3d-graph-nodesize\" type=\"range\" min=\"0.2\" max=\"2\" step=\"0.05\" [(ngModel)]=\"graphCfg.nodeSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-spacing\">{{ t('scene3d.repeater.graph_spacing', 'Spaziatura') }} ({{ graphCfg.spacing }})</label>\n <input id=\"scene3d-graph-spacing\" type=\"range\" min=\"1\" max=\"10\" step=\"0.5\" [(ngModel)]=\"graphCfg.spacing\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-linkcolor\">{{ t('scene3d.repeater.graph_linkcolor', 'Colore archi') }}</label>\n <div class=\"scene3d-vector-row\"><input id=\"scene3d-graph-linkcolor\" type=\"color\" [(ngModel)]=\"graphCfg.linkColor\" (ngModelChange)=\"onRepeaterConfigChange()\" /></div>\n </div>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\n <input type=\"checkbox\" [(ngModel)]=\"graphCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n {{ t('scene3d.repeater.chart_labels', 'Etichette') }}\n </label>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.graph_hint', 'ID + padre definiscono la gerarchia; valore \u2192 dimensione nodo. Premi Rigenera.') }}</div>\n }\n }\n\n <!-- Archetipo Map-pins (lat/lon \u2192 pin su piano) -->\n @if (repeaterCfg.layout.mode === 'map') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('map')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('map')\" [class.pi-chevron-down]=\"!isSectionCollapsed('map')\"></i>\n {{ t('scene3d.repeater.map', 'Mappa (pin)') }}\n </div>\n @if (!isSectionCollapsed('map')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-lat\">{{ t('scene3d.repeater.map_lat', 'Colonna latitudine') }}</label>\n <p-select inputId=\"scene3d-map-lat\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.latColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-lon\">{{ t('scene3d.repeater.map_lon', 'Colonna longitudine') }}</label>\n <p-select inputId=\"scene3d-map-lon\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.lonColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select inputId=\"scene3d-map-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_size_ph', 'Costante')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-label\">{{ t('scene3d.repeater.map_label', 'Colonna etichetta (opz.)') }}</label>\n <p-select inputId=\"scene3d-map-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-size\">{{ t('scene3d.repeater.map_size', 'Dimensione mappa') }} ({{ mapCfg.planeSize }})</label>\n <input id=\"scene3d-map-size\" type=\"range\" min=\"6\" max=\"50\" step=\"1\" [(ngModel)]=\"mapCfg.planeSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-pincolor\">{{ t('scene3d.repeater.map_pincolor', 'Colore pin (senza valore)') }}</label>\n <div class=\"scene3d-vector-row\"><input id=\"scene3d-map-pincolor\" type=\"color\" [(ngModel)]=\"mapCfg.pinColor\" (ngModelChange)=\"onRepeaterConfigChange()\" /></div>\n </div>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"mapCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.map_hint', 'Lat/lon \u2192 pin sul piano; valore \u2192 altezza/colore. Premi Rigenera.') }}</div>\n }\n }\n\n <!-- Archetipo Warehouse-shelf (corsia \u00D7 ripiano \u00D7 quantita') -->\n @if (repeaterCfg.layout.mode === 'warehouse') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('warehouse')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('warehouse')\" [class.pi-chevron-down]=\"!isSectionCollapsed('warehouse')\"></i>\n {{ t('scene3d.repeater.warehouse', 'Magazzino (scaffali)') }}\n </div>\n @if (!isSectionCollapsed('warehouse')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-aisle\">{{ t('scene3d.repeater.wh_aisle', 'Colonna corsia') }}</label>\n <p-select inputId=\"scene3d-wh-aisle\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.aisleColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-shelf\">{{ t('scene3d.repeater.wh_shelf', 'Colonna ripiano') }}</label>\n <p-select inputId=\"scene3d-wh-shelf\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.shelfColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-qty\">{{ t('scene3d.repeater.wh_qty', 'Colonna quantit\u00E0') }}</label>\n <p-select inputId=\"scene3d-wh-qty\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.qtyColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-label\">{{ t('scene3d.repeater.wh_label', 'Colonna articolo (opz.)') }}</label>\n <p-select inputId=\"scene3d-wh-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-box\">{{ t('scene3d.repeater.wh_box', 'Dimensione scatola') }} ({{ warehouseCfg.boxSize }})</label>\n <input id=\"scene3d-wh-box\" type=\"range\" min=\"0.4\" max=\"3\" step=\"0.1\" [(ngModel)]=\"warehouseCfg.boxSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"warehouseCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.wh_hint', 'Corsia \u00D7 ripiano = posizione; quantit\u00E0 \u2192 altezza scatola. Premi Rigenera.') }}</div>\n }\n }\n\n <!-- Archetipo Kanban 3D (stato \u2192 colonne di card) -->\n @if (repeaterCfg.layout.mode === 'kanban') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('kanban')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('kanban')\" [class.pi-chevron-down]=\"!isSectionCollapsed('kanban')\"></i>\n {{ t('scene3d.repeater.kanban', 'Kanban 3D') }}\n </div>\n @if (!isSectionCollapsed('kanban')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-kb-status\">{{ t('scene3d.repeater.kb_status', 'Colonna stato') }}</label>\n <p-select inputId=\"scene3d-kb-status\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"kanbanCfg.statusColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-kb-label\">{{ t('scene3d.repeater.kb_label', 'Colonna titolo (opz.)') }}</label>\n <p-select inputId=\"scene3d-kb-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"kanbanCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-kb-card\">{{ t('scene3d.repeater.kb_card', 'Larghezza card') }} ({{ kanbanCfg.cardWidth }})</label>\n <input id=\"scene3d-kb-card\" type=\"range\" min=\"0.8\" max=\"4\" step=\"0.1\" [(ngModel)]=\"kanbanCfg.cardWidth\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"kanbanCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.kb_hint', 'Ogni stato = una colonna; le card si impilano. Premi Rigenera.') }}</div>\n }\n }\n\n <!-- Archetipo Calendar-wall (data \u2192 griglia settimane\u00D7giorni) -->\n @if (repeaterCfg.layout.mode === 'calendar') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('calendar')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('calendar')\" [class.pi-chevron-down]=\"!isSectionCollapsed('calendar')\"></i>\n {{ t('scene3d.repeater.calendar', 'Calendario') }}\n </div>\n @if (!isSectionCollapsed('calendar')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-date\">{{ t('scene3d.repeater.cal_date', 'Colonna data') }}</label>\n <p-select inputId=\"scene3d-cal-date\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"calendarCfg.dateColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select inputId=\"scene3d-cal-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"calendarCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_size_ph', 'Costante')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-height\">{{ t('scene3d.repeater.surf_height', 'Scala altezza') }} ({{ calendarCfg.heightScale }})</label>\n <input id=\"scene3d-cal-height\" type=\"range\" min=\"1\" max=\"15\" step=\"0.5\" [(ngModel)]=\"calendarCfg.heightScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-cell\">{{ t('scene3d.repeater.surf_cell', 'Dimensione cella') }} ({{ calendarCfg.cellSize }})</label>\n <input id=\"scene3d-cal-cell\" type=\"range\" min=\"0.4\" max=\"3\" step=\"0.1\" [(ngModel)]=\"calendarCfg.cellSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"calendarCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.cal_hint', 'Le date riempiono una griglia settimane\u00D7giorni; valore \u2192 altezza/colore. Premi Rigenera.') }}</div>\n }\n }\n\n <!-- Archetipo Stacked bars (categoria \u00D7 serie) -->\n @if (repeaterCfg.layout.mode === 'stacked') {\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('stacked')\">\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('stacked')\" [class.pi-chevron-down]=\"!isSectionCollapsed('stacked')\"></i>\n {{ t('scene3d.repeater.stacked', 'Barre impilate') }}\n </div>\n @if (!isSectionCollapsed('stacked')) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-category\">{{ t('scene3d.repeater.st_category', 'Colonna categoria') }}</label>\n <p-select inputId=\"scene3d-st-category\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"stackedCfg.categoryColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-series\">{{ t('scene3d.repeater.st_series', 'Colonna serie') }}</label>\n <p-select inputId=\"scene3d-st-series\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"stackedCfg.seriesColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select inputId=\"scene3d-st-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"stackedCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-height\">{{ t('scene3d.repeater.chart_height', 'Altezza') }} ({{ stackedCfg.height }})</label>\n <input id=\"scene3d-st-height\" type=\"range\" min=\"2\" max=\"20\" step=\"0.5\" [(ngModel)]=\"stackedCfg.height\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-bar-width\">{{ t('scene3d.repeater.chart_bar_radius', 'Larghezza barra') }} ({{ stackedCfg.barWidth }})</label>\n <input id=\"scene3d-st-bar-width\" type=\"range\" min=\"0.4\" max=\"3\" step=\"0.1\" [(ngModel)]=\"stackedCfg.barWidth\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"stackedCfg.showLegend\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_legend', 'Legenda') }}</label>\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"stackedCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.st_hint', 'Categoria = colonna; serie = segmenti impilati. Premi Rigenera.') }}</div>\n }\n }\n\n @if (repeaterCfg.layout.mode === 'grid' || repeaterCfg.layout.mode === 'cube') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-order\">{{ t('scene3d.repeater.order', 'Ordine di flusso') }}</label>\n <select id=\"scene3d-repeater-order\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.layout.order\" (ngModelChange)=\"onRepeaterConfigChange()\">\n @for (o of layoutOrderOptions; track o.value) { <option [value]=\"o.value\">{{ o.label }}</option> }\n </select>\n </div>\n }\n <!-- Verso di riempimento per-asse (o senso di rotazione per il cerchio). -->\n @for (ax of layoutAxes; track ax) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" [attr.for]=\"'scene3d-axis-dir-' + ax\">{{ axisDirLabel(ax) }}</label>\n <select class=\"scene3d-material-select\" [id]=\"'scene3d-axis-dir-' + ax\" [ngModel]=\"getAxisDir(ax)\" (ngModelChange)=\"setAxisDir(ax, $event)\">\n @for (o of axisDirOptions(ax); track o.value) { <option [ngValue]=\"o.value\">{{ o.label }}</option> }\n </select>\n </div>\n }\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-pos-range\">{{ t('scene3d.repeater.pos_range', 'Range posizione') }} ({{ repeaterCfg.normalize.posRange }})</label>\n <input id=\"scene3d-repeater-pos-range\" type=\"range\" min=\"4\" max=\"200\" step=\"2\" [(ngModel)]=\"repeaterCfg.normalize.posRange\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-scale-min\">{{ t('scene3d.repeater.scale_range', 'Scala min/max') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-repeater-scale-min\" type=\"number\" step=\"0.1\" min=\"0.01\" [(ngModel)]=\"repeaterCfg.normalize.scaleMin\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n <input type=\"number\" step=\"0.1\" min=\"0.01\" [(ngModel)]=\"repeaterCfg.normalize.scaleMax\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n\n <p-button\n icon=\"pi pi-refresh\"\n [label]=\"t('scene3d.repeater.regenerate', 'Rigenera') + (repeaterCount ? ' (' + repeaterCount + ')' : '')\"\n size=\"small\" [loading]=\"repeaterRegenerating\"\n (onClick)=\"regenerateSelectedRepeater()\">\n </p-button>\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.repeater.hint', 'Le mesh sono rigenerate dai dati (non salvate nel JSON). Sposta il gruppo coi gizmo; click su una mesh apre la CRUD del record nel viewer.') }}\n </div>\n\n <!-- Interazioni: callback JS su click/dblclick/rightclick di un item.\n Sezione collassabile in fondo (collassata di default). -->\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"callbacksSectionOpen = !callbacksSectionOpen\">\n <i class=\"pi\" [class.pi-chevron-right]=\"!callbacksSectionOpen\" [class.pi-chevron-down]=\"callbacksSectionOpen\"></i>\n {{ t('scene3d.repeater.interactions', 'Interazioni (callback JS)') }}\n </div>\n @if (callbacksSectionOpen) {\n <div class=\"scene3d-prop-group\">\n <p-button size=\"small\" [text]=\"true\"\n [severity]=\"repeaterCallbackIsSet('click') ? 'primary' : 'secondary'\"\n icon=\"pi pi-code\"\n [label]=\"(repeaterCallbackIsSet('click') ? '\u25CF ' : '') + t('scene3d.repeater.cb_click', 'Click')\"\n (onClick)=\"editRepeaterCallback('click')\"></p-button>\n <p-button size=\"small\" [text]=\"true\"\n [severity]=\"repeaterCallbackIsSet('dblclick') ? 'primary' : 'secondary'\"\n icon=\"pi pi-code\"\n [label]=\"(repeaterCallbackIsSet('dblclick') ? '\u25CF ' : '') + t('scene3d.repeater.cb_dblclick', 'Doppio-click')\"\n (onClick)=\"editRepeaterCallback('dblclick')\"></p-button>\n <p-button size=\"small\" [text]=\"true\"\n [severity]=\"repeaterCallbackIsSet('rightclick') ? 'primary' : 'secondary'\"\n icon=\"pi pi-code\"\n [label]=\"(repeaterCallbackIsSet('rightclick') ? '\u25CF ' : '') + t('scene3d.repeater.cb_rightclick', 'Tasto destro')\"\n (onClick)=\"editRepeaterCallback('rightclick')\"></p-button>\n </div>\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.cb_hint', 'Nel callback: record (item), mesh, wuic.showItemPopup({mode,fields,title}). Vale in designer e viewer.') }}</div>\n }\n </div>\n }\n\n <!-- Fisica per-oggetto (Rapier): corpo rigido + parametri -->\n @if (selectedIsMesh) {\n <div class=\"scene3d-material-section\">\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.physics.title', 'Fisica') }}</div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-body\">{{ t('scene3d.physics.body', 'Corpo') }}</label>\n <select id=\"scene3d-physics-body\" class=\"scene3d-material-select\" [(ngModel)]=\"propPhysics.type\" (ngModelChange)=\"onPhysicsInputChange()\">\n <option value=\"none\">{{ t('scene3d.physics.none', 'Nessuno') }}</option>\n <option value=\"static\">{{ t('scene3d.physics.static', 'Statico') }}</option>\n <option value=\"dynamic\">{{ t('scene3d.physics.dynamic', 'Dinamico') }}</option>\n <option value=\"kinematic\">{{ t('scene3d.physics.kinematic', 'Cinematico') }}</option>\n </select>\n </div>\n @if (propPhysics.type !== 'none') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-shape\">{{ t('scene3d.physics.shape', 'Forma collider') }}</label>\n <select id=\"scene3d-physics-shape\" class=\"scene3d-material-select\" [(ngModel)]=\"propPhysics.shape\" (ngModelChange)=\"onPhysicsInputChange()\">\n <option value=\"auto\">{{ t('scene3d.physics.shape_auto', 'Auto (dalla geometria)') }}</option>\n <option value=\"box\">Box</option>\n <option value=\"sphere\">{{ t('scene3d.physics.shape_sphere', 'Sfera') }}</option>\n <option value=\"hull\">{{ t('scene3d.physics.shape_hull', 'Convex hull') }}</option>\n </select>\n </div>\n @if (propPhysics.type === 'dynamic') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-mass\">{{ t('scene3d.physics.mass', 'Massa (kg)') }}</label>\n <input id=\"scene3d-physics-mass\" type=\"number\" step=\"0.1\" min=\"0.001\" [(ngModel)]=\"propPhysics.mass\" (ngModelChange)=\"onPhysicsInputChange()\" />\n </div>\n }\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-friction\">{{ t('scene3d.physics.friction', 'Attrito') }} ({{ propPhysics.friction }})</label>\n <input id=\"scene3d-physics-friction\" type=\"range\" min=\"0\" max=\"1\" step=\"0.05\" [(ngModel)]=\"propPhysics.friction\" (ngModelChange)=\"onPhysicsInputChange()\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-restitution\">{{ t('scene3d.physics.restitution', 'Rimbalzo') }} ({{ propPhysics.restitution }})</label>\n <input id=\"scene3d-physics-restitution\" type=\"range\" min=\"0\" max=\"1\" step=\"0.05\" [(ngModel)]=\"propPhysics.restitution\" (ngModelChange)=\"onPhysicsInputChange()\" />\n </div>\n }\n </div>\n }\n\n <!-- Registrazione animazione: keyframe pos/rot/scala -> AnimationClip -->\n <div class=\"scene3d-anim-section\">\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"animRecSectionOpen = !animRecSectionOpen\">\n <i class=\"pi\" [class.pi-chevron-right]=\"!animRecSectionOpen\" [class.pi-chevron-down]=\"animRecSectionOpen\"></i>\n {{ t('scene3d.animrec.title', 'Registra animazione') }}\n </div>\n @if (animRecSectionOpen) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-animrec-clip-name\">{{ t('scene3d.animrec.clip_name', 'Nome clip') }}</label>\n <input id=\"scene3d-animrec-clip-name\" type=\"text\" [(ngModel)]=\"animRecClipName\" [placeholder]=\"t('scene3d.animrec.clip_name_ph', 'Clip 1')\" />\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\" for=\"scene3d-animrec-time\">{{ t('scene3d.animrec.time', 'Tempo T (s)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input id=\"scene3d-animrec-time\" type=\"number\" step=\"0.1\" min=\"0\" [(ngModel)]=\"animRecTime\" />\n <p-button\n icon=\"pi pi-plus\"\n [label]=\"t('scene3d.animrec.add', 'Aggiungi keyframe')\"\n size=\"small\"\n (onClick)=\"addRecKeyframe()\">\n </p-button>\n </div>\n </div>\n\n @if (animRecKeyframes.length > 0) {\n <div class=\"scene3d-kf-list\">\n @for (kf of animRecKeyframes; track kf.time; let i = $index) {\n <div class=\"scene3d-kf-row\">\n <button type=\"button\" class=\"scene3d-kf-apply\" (click)=\"applyRecKeyframe(i)\"\n [title]=\"t('scene3d.animrec.apply', 'Applica questa posa all\\'oggetto')\">\n <i class=\"pi pi-eye\"></i> t = {{ kf.time }}s\n </button>\n <button type=\"button\" class=\"scene3d-kf-delete\" (click)=\"removeRecKeyframe(i)\"\n [title]=\"t('designer.remove', 'Rimuovi')\">\n <i class=\"pi pi-trash\"></i>\n </button>\n </div>\n }\n </div>\n }\n\n <p-button\n icon=\"pi pi-video\"\n [label]=\"t('scene3d.animrec.generate', 'Genera clip')\"\n size=\"small\"\n [disabled]=\"animRecKeyframes.length < 2\"\n (onClick)=\"generateRecClip()\">\n </p-button>\n <div class=\"scene3d-binding-hint\">\n {{ t('scene3d.animrec.hint', 'Posa l\\'oggetto coi gizmo, aggiungi un keyframe al tempo T, ripeti. La clip generata usa il transport qui sopra e persiste col salvataggio (autoplay nel viewer).') }}\n </div>\n }\n </div>\n\n <div class=\"scene3d-gizmo-hint\">\n {{ t('scene3d.props.gizmo_hint', 'g = sposta, r = ruota, s = scala') }}\n </div>\n } @else {\n <div class=\"scene3d-props-empty\">\n {{ t('scene3d.props.empty', 'Nessun oggetto selezionato') }}\n </div>\n }\n </div>\n\n </div>\n\n <!-- Footer componenti scena (mirror del footer del designer 2D):\n click = seleziona, right-click = ctx menu con Rimuovi. -->\n <div class=\"scene3d-footer\">\n <span class=\"scene3d-footer-label\">{{ t('scene3d.footer.title', 'Scena') }}:</span>\n <div class=\"scene3d-footer-list\">\n @for (item of sceneObjects; track item.uuid) {\n <button\n type=\"button\"\n class=\"scene3d-footer-item\"\n [class.active]=\"item.uuid === selectedObjectUuid\"\n [title]=\"item.label\"\n (click)=\"selectFromFooter(item.uuid)\"\n (contextmenu)=\"showFooterContextMenu($event, item.uuid, sceneFooterCtxMenu)\">\n {{ item.label }}\n </button>\n } @empty {\n <span class=\"scene3d-footer-empty\">{{ t('scene3d.footer.empty', 'Nessun componente nella scena') }}</span>\n }\n </div>\n <p-contextMenu #sceneFooterCtxMenu [model]=\"footerCtxItems\" appendTo=\"body\" />\n </div>\n</div>\n", styles: [":host{display:block;width:100%;height:100%;min-height:0}.scene3d-designer-shell{display:flex;flex-direction:column;width:100%;height:100%;min-height:480px}.scene3d-toolbar{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid var(--surface-border, #dee2e6);flex:0 0 auto;flex-wrap:wrap}.scene3d-load-select{min-width:14rem}.scene3d-body{display:flex;flex:1 1 auto;min-height:0}.scene3d-palette{flex:0 0 12rem;display:flex;flex-direction:column;gap:.25rem;padding:.5rem;overflow-y:auto;border-right:1px solid var(--surface-border, #dee2e6)}.scene3d-panel-title{font-weight:600;font-size:.85rem;text-transform:uppercase;opacity:.7;margin-bottom:.25rem}.scene3d-palette-item{display:flex;align-items:center;gap:.5rem;padding:.4rem .5rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;background:var(--surface-card, #fff);cursor:pointer;text-align:left;font-size:.85rem;color:inherit}.scene3d-palette-item:hover:not(:disabled){background:var(--surface-hover, #f5f5f5)}.scene3d-palette-item:disabled{opacity:.45;cursor:not-allowed}.scene3d-palette-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.scene3d-viewport{flex:1 1 auto;min-width:0;position:relative;overflow:hidden;background:#1e1e28}.scene3d-viewport canvas{display:block;width:100%;height:100%}.scene3d-properties{flex:0 0 16rem;display:flex;flex-direction:column;gap:.6rem;padding:.75rem;overflow-y:auto;border-left:1px solid var(--surface-border, #dee2e6)}.scene3d-prop-group{display:flex;flex-direction:column;gap:.25rem}.scene3d-prop-label{font-size:.8rem;font-weight:600;opacity:.8}.scene3d-prop-readonly{font-size:.85rem;opacity:.7;word-break:break-all}.scene3d-vector-row{display:flex;gap:.25rem}.scene3d-vector-row input{width:100%;min-width:0;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem}.scene3d-props-empty,.scene3d-gizmo-hint{font-size:.8rem;opacity:.6}.scene3d-prop-checkbox{display:inline-flex;align-items:center;gap:.4rem;cursor:pointer}.scene3d-kf-list{display:flex;flex-direction:column;gap:3px;max-height:160px;overflow-y:auto;border:1px dashed var(--surface-border, #dee2e6);border-radius:4px;padding:4px}.scene3d-kf-row{display:flex;align-items:center;justify-content:space-between;gap:6px}.scene3d-kf-apply,.scene3d-kf-delete{border:1px solid var(--surface-border, #dee2e6);background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.78rem;cursor:pointer;display:inline-flex;align-items:center;gap:4px}.scene3d-kf-apply{flex:1 1 auto;justify-content:flex-start}.scene3d-kf-apply:hover{border-color:#3b82f6}.scene3d-kf-delete:hover{border-color:#ef4444;color:#ef4444}.scene3d-anim-section input[type=text],.scene3d-anim-section input[type=number]{width:100%;min-width:0;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem}.scene3d-material-section{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6)}.scene3d-material-section input[type=range]{width:100%}.scene3d-material-select{width:100%;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem;background:var(--surface-card, #fff);color:inherit}.scene3d-shader-toggle{cursor:pointer;-webkit-user-select:none;user-select:none}.scene3d-shader-src{width:100%;min-width:0;resize:vertical;padding:.35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-family:Consolas,Monaco,monospace;font-size:.72rem;line-height:1.35;background:#14141c;color:#d1d5db}.scene3d-pt-hud{position:absolute;top:8px;left:50%;transform:translate(-50%);z-index:25;display:flex;align-items:center;gap:12px;padding:4px 12px;border-radius:6px;background:#14141cd9;color:#e5e7eb;font-size:.78rem;border:1px solid #3b82f6}.scene3d-pt-scale{display:inline-flex;align-items:center;gap:6px}.scene3d-pt-scale input[type=range]{width:90px}.scene3d-pt-exit{border:1px solid #3b82f6;background:transparent;color:#93c5fd;border-radius:4px;padding:2px 8px;font-size:.75rem;cursor:pointer}.scene3d-cam-preview{position:absolute;z-index:20;width:320px;border:1px solid #3b82f6;border-radius:6px;background:#14141c;box-shadow:0 6px 24px #00000073;overflow:hidden}.scene3d-cam-preview-header{display:flex;align-items:center;justify-content:space-between;padding:3px 8px;background:#1f2937;color:#e5e7eb;font-size:.75rem;cursor:move;-webkit-user-select:none;user-select:none}.scene3d-cam-preview-close{border:none;background:transparent;color:#e5e7eb;font-size:1rem;line-height:1;cursor:pointer;padding:0 2px}.scene3d-cam-preview-body{width:320px;height:192px}.scene3d-cam-preview-body canvas{display:block}.scene3d-gizmo-hint{margin-top:.5rem;padding-top:.5rem;border-top:1px dashed var(--surface-border, #dee2e6)}.scene3d-binding-section{display:flex;flex-direction:column;gap:.6rem;margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6)}.scene3d-panel-subtitle{font-weight:600;font-size:.78rem;text-transform:uppercase;opacity:.7}.scene3d-panel-subtitle.scene3d-collapsible{display:flex;align-items:center;gap:.4rem;cursor:pointer;-webkit-user-select:none;user-select:none;padding:.15rem 0;opacity:.85}.scene3d-panel-subtitle.scene3d-collapsible:hover{opacity:1}.scene3d-panel-subtitle.scene3d-collapsible>.pi{font-size:.7rem;opacity:.8}.scene3d-field-map{display:flex;flex-direction:column;gap:.5rem;padding:.5rem;border:1px dashed var(--surface-border, #dee2e6);border-radius:4px}.scene3d-binding-select{width:100%}.scene3d-binding-section input[type=text]{width:100%;min-width:0;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem}.scene3d-open-record-btn{align-self:flex-start}.scene3d-binding-hint{font-size:.78rem;opacity:.6}.scene3d-anim-section{display:flex;flex-direction:column;gap:.6rem;margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6)}.scene3d-anim-transport{display:flex;gap:.35rem}.scene3d-anim-scrubber{width:100%;min-width:0}.scene3d-anim-time{font-size:.78rem;opacity:.6;text-align:right}.scene3d-footer{display:flex;align-items:center;gap:8px;padding:6px 8px;border-top:1px solid var(--surface-border, #dee2e6);min-height:36px;flex:0 0 auto;width:100%;overflow:hidden}.scene3d-footer-label{font-size:.85rem;font-weight:600;white-space:nowrap}.scene3d-footer-list{display:flex;align-items:center;gap:6px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;width:100%}.scene3d-footer-item{border:1px solid var(--surface-border, #dee2e6);background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.8rem;line-height:1.25rem;cursor:pointer;white-space:nowrap;display:inline-flex;align-items:center;gap:4px}.scene3d-footer-item.active{border-color:#3b82f6;color:#1d4ed8}.scene3d-footer-empty{font-size:.8rem;opacity:.6}.scene3d-scene-option{display:flex;align-items:center;gap:10px;min-width:0}.scene3d-scene-option-thumb{width:72px;height:44px;flex:0 0 auto;border-radius:4px;object-fit:cover;border:1px solid #cbd5e1;background:#1e1e28}.scene3d-scene-option-thumb-empty{display:inline-flex;align-items:center;justify-content:center;color:#64748b}.scene3d-scene-option-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"] }]
|
|
4798
|
+
args: [{ selector: 'wuic-scene3d-designer', standalone: true, imports: [FormsModule, ButtonModule, SelectModule, SplitButtonModule, ContextMenuModule, TranslateModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- F2/F3: viewport WebGL (canvas three.js) + palette oggetti a sinistra +\r\n property panel a destra + toolbar (save/load/new/delete). -->\r\n<div class=\"scene3d-designer-shell\">\r\n\r\n <!-- Toolbar -->\r\n <div class=\"scene3d-toolbar\">\r\n <p-button\r\n icon=\"pi pi-file\"\r\n [label]=\"t('scene3d.toolbar.new', 'Nuova')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n (onClick)=\"newScene()\">\r\n </p-button>\r\n\r\n <p-button\r\n icon=\"pi pi-save\"\r\n [label]=\"t('scene3d.toolbar.save', 'Salva')\"\r\n size=\"small\"\r\n [loading]=\"savingScene\"\r\n (onClick)=\"saveScene()\">\r\n </p-button>\r\n\r\n <p-select\r\n [options]=\"savedSceneOptions\"\r\n [(ngModel)]=\"selectedSavedSceneKey\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n [filter]=\"true\"\r\n [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.toolbar.load_placeholder', 'Carica scena...')\"\r\n appendTo=\"body\"\r\n styleClass=\"scene3d-load-select\"\r\n (onChange)=\"onSavedSceneSelectionChange()\">\r\n <!-- Anteprima scena (thumbnail dal worker server-side GPU); placeholder cubo se assente. -->\r\n <ng-template pTemplate=\"item\" let-opt>\r\n <div class=\"scene3d-scene-option\">\r\n @if (opt.thumbnail) {\r\n <img class=\"scene3d-scene-option-thumb\" [src]=\"opt.thumbnail\" [alt]=\"opt.label\" loading=\"lazy\" />\r\n } @else {\r\n <span class=\"scene3d-scene-option-thumb scene3d-scene-option-thumb-empty\">\r\n <i class=\"pi pi-box\"></i>\r\n </span>\r\n }\r\n <span class=\"scene3d-scene-option-label\">{{ opt.label }}</span>\r\n </div>\r\n </ng-template>\r\n </p-select>\r\n\r\n <p-button\r\n icon=\"pi pi-folder-open\"\r\n [label]=\"t('scene3d.toolbar.load', 'Carica')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n [loading]=\"loadingScene\"\r\n [disabled]=\"!selectedSavedSceneKey\"\r\n (onClick)=\"loadScene()\">\r\n </p-button>\r\n\r\n <p-button\r\n icon=\"pi pi-trash\"\r\n [label]=\"t('scene3d.toolbar.delete_selected', 'Elimina selezionato')\"\r\n severity=\"danger\"\r\n size=\"small\"\r\n [disabled]=\"!selectedObjectUuid\"\r\n (onClick)=\"deleteSelectedObject()\">\r\n </p-button>\r\n\r\n <!-- Toggle luci default del viewport: OFF = si vede solo l'effetto delle luci droppate -->\r\n <p-button\r\n icon=\"pi pi-sun\"\r\n [label]=\"t('scene3d.toolbar.default_lights', 'Luci default')\"\r\n [severity]=\"defaultLightsEnabled ? 'primary' : 'secondary'\"\r\n [outlined]=\"!defaultLightsEnabled\"\r\n size=\"small\"\r\n [title]=\"t('scene3d.toolbar.default_lights_hint', 'Attiva/disattiva le luci di default del viewport')\"\r\n (onClick)=\"toggleDefaultLights()\">\r\n </p-button>\r\n\r\n <!-- Toggle shadow map del renderer (default ON) -->\r\n <p-button\r\n icon=\"pi pi-moon\"\r\n [label]=\"t('scene3d.toolbar.shadows', 'Ombre')\"\r\n [severity]=\"shadowsEnabled ? 'primary' : 'secondary'\"\r\n [outlined]=\"!shadowsEnabled\"\r\n size=\"small\"\r\n [title]=\"t('scene3d.toolbar.shadows_hint', 'Attiva/disattiva le ombre proiettate')\"\r\n (onClick)=\"toggleShadows()\">\r\n </p-button>\r\n\r\n <!-- Renderer WebGPU (opt-in): fallback WebGL se non supportato; shader GLSL e PT restano WebGL-only -->\r\n <p-button\r\n icon=\"pi pi-bolt\"\r\n label=\"WebGPU\"\r\n [severity]=\"rendererMode === 'webgpu' ? 'primary' : 'secondary'\"\r\n [outlined]=\"rendererMode !== 'webgpu'\"\r\n size=\"small\"\r\n [loading]=\"rendererSwitching\"\r\n [disabled]=\"!webGpuSupported\"\r\n [title]=\"webGpuSupported\r\n ? t('scene3d.renderer.toggle_hint', 'Renderer WebGPU (node-materials). Shader GLSL custom e path tracing restano disponibili solo in WebGL')\r\n : t('scene3d.renderer.webgpu_unavailable', 'WebGPU non disponibile in questo browser: resto su WebGL')\"\r\n (onClick)=\"toggleWebGpuRenderer()\">\r\n </p-button>\r\n\r\n <!-- Toggle guide di scena: proxy luci/camera + linee helper/frustum -->\r\n <p-button\r\n icon=\"pi pi-eye\"\r\n [label]=\"t('scene3d.toolbar.helpers', 'Guide')\"\r\n [severity]=\"helpersVisible ? 'primary' : 'secondary'\"\r\n [outlined]=\"!helpersVisible\"\r\n size=\"small\"\r\n [title]=\"t('scene3d.toolbar.helpers_hint', 'Mostra/nasconde i placeholder delle luci e le linee helper/frustum')\"\r\n (onClick)=\"toggleHelpers()\">\r\n </p-button>\r\n\r\n <!-- Auto-rigenera (toggle globale): se ON, ogni cambio di proprieta' rigenera\r\n automaticamente il repeater selezionato (con debounce), senza cliccare\r\n \"Rigenera\". Per gli altri oggetti palette i cambi si applicano gia' live. -->\r\n <p-button\r\n icon=\"pi pi-sync\"\r\n [label]=\"t('scene3d.toolbar.auto_regen', 'Auto-rigenera')\"\r\n [severity]=\"autoRegen ? 'primary' : 'secondary'\"\r\n [outlined]=\"!autoRegen\"\r\n size=\"small\"\r\n [title]=\"t('scene3d.toolbar.auto_regen_hint', 'Se attivo, ogni modifica di proprieta rigenera automaticamente (repeater), senza premere Rigenera')\"\r\n (onClick)=\"autoRegen = !autoRegen\">\r\n </p-button>\r\n\r\n <!-- Render qualita' (path tracing progressivo, lazy): GI + ombre colorate dai vetri -->\r\n <p-button\r\n icon=\"pi pi-sparkles\"\r\n [label]=\"pathTracingEnabled ? t('scene3d.toolbar.pt_exit', 'Realtime') : t('scene3d.toolbar.pt', 'Render PT')\"\r\n [severity]=\"pathTracingEnabled ? 'primary' : 'secondary'\"\r\n [outlined]=\"!pathTracingEnabled\"\r\n size=\"small\"\r\n [loading]=\"ptBuilding\"\r\n [title]=\"t('scene3d.toolbar.pt_hint', 'Render path-traced progressivo (GI, ombre colorate dai vetri). Modalita\\' preview: editing sospeso')\"\r\n (onClick)=\"togglePathTracing()\">\r\n </p-button>\r\n\r\n <!-- Fisica: Play/Stop simulazione + menu (Applica pose / Impostazioni scena) -->\r\n <p-splitButton\r\n [icon]=\"physicsSimulating ? 'pi pi-stop' : 'pi pi-play'\"\r\n [label]=\"physicsSimulating ? t('scene3d.toolbar.physics_stop', 'Stop fisica') : t('scene3d.toolbar.physics_play', 'Fisica')\"\r\n [severity]=\"physicsSimulating ? 'primary' : 'secondary'\"\r\n [outlined]=\"!physicsSimulating\"\r\n size=\"small\"\r\n [model]=\"physicsMenuItems\"\r\n appendTo=\"body\"\r\n [attr.title]=\"t('scene3d.toolbar.physics_hint', 'Simula i corpi rigidi configurati (Stop = ripristina le pose)')\"\r\n (onClick)=\"togglePhysicsSimulation()\">\r\n </p-splitButton>\r\n\r\n <!-- Bake luci per-vertice: cuoce l'illuminazione nei vertex colors (v1 lightmap) -->\r\n <p-button\r\n icon=\"pi pi-bolt\"\r\n [label]=\"t('scene3d.toolbar.bake', 'Bake luci')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n [title]=\"t('scene3d.toolbar.bake_hint', 'Cuoce l\\'illuminazione corrente nei vertex colors (materiale statico unlit)')\"\r\n (onClick)=\"bakeLightsAction()\">\r\n </p-button>\r\n\r\n <!-- Crea una camera di scena che replica il punto di vista attuale del designer -->\r\n <p-button\r\n icon=\"pi pi-camera\"\r\n [label]=\"t('scene3d.toolbar.camera_from_view', 'Camera da vista attuale')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n [title]=\"t('scene3d.toolbar.camera_from_view_hint', 'Crea una camera nella scena con posizione e inquadratura identiche alla vista corrente')\"\r\n (onClick)=\"createCameraFromView()\">\r\n </p-button>\r\n\r\n <!-- Vista camera: render dalla camera utente (selezionata o prima in scena).\r\n Con piu' camere in scena diventa uno split-button col picker. -->\r\n @if (sceneCameraCount > 1) {\r\n <p-splitButton\r\n icon=\"pi pi-video\"\r\n [label]=\"viewingThroughCamera ? t('scene3d.toolbar.designer_view', 'Vista designer') : t('scene3d.toolbar.camera_view', 'Vista camera')\"\r\n [severity]=\"viewingThroughCamera ? 'primary' : 'secondary'\"\r\n [outlined]=\"!viewingThroughCamera\"\r\n size=\"small\"\r\n [model]=\"cameraViewMenuItems\"\r\n appendTo=\"body\"\r\n [attr.title]=\"t('scene3d.toolbar.camera_view_hint', 'Alterna tra la vista di editing e la vista dalla camera della scena')\"\r\n (onClick)=\"toggleCameraView()\">\r\n </p-splitButton>\r\n } @else {\r\n <p-button\r\n icon=\"pi pi-video\"\r\n [label]=\"viewingThroughCamera ? t('scene3d.toolbar.designer_view', 'Vista designer') : t('scene3d.toolbar.camera_view', 'Vista camera')\"\r\n [severity]=\"viewingThroughCamera ? 'primary' : 'secondary'\"\r\n [outlined]=\"!viewingThroughCamera\"\r\n size=\"small\"\r\n [title]=\"t('scene3d.toolbar.camera_view_hint', 'Alterna tra la vista di editing e la vista dalla camera della scena')\"\r\n (onClick)=\"toggleCameraView()\">\r\n </p-button>\r\n }\r\n </div>\r\n\r\n <div class=\"scene3d-body\">\r\n\r\n <!-- Palette (sinistra) -->\r\n <div class=\"scene3d-palette\">\r\n <div class=\"scene3d-panel-title\">{{ t('scene3d.palette.title', 'Oggetti') }}</div>\r\n @for (item of paletteItems; track item.type) {\r\n <button\r\n type=\"button\"\r\n class=\"scene3d-palette-item\"\r\n [disabled]=\"!item.fromPalette\"\r\n [title]=\"item.label\"\r\n (click)=\"addFromPalette(item.type)\">\r\n <i [class]=\"item.icon\"></i>\r\n <span class=\"scene3d-palette-label\">{{ item.label }}</span>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Viewport (centro) -->\r\n <div #viewportContainer class=\"scene3d-viewport\">\r\n <!-- HUD path tracing: build, sample progressivi, scala risoluzione -->\r\n @if (ptBuilding || pathTracingEnabled) {\r\n <div class=\"scene3d-pt-hud\">\r\n @if (ptBuilding) {\r\n <span>{{ t('scene3d.pt.building', 'Costruzione BVH...') }}</span>\r\n } @else {\r\n <span>{{ ptSamples }} {{ t('scene3d.pt.samples_suffix', 'sample') }}</span>\r\n <label class=\"scene3d-pt-scale\">\r\n {{ t('scene3d.pt.scale', 'Risoluzione') }}\r\n <input type=\"range\" min=\"0.25\" max=\"1\" step=\"0.25\"\r\n [(ngModel)]=\"ptResolutionScale\" (ngModelChange)=\"onPtResolutionScaleChange()\" />\r\n </label>\r\n <button type=\"button\" class=\"scene3d-pt-exit\" (click)=\"togglePathTracing()\">\r\n {{ t('scene3d.pt.exit', 'Torna al realtime') }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Popup anteprima camera: real-time, trascinabile dall'header -->\r\n @if (cameraPreviewEnabled) {\r\n <div class=\"scene3d-cam-preview\" [style.left.px]=\"previewPos.x\" [style.top.px]=\"previewPos.y\">\r\n <div class=\"scene3d-cam-preview-header\" (mousedown)=\"startPreviewDrag($event)\">\r\n <span class=\"scene3d-cam-preview-title\">{{ t('scene3d.preview.title', 'Anteprima camera') }}</span>\r\n <button type=\"button\" class=\"scene3d-cam-preview-close\" (click)=\"toggleCameraPreview()\" (mousedown)=\"$event.stopPropagation()\">\u00D7</button>\r\n </div>\r\n <div #previewCanvasHost class=\"scene3d-cam-preview-body\"></div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Property panel (destra) -->\r\n <div class=\"scene3d-properties\">\r\n <div class=\"scene3d-panel-title\">{{ t('scene3d.props.title', 'Proprieta') }}</div>\r\n\r\n @if (selectedObjectUuid) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-prop-name\">{{ t('scene3d.props.name', 'Nome') }}</label>\r\n <input id=\"scene3d-prop-name\" type=\"text\" [(ngModel)]=\"selectedObjectName\"\r\n (ngModelChange)=\"renameSelectedObject($event)\"\r\n [placeholder]=\"t('scene3d.props.name_placeholder', 'Nome oggetto')\" />\r\n </div>\r\n\r\n <!-- ID della camera: e' il camera_id per l'URL del viewer\r\n (#/scene3d_viewer/<scene_key>/<camera_id>). Persiste nel salvataggio. -->\r\n @if (selectedIsCamera) {\r\n <div class=\"scene3d-prop-group\">\r\n <span class=\"scene3d-prop-label\">{{ t('scene3d.props.camera_id', 'ID camera (viewer)') }}</span>\r\n <div class=\"scene3d-prop-readonly\">{{ selectedObjectUuid }}</div>\r\n </div>\r\n\r\n <!-- Anteprima real-time in popup trascinabile dal punto di vista della camera -->\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input\r\n type=\"checkbox\"\r\n [ngModel]=\"cameraPreviewEnabled && previewCameraUuid === selectedObjectUuid\"\r\n (ngModelChange)=\"toggleCameraPreview()\" />\r\n {{ t('scene3d.props.camera_preview', 'Anteprima camera') }}\r\n </label>\r\n </div>\r\n }\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-pos-x\">{{ t('scene3d.props.position', 'Posizione') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-pos-x\" type=\"number\" step=\"0.1\" [(ngModel)]=\"propPosition.x\" (ngModelChange)=\"onTransformInputChange()\" />\r\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propPosition.y\" (ngModelChange)=\"onTransformInputChange()\" />\r\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propPosition.z\" (ngModelChange)=\"onTransformInputChange()\" />\r\n </div>\r\n </div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-rot-x\">{{ t('scene3d.props.rotation', 'Rotazione (deg)') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-rot-x\" type=\"number\" step=\"1\" [(ngModel)]=\"propRotation.x\" (ngModelChange)=\"onTransformInputChange()\" />\r\n <input type=\"number\" step=\"1\" [(ngModel)]=\"propRotation.y\" (ngModelChange)=\"onTransformInputChange()\" />\r\n <input type=\"number\" step=\"1\" [(ngModel)]=\"propRotation.z\" (ngModelChange)=\"onTransformInputChange()\" />\r\n </div>\r\n </div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-scale-x\">{{ t('scene3d.props.scale', 'Scala') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-scale-x\" type=\"number\" step=\"0.1\" [(ngModel)]=\"propScale.x\" (ngModelChange)=\"onTransformInputChange()\" />\r\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propScale.y\" (ngModelChange)=\"onTransformInputChange()\" />\r\n <input type=\"number\" step=\"0.1\" [(ngModel)]=\"propScale.z\" (ngModelChange)=\"onTransformInputChange()\" />\r\n </div>\r\n </div>\r\n\r\n @if (selectedHasMaterialColor) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-prop-color\">{{ t('scene3d.props.color', 'Colore') }}</label>\r\n <input id=\"scene3d-prop-color\" type=\"color\" [(ngModel)]=\"propColor\" (ngModelChange)=\"onColorInputChange()\" />\r\n </div>\r\n }\r\n\r\n <!-- castShadow per-oggetto (default ON alla creazione, disattivabile) -->\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"propCastShadow\" (ngModelChange)=\"onCastShadowInputChange()\" />\r\n {{ t('scene3d.props.cast_shadow', 'Proietta ombra') }}\r\n </label>\r\n </div>\r\n\r\n <!-- Editor materiale PBR (MeshStandardMaterial) -->\r\n @if (selectedHasStandardMaterial) {\r\n <div class=\"scene3d-material-section\">\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.material.title', 'Materiale') }}</div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-metalness\">{{ t('scene3d.material.metalness', 'Metalness') }} ({{ propMaterial.metalness }})</label>\r\n <input id=\"scene3d-mat-metalness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.metalness\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-roughness\">{{ t('scene3d.material.roughness', 'Roughness') }} ({{ propMaterial.roughness }})</label>\r\n <input id=\"scene3d-mat-roughness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.roughness\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-opacity\">{{ t('scene3d.material.opacity', 'Opacita\\'') }} ({{ propMaterial.opacity }})</label>\r\n <input id=\"scene3d-mat-opacity\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.opacity\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-emissive\">{{ t('scene3d.material.emissive', 'Emissivo') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-mat-emissive\" type=\"color\" [(ngModel)]=\"propMaterial.emissive\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n <input type=\"number\" step=\"0.1\" min=\"0\" [(ngModel)]=\"propMaterial.emissiveIntensity\" (ngModelChange)=\"onMaterialInputChange()\"\r\n [title]=\"t('scene3d.material.emissive_intensity', 'Intensita\\' emissiva')\" />\r\n </div>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-side\">{{ t('scene3d.material.side', 'Facce') }}</label>\r\n <select id=\"scene3d-mat-side\" class=\"scene3d-material-select\" [(ngModel)]=\"propMaterial.side\" (ngModelChange)=\"onMaterialInputChange()\">\r\n <option value=\"front\">{{ t('scene3d.material.side_front', 'Fronte') }}</option>\r\n <option value=\"back\">{{ t('scene3d.material.side_back', 'Retro') }}</option>\r\n <option value=\"double\">{{ t('scene3d.material.side_double', 'Doppia') }}</option>\r\n </select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"propMaterial.wireframe\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n {{ t('scene3d.material.wireframe', 'Wireframe') }}\r\n </label>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"propMaterial.flatShading\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n {{ t('scene3d.material.flat', 'Flat shading') }}\r\n </label>\r\n </div>\r\n\r\n <!-- Vetro fisico (transmission): rifrazione reale di cio' che sta dietro.\r\n In Render PT l'assorbimento (colore+distanza) tinge anche la LUCE\r\n che attraversa il vetro sugli altri oggetti (Beer-Lambert). -->\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.material.glass', 'Vetro (transmission)') }}</div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-transmission\">{{ t('scene3d.material.transmission', 'Trasmissione') }} ({{ propMaterial.transmission }})</label>\r\n <input id=\"scene3d-mat-transmission\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"propMaterial.transmission\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n @if (propMaterial.transmission > 0) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-ior\">{{ t('scene3d.material.ior', 'IOR') }} ({{ propMaterial.ior }})</label>\r\n <input id=\"scene3d-mat-ior\" type=\"range\" min=\"1\" max=\"2.33\" step=\"0.01\" [(ngModel)]=\"propMaterial.ior\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-thickness\">{{ t('scene3d.material.thickness', 'Spessore') }} ({{ propMaterial.thickness }})</label>\r\n <input id=\"scene3d-mat-thickness\" type=\"range\" min=\"0\" max=\"5\" step=\"0.1\" [(ngModel)]=\"propMaterial.thickness\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n <!-- Assorbimento volumetrico: colora la luce trasmessa (visibile in PT). -->\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-attenuation-color\">{{ t('scene3d.material.attenuation_color', 'Colore assorbimento') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-mat-attenuation-color\" type=\"color\" [(ngModel)]=\"propMaterial.attenuationColor\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-mat-attenuation-distance\">{{ t('scene3d.material.attenuation_distance', 'Distanza assorbimento') }} ({{ propMaterial.attenuationDistance }})</label>\r\n <input id=\"scene3d-mat-attenuation-distance\" type=\"range\" min=\"0.05\" max=\"10\" step=\"0.05\" [(ngModel)]=\"propMaterial.attenuationDistance\" (ngModelChange)=\"onMaterialInputChange()\" />\r\n </div>\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.material.glass_hint', 'Con transmission tieni Opacita\\' = 1 (la trasparenza la gestisce il vetro). Il colore assorbimento tinge la luce filtrata: visibile in Render PT.') }}\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Shader GLSL custom -->\r\n @if (selectedIsMesh) {\r\n <div class=\"scene3d-material-section\">\r\n <div class=\"scene3d-panel-subtitle scene3d-shader-toggle\" (click)=\"shaderSectionOpen = !shaderSectionOpen\">\r\n {{ t('scene3d.shader.title', 'Shader custom (GLSL)') }} {{ shaderSectionOpen ? '\u25BE' : '\u25B8' }}\r\n </div>\r\n @if (shaderSectionOpen) {\r\n <!-- Effetto JSON (dual-backend): editor Monaco con schema, compilato\r\n in GLSL su WebGL e in node-material su WebGPU -->\r\n <div class=\"scene3d-anim-transport\">\r\n <p-button\r\n icon=\"pi pi-sliders-h\"\r\n [label]=\"t('scene3d.shader.effect_edit', 'Effetto JSON')\"\r\n size=\"small\"\r\n [severity]=\"selectedHasShaderEffect ? 'primary' : 'secondary'\"\r\n [title]=\"t('scene3d.shader.effect_tooltip', 'JSON con schema (completion/validazione in editor). Compilato in GLSL su WebGL e in node-material su WebGPU.')\"\r\n (onClick)=\"editShaderEffect()\">\r\n </p-button>\r\n @if (selectedHasShaderEffect) {\r\n <p-button\r\n icon=\"pi pi-undo\"\r\n [label]=\"t('scene3d.shader.effect_remove', 'Rimuovi effetto')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n (onClick)=\"removeShaderEffect()\">\r\n </p-button>\r\n }\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-shader-vertex\">{{ t('scene3d.shader.vertex', 'Vertex shader') }}</label>\r\n <textarea id=\"scene3d-shader-vertex\" class=\"scene3d-shader-src\" rows=\"6\" spellcheck=\"false\" [(ngModel)]=\"shaderVertexSrc\"></textarea>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-shader-fragment\">{{ t('scene3d.shader.fragment', 'Fragment shader') }}</label>\r\n <textarea id=\"scene3d-shader-fragment\" class=\"scene3d-shader-src\" rows=\"8\" spellcheck=\"false\" [(ngModel)]=\"shaderFragmentSrc\"></textarea>\r\n </div>\r\n <div class=\"scene3d-anim-transport\">\r\n <p-button\r\n icon=\"pi pi-check\"\r\n [label]=\"t('scene3d.shader.apply', 'Applica shader')\"\r\n size=\"small\"\r\n (onClick)=\"applyCustomShader()\">\r\n </p-button>\r\n @if (selectedHasCustomShader) {\r\n <p-button\r\n icon=\"pi pi-undo\"\r\n [label]=\"t('scene3d.shader.restore', 'Ripristina')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n (onClick)=\"restoreCustomShader()\">\r\n </p-button>\r\n }\r\n </div>\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.shader.hint', 'Uniform disponibile: uTime (secondi, animata). Errori GLSL in console.') }}\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- F5: Binding rotta scaffoldata + field-map + open-record.\r\n Nascosto per il Mesh Repeater: il repeater binda per-record (config sua). -->\r\n @if (!selectedIsRepeater) {\r\n <div class=\"scene3d-binding-section\">\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.binding.title', 'Binding rotta') }}</div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-route\">{{ t('scene3d.binding.route', 'Rotta') }}</label>\r\n <p-select\r\n inputId=\"scene3d-binding-route\"\r\n [options]=\"routeOptions\"\r\n [(ngModel)]=\"bindingRoute\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n [filter]=\"true\"\r\n [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.binding.route_placeholder', 'Seleziona rotta...')\"\r\n appendTo=\"body\"\r\n styleClass=\"scene3d-binding-select\"\r\n (onChange)=\"onBindingRouteChange()\">\r\n </p-select>\r\n </div>\r\n\r\n @if (selectedHasBinding) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-record-id\">{{ t('scene3d.binding.record_id', 'Record ID (opzionale)') }}</label>\r\n <input\r\n id=\"scene3d-binding-record-id\"\r\n type=\"text\"\r\n [(ngModel)]=\"bindingRecordId\"\r\n (ngModelChange)=\"onBindingDetailChange()\"\r\n [placeholder]=\"t('scene3d.binding.record_id_placeholder', 'Vuoto = binding di rotta')\" />\r\n </div>\r\n\r\n <div class=\"scene3d-field-map\">\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.binding.field_map', 'Mappa campi') }}</div>\r\n\r\n @if (bindingColumnsLoading) {\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.binding.columns_loading', 'Caricamento colonne...') }}</div>\r\n }\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-map-label\">{{ t('scene3d.binding.map_label', 'Etichetta') }}</label>\r\n <p-select\r\n inputId=\"scene3d-binding-map-label\"\r\n [options]=\"bindingColumnOptions\"\r\n [(ngModel)]=\"bindingFieldMap.label\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n [filter]=\"true\"\r\n [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.binding.column_placeholder', 'Colonna...')\"\r\n appendTo=\"body\"\r\n styleClass=\"scene3d-binding-select\"\r\n (onChange)=\"onBindingDetailChange()\">\r\n </p-select>\r\n </div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-map-color\">{{ t('scene3d.binding.map_color', 'Colore') }}</label>\r\n <p-select\r\n inputId=\"scene3d-binding-map-color\"\r\n [options]=\"bindingColumnOptions\"\r\n [(ngModel)]=\"bindingFieldMap.color\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n [filter]=\"true\"\r\n [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.binding.column_placeholder', 'Colonna...')\"\r\n appendTo=\"body\"\r\n styleClass=\"scene3d-binding-select\"\r\n (onChange)=\"onBindingDetailChange()\">\r\n </p-select>\r\n </div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-binding-map-visible\">{{ t('scene3d.binding.map_visible', 'Visibile') }}</label>\r\n <p-select\r\n inputId=\"scene3d-binding-map-visible\"\r\n [options]=\"bindingColumnOptions\"\r\n [(ngModel)]=\"bindingFieldMap.visible\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n [filter]=\"true\"\r\n [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.binding.column_placeholder', 'Colonna...')\"\r\n appendTo=\"body\"\r\n styleClass=\"scene3d-binding-select\"\r\n (onChange)=\"onBindingDetailChange()\">\r\n </p-select>\r\n </div>\r\n </div>\r\n\r\n <p-button\r\n icon=\"pi pi-external-link\"\r\n [label]=\"t('scene3d.binding.open_record', 'Apri record')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n styleClass=\"scene3d-open-record-btn\"\r\n (onClick)=\"openBoundRecord()\">\r\n </p-button>\r\n\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.binding.open_hint', 'Doppio click su un oggetto bindato per aprire la CRUD della rotta') }}\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Iter2 F2: animazioni / timeline (solo se il selezionato ha clip) -->\r\n @if (selectedHasAnimations) {\r\n <div class=\"scene3d-anim-section\">\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.anim.title', 'Animazioni') }}</div>\r\n\r\n @if (animClipOptions.length > 1) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-anim-clip\">{{ t('scene3d.anim.clip', 'Clip') }}</label>\r\n <p-select\r\n inputId=\"scene3d-anim-clip\"\r\n [options]=\"animClipOptions\"\r\n [(ngModel)]=\"selectedAnimClipIndex\"\r\n optionLabel=\"label\"\r\n optionValue=\"value\"\r\n appendTo=\"body\"\r\n styleClass=\"scene3d-binding-select\"\r\n (onChange)=\"onAnimClipChange()\">\r\n </p-select>\r\n </div>\r\n }\r\n\r\n <div class=\"scene3d-anim-transport\">\r\n <p-button\r\n icon=\"pi pi-play\"\r\n [title]=\"t('scene3d.anim.play', 'Play')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n [disabled]=\"animPlaying\"\r\n (onClick)=\"animPlay()\">\r\n </p-button>\r\n <p-button\r\n icon=\"pi pi-pause\"\r\n [title]=\"t('scene3d.anim.pause', 'Pausa')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n [disabled]=\"!animPlaying\"\r\n (onClick)=\"animPause()\">\r\n </p-button>\r\n <p-button\r\n icon=\"pi pi-stop\"\r\n [title]=\"t('scene3d.anim.stop', 'Stop')\"\r\n severity=\"secondary\"\r\n size=\"small\"\r\n (onClick)=\"animStop()\">\r\n </p-button>\r\n <p-button\r\n icon=\"pi pi-trash\"\r\n [title]=\"t('scene3d.anim.delete_clip', 'Elimina clip')\"\r\n severity=\"danger\"\r\n size=\"small\"\r\n (onClick)=\"deleteSelectedAnimClip()\">\r\n </p-button>\r\n </div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-anim-time\">{{ t('scene3d.anim.time', 'Tempo (s)') }}</label>\r\n <input\r\n id=\"scene3d-anim-time\"\r\n type=\"range\"\r\n class=\"scene3d-anim-scrubber\"\r\n min=\"0\"\r\n [max]=\"animDuration\"\r\n step=\"0.01\"\r\n [(ngModel)]=\"animScrubTime\"\r\n (ngModelChange)=\"onAnimScrub()\" />\r\n <div class=\"scene3d-anim-time\">{{ animScrubTime }} / {{ animDuration }}</div>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Mesh Repeater (DATAREPEATER 3D): una mesh per record, propriet\u00E0 bindate -->\r\n @if (selectedIsRepeater) {\r\n <div class=\"scene3d-material-section\">\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.repeater.title', 'Mesh Repeater') }}</div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-route\">{{ t('scene3d.repeater.route', 'Rotta') }}</label>\r\n <p-select\r\n inputId=\"scene3d-repeater-route\"\r\n [options]=\"routeOptions\" [(ngModel)]=\"repeaterCfg.route\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.route_ph', 'Seleziona rotta...')\"\r\n appendTo=\"body\" styleClass=\"scene3d-binding-select\"\r\n (onChange)=\"onRepeaterRouteChange()\">\r\n </p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-max\">{{ t('scene3d.repeater.max', 'Max record') }}</label>\r\n <input id=\"scene3d-repeater-max\" type=\"number\" min=\"1\" step=\"10\" [(ngModel)]=\"repeaterCfg.maxRecords\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-mesh-source\">{{ t('scene3d.repeater.mesh_source', 'Sorgente mesh') }}</label>\r\n <select id=\"scene3d-repeater-mesh-source\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.meshSource.mode\" (ngModelChange)=\"onRepeaterConfigChange()\">\r\n <option value=\"fixed\">{{ t('scene3d.repeater.fixed', 'Fissa') }}</option>\r\n <option value=\"column\">{{ t('scene3d.repeater.from_column', 'Da colonna') }}</option>\r\n <option value=\"asset\">{{ t('scene3d.repeater.from_asset', 'Asset (colonna)') }}</option>\r\n </select>\r\n </div>\r\n @if (repeaterCfg.meshSource.mode === 'fixed') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-mesh-type\">{{ t('scene3d.repeater.mesh_type', 'Tipo mesh') }}</label>\r\n <select id=\"scene3d-repeater-mesh-type\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.meshSource.type\" (ngModelChange)=\"onRepeaterConfigChange()\">\r\n @for (o of repeaterMeshTypeOptions; track o.value) { <option [value]=\"o.value\">{{ o.label }}</option> }\r\n </select>\r\n </div>\r\n } @else {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-mesh-column\">{{ repeaterCfg.meshSource.mode === 'asset'\r\n ? t('scene3d.repeater.asset_column', 'Colonna asset (upload GLB/OBJ/STL)')\r\n : t('scene3d.repeater.mesh_column', 'Colonna tipo') }}</label>\r\n <p-select inputId=\"scene3d-repeater-mesh-column\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"repeaterCfg.meshSource.column\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n appendTo=\"body\" styleClass=\"scene3d-binding-select\" (onChange)=\"onRepeaterConfigChange()\"></p-select>\r\n </div>\r\n }\r\n\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.repeater.field_map', 'Mappa campi \u2192 colonne') }}</div>\r\n @for (f of repeaterFields; track f.key) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" [attr.for]=\"'scene3d-rep-field-' + f.key\">{{ f.label }}</label>\r\n <p-select [inputId]=\"'scene3d-rep-field-' + f.key\" [options]=\"bindingColumnOptions\"\r\n [ngModel]=\"getFieldMapCol(f.key)\" (ngModelChange)=\"setFieldMapCol(f.key, $event)\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\"\r\n appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n }\r\n\r\n <!-- Opzioni texture (visibili solo con la colonna `texture` mappata):\r\n testo\u2192texture testuale, upload/img\u2192immagine, + modo di applicazione. -->\r\n @if (getFieldMapCol('texture')) {\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.repeater.texture_opts', 'Texture (colonna)') }}</div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-source\">{{ t('scene3d.repeater.texture_source', 'Sorgente') }}</label>\r\n <select id=\"scene3d-texture-source\" class=\"scene3d-material-select\" [(ngModel)]=\"textureApply.sourceKind\" (ngModelChange)=\"onTextureApplyChange()\">\r\n <option value=\"auto\">{{ t('scene3d.repeater.texture_auto', 'Auto (dal tipo colonna)') }}</option>\r\n <option value=\"text\">{{ t('scene3d.repeater.texture_text', 'Testo \u2192 texture') }}</option>\r\n <option value=\"image\">{{ t('scene3d.repeater.texture_image', 'Immagine / upload') }}</option>\r\n </select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-apply\">{{ t('scene3d.repeater.texture_apply', 'Applicazione') }}</label>\r\n <select id=\"scene3d-texture-apply\" class=\"scene3d-material-select\" [(ngModel)]=\"textureApply.wrap\" (ngModelChange)=\"onTextureApplyChange()\">\r\n <option value=\"stretch\">{{ t('scene3d.repeater.texture_stretch', 'Stretch (adatta)') }}</option>\r\n <option value=\"repeat\">{{ t('scene3d.repeater.texture_repeat', 'Ripeti') }}</option>\r\n <option value=\"mirror\">{{ t('scene3d.repeater.texture_mirror', 'Ripeti a specchio') }}</option>\r\n </select>\r\n </div>\r\n @if (textureApply.wrap !== 'stretch') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-repeat-x\">{{ t('scene3d.repeater.texture_repeat_xy', 'Ripetizioni X / Y') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-texture-repeat-x\" type=\"number\" step=\"0.5\" min=\"0.1\" [(ngModel)]=\"textureApply.repeatX\" (ngModelChange)=\"onTextureApplyChange()\" />\r\n <input type=\"number\" step=\"0.5\" min=\"0.1\" [(ngModel)]=\"textureApply.repeatY\" (ngModelChange)=\"onTextureApplyChange()\" />\r\n </div>\r\n </div>\r\n }\r\n @if (textureApply.sourceKind !== 'image') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-texture-text-bg\">{{ t('scene3d.repeater.texture_text_colors', 'Colori testo (sfondo / testo)') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-texture-text-bg\" type=\"color\" [(ngModel)]=\"textureApply.textBg\" (ngModelChange)=\"onTextureApplyChange()\" />\r\n <input type=\"color\" [(ngModel)]=\"textureApply.textFg\" (ngModelChange)=\"onTextureApplyChange()\" />\r\n </div>\r\n </div>\r\n }\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.repeater.texture_hint', 'Dopo aver cambiato le opzioni premi Rigenera per riapplicare le texture.') }}\r\n </div>\r\n }\r\n\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-layout\">{{ t('scene3d.repeater.layout', 'Layout (senza colonne posizione)') }}</label>\r\n <select id=\"scene3d-repeater-layout\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.layout.mode\" (ngModelChange)=\"onRepeaterLayoutModeChange()\">\r\n <optgroup [label]=\"t('scene3d.repeater.layout_simple', 'Semplici')\">\r\n <option value=\"grid\">Grid</option>\r\n <option value=\"cube\">{{ t('scene3d.repeater.cube', 'Cubo') }}</option>\r\n <option value=\"line\">{{ t('scene3d.repeater.line', 'Linea') }}</option>\r\n <option value=\"circle\">{{ t('scene3d.repeater.circle', 'Cerchio') }}</option>\r\n </optgroup>\r\n <optgroup [label]=\"t('scene3d.repeater.layout_archetypes', 'Archetipi')\">\r\n <option value=\"carousel\">{{ t('scene3d.repeater.carousel', 'Carousel 3D') }}</option>\r\n <option value=\"chart\">{{ t('scene3d.repeater.chart', 'Chart 3D') }}</option>\r\n <option value=\"surface\">{{ t('scene3d.repeater.surface', 'Surface / Heatmap') }}</option>\r\n <option value=\"gauge\">{{ t('scene3d.repeater.gauge', 'Gauge / KPI') }}</option>\r\n <option value=\"nodegraph\">{{ t('scene3d.repeater.nodegraph', 'Node graph') }}</option>\r\n <option value=\"tree\">{{ t('scene3d.repeater.tree', 'Albero (tree)') }}</option>\r\n <option value=\"map\">{{ t('scene3d.repeater.map', 'Mappa (pin)') }}</option>\r\n <option value=\"warehouse\">{{ t('scene3d.repeater.warehouse', 'Magazzino (scaffali)') }}</option>\r\n <option value=\"kanban\">{{ t('scene3d.repeater.kanban', 'Kanban 3D') }}</option>\r\n <option value=\"calendar\">{{ t('scene3d.repeater.calendar', 'Calendario') }}</option>\r\n <option value=\"stacked\">{{ t('scene3d.repeater.stacked', 'Barre impilate') }}</option>\r\n </optgroup>\r\n </select>\r\n </div>\r\n\r\n <!-- Archetipo Carousel 3D: sezioni collassabili Carousel / Disco / Frecce. -->\r\n @if (repeaterCfg.layout.mode === 'carousel') {\r\n <!-- Sezione Carousel (generale) -->\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('carousel')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('carousel')\" [class.pi-chevron-down]=\"!isSectionCollapsed('carousel')\"></i>\r\n {{ t('scene3d.repeater.carousel', 'Carousel 3D') }}\r\n </div>\r\n @if (!isSectionCollapsed('carousel')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-visible\">{{ t('scene3d.repeater.carousel_visible', 'Item visibili') }} ({{ carouselCfg.visible }})</label>\r\n <input id=\"scene3d-carousel-visible\" type=\"range\" min=\"1\" max=\"9\" step=\"1\" [(ngModel)]=\"carouselCfg.visible\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-radius\">{{ t('scene3d.repeater.carousel_radius', 'Curvatura (raggio)') }} ({{ carouselCfg.radius }})</label>\r\n <input id=\"scene3d-carousel-radius\" type=\"range\" min=\"2\" max=\"30\" step=\"0.5\" [(ngModel)]=\"carouselCfg.radius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-slot-angle\">{{ t('scene3d.repeater.carousel_slot_angle', 'Angolo tra slot') }} ({{ carouselCfg.slotAngleDeg }}\u00B0)</label>\r\n <input id=\"scene3d-carousel-slot-angle\" type=\"range\" min=\"8\" max=\"60\" step=\"1\" [(ngModel)]=\"carouselCfg.slotAngleDeg\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-focus-scale\">{{ t('scene3d.repeater.carousel_scales', 'Scala centro / lati') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-carousel-focus-scale\" type=\"number\" step=\"0.05\" min=\"0.1\" [(ngModel)]=\"carouselCfg.focusScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n <input type=\"number\" step=\"0.05\" min=\"0.1\" [(ngModel)]=\"carouselCfg.sideScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-mesh-y-offset\">{{ t('scene3d.repeater.carousel_mesh_y_offset', 'Offset verticale mesh') }} ({{ carouselCfg.meshYOffset ?? 0 }})</label>\r\n <input id=\"scene3d-carousel-mesh-y-offset\" type=\"range\" min=\"-5\" max=\"5\" step=\"0.1\" [(ngModel)]=\"carouselCfg.meshYOffset\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-mesh-rot-x\">{{ t('scene3d.repeater.carousel_mesh_rot', 'Rotazione mesh X / Y / Z (\u00B0)') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-carousel-mesh-rot-x\" type=\"number\" step=\"15\" [(ngModel)]=\"carouselCfg.meshRotX\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n <input type=\"number\" step=\"15\" [(ngModel)]=\"carouselCfg.meshRotY\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n <input type=\"number\" step=\"15\" [(ngModel)]=\"carouselCfg.meshRotZ\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"carouselCfg.showBase\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.carousel_base', 'Base') }}\r\n </label>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"carouselCfg.showArrows\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.carousel_arrows', 'Frecce nav') }}\r\n </label>\r\n </div>\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.repeater.carousel_hint', 'Nel viewer le frecce 3D scorrono i record (item centrale a fuoco). Premi Rigenera dopo le modifiche.') }}\r\n </div>\r\n }\r\n\r\n <!-- Sezione Disco -->\r\n @if (carouselCfg.showBase) {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('carousel_disc')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('carousel_disc')\" [class.pi-chevron-down]=\"!isSectionCollapsed('carousel_disc')\"></i>\r\n {{ t('scene3d.repeater.carousel_disc', 'Disco') }}\r\n </div>\r\n @if (!isSectionCollapsed('carousel_disc')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-disc-size\">{{ t('scene3d.repeater.carousel_disc_size', 'Dimensione disco') }} ({{ carouselCfg.baseSize }})</label>\r\n <input id=\"scene3d-carousel-disc-size\" type=\"range\" min=\"0.2\" max=\"1.5\" step=\"0.02\" [(ngModel)]=\"carouselCfg.baseSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-color\">{{ t('scene3d.repeater.carousel_base_color', 'Colore base') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-carousel-base-color\" type=\"color\" [(ngModel)]=\"carouselCfg.baseColor\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-metalness\">{{ t('scene3d.repeater.carousel_metalness', 'Metalness') }} ({{ carouselCfg.baseMetalness }})</label>\r\n <input id=\"scene3d-carousel-base-metalness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.baseMetalness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-roughness\">{{ t('scene3d.repeater.carousel_roughness', 'Roughness') }} ({{ carouselCfg.baseRoughness }})</label>\r\n <input id=\"scene3d-carousel-base-roughness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.baseRoughness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-base-clearcoat\">{{ t('scene3d.repeater.carousel_clearcoat', 'Clearcoat') }} ({{ carouselCfg.baseClearcoat }})</label>\r\n <input id=\"scene3d-carousel-base-clearcoat\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.baseClearcoat\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n }\r\n }\r\n\r\n <!-- Sezione Frecce -->\r\n @if (carouselCfg.showArrows) {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('carousel_arrows')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('carousel_arrows')\" [class.pi-chevron-down]=\"!isSectionCollapsed('carousel_arrows')\"></i>\r\n {{ t('scene3d.repeater.carousel_arrows', 'Frecce nav') }}\r\n </div>\r\n @if (!isSectionCollapsed('carousel_arrows')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-size\">{{ t('scene3d.repeater.carousel_arrow_size', 'Dimensione frecce') }} ({{ carouselCfg.arrowSize }})</label>\r\n <input id=\"scene3d-carousel-arrow-size\" type=\"range\" min=\"0.3\" max=\"2.5\" step=\"0.05\" [(ngModel)]=\"carouselCfg.arrowSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-color\">{{ t('scene3d.repeater.carousel_arrow_color', 'Colore frecce') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-carousel-arrow-color\" type=\"color\" [(ngModel)]=\"carouselCfg.arrowColor\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-emissive\">{{ t('scene3d.repeater.carousel_arrow_glow', 'Bagliore (emissive)') }} ({{ carouselCfg.arrowEmissive }})</label>\r\n <input id=\"scene3d-carousel-arrow-emissive\" type=\"range\" min=\"0\" max=\"3\" step=\"0.05\" [(ngModel)]=\"carouselCfg.arrowEmissive\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-metalness\">{{ t('scene3d.repeater.carousel_metalness', 'Metalness') }} ({{ carouselCfg.arrowMetalness }})</label>\r\n <input id=\"scene3d-carousel-arrow-metalness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.arrowMetalness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-roughness\">{{ t('scene3d.repeater.carousel_roughness', 'Roughness') }} ({{ carouselCfg.arrowRoughness }})</label>\r\n <input id=\"scene3d-carousel-arrow-roughness\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.arrowRoughness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-carousel-arrow-clearcoat\">{{ t('scene3d.repeater.carousel_clearcoat', 'Clearcoat') }} ({{ carouselCfg.arrowClearcoat }})</label>\r\n <input id=\"scene3d-carousel-arrow-clearcoat\" type=\"range\" min=\"0\" max=\"1\" step=\"0.01\" [(ngModel)]=\"carouselCfg.arrowClearcoat\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n }\r\n }\r\n }\r\n\r\n <!-- Archetipo Chart 3D: bar->cilindri, pie->donut(torus), area->ribbon. -->\r\n @if (repeaterCfg.layout.mode === 'chart') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('chart')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('chart')\" [class.pi-chevron-down]=\"!isSectionCollapsed('chart')\"></i>\r\n {{ t('scene3d.repeater.chart', 'Chart 3D') }}\r\n </div>\r\n @if (!isSectionCollapsed('chart')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-type\">{{ t('scene3d.repeater.chart_type', 'Tipo chart') }}</label>\r\n <select id=\"scene3d-chart-type\" class=\"scene3d-material-select\" [(ngModel)]=\"chartCfg.type\" (ngModelChange)=\"onRepeaterConfigChange()\">\r\n <option value=\"bar\">{{ t('scene3d.repeater.chart_bar', 'Barre (cilindri)') }}</option>\r\n <option value=\"pie\">{{ t('scene3d.repeater.chart_pie', 'Torta (donut)') }}</option>\r\n <option value=\"area\">{{ t('scene3d.repeater.chart_area', 'Area (line/spline)') }}</option>\r\n <option value=\"funnel\">{{ t('scene3d.repeater.chart_funnel', 'Funnel (coni)') }}</option>\r\n <option value=\"radar\">{{ t('scene3d.repeater.chart_radar', 'Radar (stella)') }}</option>\r\n <option value=\"scatter\">{{ t('scene3d.repeater.chart_scatter', 'Scatter / bolle') }}</option>\r\n </select>\r\n </div>\r\n @if (chartCfg.type !== 'scatter') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\r\n <p-select inputId=\"scene3d-chart-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n }\r\n @if (chartCfg.type === 'scatter') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-x\">{{ t('scene3d.repeater.chart_x', 'Colonna X') }}</label>\r\n <p-select inputId=\"scene3d-chart-x\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.xColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-y\">{{ t('scene3d.repeater.chart_y', 'Colonna Y (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-chart-y\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.yColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-z\">{{ t('scene3d.repeater.chart_z', 'Colonna Z (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-chart-z\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.zColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-size\">{{ t('scene3d.repeater.chart_size', 'Colonna dimensione (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-chart-size\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.sizeColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.chart_size_ph', 'Costante')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n }\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-color-col\">{{ t('scene3d.repeater.chart_color_col', 'Colonna colore (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-chart-color-col\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.colorColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.chart_palette', 'Palette automatica')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n @if (chartCfg.type === 'bar' || chartCfg.type === 'area' || chartCfg.type === 'funnel' || chartCfg.type === 'scatter') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-height\">{{ chartCfg.type === 'scatter' ? t('scene3d.repeater.chart_span', 'Estensione') : t('scene3d.repeater.chart_height', 'Altezza') }} ({{ chartCfg.height }})</label>\r\n <input id=\"scene3d-chart-height\" type=\"range\" min=\"1\" max=\"20\" step=\"0.5\" [(ngModel)]=\"chartCfg.height\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n }\r\n @if (chartCfg.type === 'bar' || chartCfg.type === 'area') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-spacing\">{{ t('scene3d.repeater.chart_spacing', 'Passo') }} ({{ chartCfg.spacing }})</label>\r\n <input id=\"scene3d-chart-spacing\" type=\"range\" min=\"0.5\" max=\"5\" step=\"0.1\" [(ngModel)]=\"chartCfg.spacing\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n }\r\n @if (chartCfg.type === 'bar' || chartCfg.type === 'scatter') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-bar-radius\">{{ chartCfg.type === 'scatter' ? t('scene3d.repeater.chart_bubble', 'Raggio bolla') : t('scene3d.repeater.chart_bar_radius', 'Raggio barra') }} ({{ chartCfg.barRadius }})</label>\r\n <input id=\"scene3d-chart-bar-radius\" type=\"range\" min=\"0.1\" max=\"2\" step=\"0.05\" [(ngModel)]=\"chartCfg.barRadius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n }\r\n @if (chartCfg.type === 'pie' || chartCfg.type === 'funnel' || chartCfg.type === 'radar') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-radius\">{{ t('scene3d.repeater.chart_radius', 'Raggio') }} ({{ chartCfg.radius }})</label>\r\n <input id=\"scene3d-chart-radius\" type=\"range\" min=\"1\" max=\"20\" step=\"0.5\" [(ngModel)]=\"chartCfg.radius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n }\r\n @if (chartCfg.type === 'pie' || chartCfg.type === 'area') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-thickness\">{{ chartCfg.type === 'pie' ? t('scene3d.repeater.chart_tube', 'Spessore donut') : t('scene3d.repeater.chart_depth', 'Profondita\\' area') }} ({{ chartCfg.thickness }})</label>\r\n <input id=\"scene3d-chart-thickness\" type=\"range\" min=\"0.1\" max=\"5\" step=\"0.1\" [(ngModel)]=\"chartCfg.thickness\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n }\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.chart_labels', 'Etichette') }}\r\n </label>\r\n @if (chartCfg.type === 'bar' || chartCfg.type === 'area') {\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.showAxes\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.chart_axes', 'Assi') }}\r\n </label>\r\n }\r\n @if (chartCfg.type === 'bar' || chartCfg.type === 'pie') {\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.showLegend\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.chart_legend', 'Legenda') }}\r\n </label>\r\n }\r\n @if (chartCfg.type === 'area') {\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"chartCfg.smooth\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.chart_smooth', 'Smoothing spline') }}\r\n </label>\r\n }\r\n </div>\r\n @if (chartCfg.showLabels) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-chart-label-col\">{{ t('scene3d.repeater.chart_label_col', 'Colonna etichetta (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-chart-label-col\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"chartCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\"\r\n [placeholder]=\"t('scene3d.repeater.chart_label_val', 'Usa il valore')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n }\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.repeater.chart_hint', 'La colonna valore guida l\\'altezza (barre/area) o la quota (torta). Premi Rigenera dopo le modifiche.') }}\r\n </div>\r\n }\r\n }\r\n\r\n <!-- Archetipo Surface / Heatmap (matrice riga\u00D7colonna\u00D7valore) -->\r\n @if (repeaterCfg.layout.mode === 'surface') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('surface')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('surface')\" [class.pi-chevron-down]=\"!isSectionCollapsed('surface')\"></i>\r\n {{ t('scene3d.repeater.surface', 'Surface / Heatmap') }}\r\n </div>\r\n @if (!isSectionCollapsed('surface')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-row\">{{ t('scene3d.repeater.surf_row', 'Colonna riga') }}</label>\r\n <p-select inputId=\"scene3d-surf-row\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"surfaceCfg.rowColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-col\">{{ t('scene3d.repeater.surf_col', 'Colonna colonna') }}</label>\r\n <p-select inputId=\"scene3d-surf-col\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"surfaceCfg.colColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\r\n <p-select inputId=\"scene3d-surf-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"surfaceCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-height\">{{ t('scene3d.repeater.surf_height', 'Scala altezza') }} ({{ surfaceCfg.heightScale }})</label>\r\n <input id=\"scene3d-surf-height\" type=\"range\" min=\"1\" max=\"20\" step=\"0.5\" [(ngModel)]=\"surfaceCfg.heightScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-cell\">{{ t('scene3d.repeater.surf_cell', 'Dimensione cella') }} ({{ surfaceCfg.cellSize }})</label>\r\n <input id=\"scene3d-surf-cell\" type=\"range\" min=\"0.4\" max=\"4\" step=\"0.1\" [(ngModel)]=\"surfaceCfg.cellSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-surf-color-low\">{{ t('scene3d.repeater.surf_colors', 'Colori (basso / alto)') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-surf-color-low\" type=\"color\" [(ngModel)]=\"surfaceCfg.colorLow\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n <input type=\"color\" [(ngModel)]=\"surfaceCfg.colorHigh\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n </div>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"surfaceCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.chart_labels', 'Etichette') }}\r\n </label>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.surf_hint', 'Riga \u00D7 colonna = celle; valore \u2192 altezza e colore. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n <!-- Archetipo Gauge / KPI (valore singolo dal primo record) -->\r\n @if (repeaterCfg.layout.mode === 'gauge') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('gauge')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('gauge')\" [class.pi-chevron-down]=\"!isSectionCollapsed('gauge')\"></i>\r\n {{ t('scene3d.repeater.gauge', 'Gauge / KPI') }}\r\n </div>\r\n @if (!isSectionCollapsed('gauge')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\r\n <p-select inputId=\"scene3d-gauge-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"gaugeCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-label\">{{ t('scene3d.repeater.gauge_label', 'Colonna etichetta (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-gauge-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"gaugeCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_label_val', 'Usa il valore')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-min\">{{ t('scene3d.repeater.gauge_range', 'Min / Max') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-gauge-min\" type=\"number\" step=\"1\" [(ngModel)]=\"gaugeCfg.min\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n <input type=\"number\" step=\"1\" [(ngModel)]=\"gaugeCfg.max\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-gauge-radius\">{{ t('scene3d.repeater.chart_radius', 'Raggio') }} ({{ gaugeCfg.radius }})</label>\r\n <input id=\"scene3d-gauge-radius\" type=\"range\" min=\"1\" max=\"12\" step=\"0.5\" [(ngModel)]=\"gaugeCfg.radius\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.gauge_hint', 'Usa il valore del PRIMO record. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n <!-- Archetipi Node-graph / Tree (gerarchia id \u2192 parent_id) -->\r\n @if (repeaterCfg.layout.mode === 'nodegraph' || repeaterCfg.layout.mode === 'tree') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('graph')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('graph')\" [class.pi-chevron-down]=\"!isSectionCollapsed('graph')\"></i>\r\n {{ repeaterCfg.layout.mode === 'tree' ? t('scene3d.repeater.tree', 'Albero (tree)') : t('scene3d.repeater.nodegraph', 'Node graph') }}\r\n </div>\r\n @if (!isSectionCollapsed('graph')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-id\">{{ t('scene3d.repeater.graph_id', 'Colonna ID') }}</label>\r\n <p-select inputId=\"scene3d-graph-id\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.idColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-parent\">{{ t('scene3d.repeater.graph_parent', 'Colonna padre') }}</label>\r\n <p-select inputId=\"scene3d-graph-parent\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.parentColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-label\">{{ t('scene3d.repeater.graph_label', 'Colonna etichetta') }}</label>\r\n <p-select inputId=\"scene3d-graph-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-value\">{{ t('scene3d.repeater.graph_value', 'Colonna valore (dim. nodo)') }}</label>\r\n <p-select inputId=\"scene3d-graph-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"graphCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\"\r\n optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_palette', 'Palette automatica')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-nodesize\">{{ t('scene3d.repeater.graph_nodesize', 'Dimensione nodo') }} ({{ graphCfg.nodeSize }})</label>\r\n <input id=\"scene3d-graph-nodesize\" type=\"range\" min=\"0.2\" max=\"2\" step=\"0.05\" [(ngModel)]=\"graphCfg.nodeSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-spacing\">{{ t('scene3d.repeater.graph_spacing', 'Spaziatura') }} ({{ graphCfg.spacing }})</label>\r\n <input id=\"scene3d-graph-spacing\" type=\"range\" min=\"1\" max=\"10\" step=\"0.5\" [(ngModel)]=\"graphCfg.spacing\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-graph-linkcolor\">{{ t('scene3d.repeater.graph_linkcolor', 'Colore archi') }}</label>\r\n <div class=\"scene3d-vector-row\"><input id=\"scene3d-graph-linkcolor\" type=\"color\" [(ngModel)]=\"graphCfg.linkColor\" (ngModelChange)=\"onRepeaterConfigChange()\" /></div>\r\n </div>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\">\r\n <input type=\"checkbox\" [(ngModel)]=\"graphCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n {{ t('scene3d.repeater.chart_labels', 'Etichette') }}\r\n </label>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.graph_hint', 'ID + padre definiscono la gerarchia; valore \u2192 dimensione nodo. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n <!-- Archetipo Map-pins (lat/lon \u2192 pin su piano) -->\r\n @if (repeaterCfg.layout.mode === 'map') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('map')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('map')\" [class.pi-chevron-down]=\"!isSectionCollapsed('map')\"></i>\r\n {{ t('scene3d.repeater.map', 'Mappa (pin)') }}\r\n </div>\r\n @if (!isSectionCollapsed('map')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-lat\">{{ t('scene3d.repeater.map_lat', 'Colonna latitudine') }}</label>\r\n <p-select inputId=\"scene3d-map-lat\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.latColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-lon\">{{ t('scene3d.repeater.map_lon', 'Colonna longitudine') }}</label>\r\n <p-select inputId=\"scene3d-map-lon\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.lonColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\r\n <p-select inputId=\"scene3d-map-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_size_ph', 'Costante')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-label\">{{ t('scene3d.repeater.map_label', 'Colonna etichetta (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-map-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"mapCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-size\">{{ t('scene3d.repeater.map_size', 'Dimensione mappa') }} ({{ mapCfg.planeSize }})</label>\r\n <input id=\"scene3d-map-size\" type=\"range\" min=\"6\" max=\"50\" step=\"1\" [(ngModel)]=\"mapCfg.planeSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-map-pincolor\">{{ t('scene3d.repeater.map_pincolor', 'Colore pin (senza valore)') }}</label>\r\n <div class=\"scene3d-vector-row\"><input id=\"scene3d-map-pincolor\" type=\"color\" [(ngModel)]=\"mapCfg.pinColor\" (ngModelChange)=\"onRepeaterConfigChange()\" /></div>\r\n </div>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"mapCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.map_hint', 'Lat/lon \u2192 pin sul piano; valore \u2192 altezza/colore. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n <!-- Archetipo Warehouse-shelf (corsia \u00D7 ripiano \u00D7 quantita') -->\r\n @if (repeaterCfg.layout.mode === 'warehouse') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('warehouse')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('warehouse')\" [class.pi-chevron-down]=\"!isSectionCollapsed('warehouse')\"></i>\r\n {{ t('scene3d.repeater.warehouse', 'Magazzino (scaffali)') }}\r\n </div>\r\n @if (!isSectionCollapsed('warehouse')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-aisle\">{{ t('scene3d.repeater.wh_aisle', 'Colonna corsia') }}</label>\r\n <p-select inputId=\"scene3d-wh-aisle\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.aisleColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-shelf\">{{ t('scene3d.repeater.wh_shelf', 'Colonna ripiano') }}</label>\r\n <p-select inputId=\"scene3d-wh-shelf\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.shelfColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-qty\">{{ t('scene3d.repeater.wh_qty', 'Colonna quantit\u00E0') }}</label>\r\n <p-select inputId=\"scene3d-wh-qty\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.qtyColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-label\">{{ t('scene3d.repeater.wh_label', 'Colonna articolo (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-wh-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"warehouseCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-wh-box\">{{ t('scene3d.repeater.wh_box', 'Dimensione scatola') }} ({{ warehouseCfg.boxSize }})</label>\r\n <input id=\"scene3d-wh-box\" type=\"range\" min=\"0.4\" max=\"3\" step=\"0.1\" [(ngModel)]=\"warehouseCfg.boxSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"warehouseCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.wh_hint', 'Corsia \u00D7 ripiano = posizione; quantit\u00E0 \u2192 altezza scatola. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n <!-- Archetipo Kanban 3D (stato \u2192 colonne di card) -->\r\n @if (repeaterCfg.layout.mode === 'kanban') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('kanban')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('kanban')\" [class.pi-chevron-down]=\"!isSectionCollapsed('kanban')\"></i>\r\n {{ t('scene3d.repeater.kanban', 'Kanban 3D') }}\r\n </div>\r\n @if (!isSectionCollapsed('kanban')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-kb-status\">{{ t('scene3d.repeater.kb_status', 'Colonna stato') }}</label>\r\n <p-select inputId=\"scene3d-kb-status\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"kanbanCfg.statusColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-kb-label\">{{ t('scene3d.repeater.kb_label', 'Colonna titolo (opz.)') }}</label>\r\n <p-select inputId=\"scene3d-kb-label\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"kanbanCfg.labelColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-kb-card\">{{ t('scene3d.repeater.kb_card', 'Larghezza card') }} ({{ kanbanCfg.cardWidth }})</label>\r\n <input id=\"scene3d-kb-card\" type=\"range\" min=\"0.8\" max=\"4\" step=\"0.1\" [(ngModel)]=\"kanbanCfg.cardWidth\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"kanbanCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.kb_hint', 'Ogni stato = una colonna; le card si impilano. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n <!-- Archetipo Calendar-wall (data \u2192 griglia settimane\u00D7giorni) -->\r\n @if (repeaterCfg.layout.mode === 'calendar') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('calendar')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('calendar')\" [class.pi-chevron-down]=\"!isSectionCollapsed('calendar')\"></i>\r\n {{ t('scene3d.repeater.calendar', 'Calendario') }}\r\n </div>\r\n @if (!isSectionCollapsed('calendar')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-date\">{{ t('scene3d.repeater.cal_date', 'Colonna data') }}</label>\r\n <p-select inputId=\"scene3d-cal-date\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"calendarCfg.dateColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\r\n <p-select inputId=\"scene3d-cal-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"calendarCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.chart_size_ph', 'Costante')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-height\">{{ t('scene3d.repeater.surf_height', 'Scala altezza') }} ({{ calendarCfg.heightScale }})</label>\r\n <input id=\"scene3d-cal-height\" type=\"range\" min=\"1\" max=\"15\" step=\"0.5\" [(ngModel)]=\"calendarCfg.heightScale\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-cal-cell\">{{ t('scene3d.repeater.surf_cell', 'Dimensione cella') }} ({{ calendarCfg.cellSize }})</label>\r\n <input id=\"scene3d-cal-cell\" type=\"range\" min=\"0.4\" max=\"3\" step=\"0.1\" [(ngModel)]=\"calendarCfg.cellSize\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"calendarCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.cal_hint', 'Le date riempiono una griglia settimane\u00D7giorni; valore \u2192 altezza/colore. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n <!-- Archetipo Stacked bars (categoria \u00D7 serie) -->\r\n @if (repeaterCfg.layout.mode === 'stacked') {\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"toggleSection('stacked')\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"isSectionCollapsed('stacked')\" [class.pi-chevron-down]=\"!isSectionCollapsed('stacked')\"></i>\r\n {{ t('scene3d.repeater.stacked', 'Barre impilate') }}\r\n </div>\r\n @if (!isSectionCollapsed('stacked')) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-category\">{{ t('scene3d.repeater.st_category', 'Colonna categoria') }}</label>\r\n <p-select inputId=\"scene3d-st-category\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"stackedCfg.categoryColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-series\">{{ t('scene3d.repeater.st_series', 'Colonna serie') }}</label>\r\n <p-select inputId=\"scene3d-st-series\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"stackedCfg.seriesColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-value\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\r\n <p-select inputId=\"scene3d-st-value\" [options]=\"bindingColumnOptions\" [(ngModel)]=\"stackedCfg.valueColumn\" (onChange)=\"onRepeaterConfigChange()\" optionLabel=\"label\" optionValue=\"value\" [filter]=\"true\" [showClear]=\"true\" [placeholder]=\"t('scene3d.repeater.col_ph', 'Colonna...')\" appendTo=\"body\" styleClass=\"scene3d-binding-select\"></p-select>\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-height\">{{ t('scene3d.repeater.chart_height', 'Altezza') }} ({{ stackedCfg.height }})</label>\r\n <input id=\"scene3d-st-height\" type=\"range\" min=\"2\" max=\"20\" step=\"0.5\" [(ngModel)]=\"stackedCfg.height\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-st-bar-width\">{{ t('scene3d.repeater.chart_bar_radius', 'Larghezza barra') }} ({{ stackedCfg.barWidth }})</label>\r\n <input id=\"scene3d-st-bar-width\" type=\"range\" min=\"0.4\" max=\"3\" step=\"0.1\" [(ngModel)]=\"stackedCfg.barWidth\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"stackedCfg.showLegend\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_legend', 'Legenda') }}</label>\r\n <label class=\"scene3d-prop-label scene3d-prop-checkbox\"><input type=\"checkbox\" [(ngModel)]=\"stackedCfg.showLabels\" (ngModelChange)=\"onRepeaterConfigChange()\" /> {{ t('scene3d.repeater.chart_labels', 'Etichette') }}</label>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.st_hint', 'Categoria = colonna; serie = segmenti impilati. Premi Rigenera.') }}</div>\r\n }\r\n }\r\n\r\n @if (repeaterCfg.layout.mode === 'grid' || repeaterCfg.layout.mode === 'cube') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-order\">{{ t('scene3d.repeater.order', 'Ordine di flusso') }}</label>\r\n <select id=\"scene3d-repeater-order\" class=\"scene3d-material-select\" [(ngModel)]=\"repeaterCfg.layout.order\" (ngModelChange)=\"onRepeaterConfigChange()\">\r\n @for (o of layoutOrderOptions; track o.value) { <option [value]=\"o.value\">{{ o.label }}</option> }\r\n </select>\r\n </div>\r\n }\r\n <!-- Verso di riempimento per-asse (o senso di rotazione per il cerchio). -->\r\n @for (ax of layoutAxes; track ax) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" [attr.for]=\"'scene3d-axis-dir-' + ax\">{{ axisDirLabel(ax) }}</label>\r\n <select class=\"scene3d-material-select\" [id]=\"'scene3d-axis-dir-' + ax\" [ngModel]=\"getAxisDir(ax)\" (ngModelChange)=\"setAxisDir(ax, $event)\">\r\n @for (o of axisDirOptions(ax); track o.value) { <option [ngValue]=\"o.value\">{{ o.label }}</option> }\r\n </select>\r\n </div>\r\n }\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-pos-range\">{{ t('scene3d.repeater.pos_range', 'Range posizione') }} ({{ repeaterCfg.normalize.posRange }})</label>\r\n <input id=\"scene3d-repeater-pos-range\" type=\"range\" min=\"4\" max=\"200\" step=\"2\" [(ngModel)]=\"repeaterCfg.normalize.posRange\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-repeater-scale-min\">{{ t('scene3d.repeater.scale_range', 'Scala min/max') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-repeater-scale-min\" type=\"number\" step=\"0.1\" min=\"0.01\" [(ngModel)]=\"repeaterCfg.normalize.scaleMin\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n <input type=\"number\" step=\"0.1\" min=\"0.01\" [(ngModel)]=\"repeaterCfg.normalize.scaleMax\" (ngModelChange)=\"onRepeaterConfigChange()\" />\r\n </div>\r\n </div>\r\n\r\n <p-button\r\n icon=\"pi pi-refresh\"\r\n [label]=\"t('scene3d.repeater.regenerate', 'Rigenera') + (repeaterCount ? ' (' + repeaterCount + ')' : '')\"\r\n size=\"small\" [loading]=\"repeaterRegenerating\"\r\n (onClick)=\"regenerateSelectedRepeater()\">\r\n </p-button>\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.repeater.hint', 'Le mesh sono rigenerate dai dati (non salvate nel JSON). Sposta il gruppo coi gizmo; click su una mesh apre la CRUD del record nel viewer.') }}\r\n </div>\r\n\r\n <!-- Interazioni: callback JS su click/dblclick/rightclick di un item.\r\n Sezione collassabile in fondo (collassata di default). -->\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"callbacksSectionOpen = !callbacksSectionOpen\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"!callbacksSectionOpen\" [class.pi-chevron-down]=\"callbacksSectionOpen\"></i>\r\n {{ t('scene3d.repeater.interactions', 'Interazioni (callback JS)') }}\r\n </div>\r\n @if (callbacksSectionOpen) {\r\n <div class=\"scene3d-prop-group\">\r\n <p-button size=\"small\" [text]=\"true\"\r\n [severity]=\"repeaterCallbackIsSet('click') ? 'primary' : 'secondary'\"\r\n icon=\"pi pi-code\"\r\n [label]=\"(repeaterCallbackIsSet('click') ? '\u25CF ' : '') + t('scene3d.repeater.cb_click', 'Click')\"\r\n (onClick)=\"editRepeaterCallback('click')\"></p-button>\r\n <p-button size=\"small\" [text]=\"true\"\r\n [severity]=\"repeaterCallbackIsSet('dblclick') ? 'primary' : 'secondary'\"\r\n icon=\"pi pi-code\"\r\n [label]=\"(repeaterCallbackIsSet('dblclick') ? '\u25CF ' : '') + t('scene3d.repeater.cb_dblclick', 'Doppio-click')\"\r\n (onClick)=\"editRepeaterCallback('dblclick')\"></p-button>\r\n <p-button size=\"small\" [text]=\"true\"\r\n [severity]=\"repeaterCallbackIsSet('rightclick') ? 'primary' : 'secondary'\"\r\n icon=\"pi pi-code\"\r\n [label]=\"(repeaterCallbackIsSet('rightclick') ? '\u25CF ' : '') + t('scene3d.repeater.cb_rightclick', 'Tasto destro')\"\r\n (onClick)=\"editRepeaterCallback('rightclick')\"></p-button>\r\n </div>\r\n <div class=\"scene3d-binding-hint\">{{ t('scene3d.repeater.cb_hint', 'Nel callback: record (item), mesh, wuic.showItemPopup({mode,fields,title}). Vale in designer e viewer.') }}</div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Fisica per-oggetto (Rapier): corpo rigido + parametri -->\r\n @if (selectedIsMesh) {\r\n <div class=\"scene3d-material-section\">\r\n <div class=\"scene3d-panel-subtitle\">{{ t('scene3d.physics.title', 'Fisica') }}</div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-body\">{{ t('scene3d.physics.body', 'Corpo') }}</label>\r\n <select id=\"scene3d-physics-body\" class=\"scene3d-material-select\" [(ngModel)]=\"propPhysics.type\" (ngModelChange)=\"onPhysicsInputChange()\">\r\n <option value=\"none\">{{ t('scene3d.physics.none', 'Nessuno') }}</option>\r\n <option value=\"static\">{{ t('scene3d.physics.static', 'Statico') }}</option>\r\n <option value=\"dynamic\">{{ t('scene3d.physics.dynamic', 'Dinamico') }}</option>\r\n <option value=\"kinematic\">{{ t('scene3d.physics.kinematic', 'Cinematico') }}</option>\r\n </select>\r\n </div>\r\n @if (propPhysics.type !== 'none') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-shape\">{{ t('scene3d.physics.shape', 'Forma collider') }}</label>\r\n <select id=\"scene3d-physics-shape\" class=\"scene3d-material-select\" [(ngModel)]=\"propPhysics.shape\" (ngModelChange)=\"onPhysicsInputChange()\">\r\n <option value=\"auto\">{{ t('scene3d.physics.shape_auto', 'Auto (dalla geometria)') }}</option>\r\n <option value=\"box\">Box</option>\r\n <option value=\"sphere\">{{ t('scene3d.physics.shape_sphere', 'Sfera') }}</option>\r\n <option value=\"hull\">{{ t('scene3d.physics.shape_hull', 'Convex hull') }}</option>\r\n </select>\r\n </div>\r\n @if (propPhysics.type === 'dynamic') {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-mass\">{{ t('scene3d.physics.mass', 'Massa (kg)') }}</label>\r\n <input id=\"scene3d-physics-mass\" type=\"number\" step=\"0.1\" min=\"0.001\" [(ngModel)]=\"propPhysics.mass\" (ngModelChange)=\"onPhysicsInputChange()\" />\r\n </div>\r\n }\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-friction\">{{ t('scene3d.physics.friction', 'Attrito') }} ({{ propPhysics.friction }})</label>\r\n <input id=\"scene3d-physics-friction\" type=\"range\" min=\"0\" max=\"1\" step=\"0.05\" [(ngModel)]=\"propPhysics.friction\" (ngModelChange)=\"onPhysicsInputChange()\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-physics-restitution\">{{ t('scene3d.physics.restitution', 'Rimbalzo') }} ({{ propPhysics.restitution }})</label>\r\n <input id=\"scene3d-physics-restitution\" type=\"range\" min=\"0\" max=\"1\" step=\"0.05\" [(ngModel)]=\"propPhysics.restitution\" (ngModelChange)=\"onPhysicsInputChange()\" />\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Registrazione animazione: keyframe pos/rot/scala -> AnimationClip -->\r\n <div class=\"scene3d-anim-section\">\r\n <div class=\"scene3d-panel-subtitle scene3d-collapsible\" (click)=\"animRecSectionOpen = !animRecSectionOpen\">\r\n <i class=\"pi\" [class.pi-chevron-right]=\"!animRecSectionOpen\" [class.pi-chevron-down]=\"animRecSectionOpen\"></i>\r\n {{ t('scene3d.animrec.title', 'Registra animazione') }}\r\n </div>\r\n @if (animRecSectionOpen) {\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-animrec-clip-name\">{{ t('scene3d.animrec.clip_name', 'Nome clip') }}</label>\r\n <input id=\"scene3d-animrec-clip-name\" type=\"text\" [(ngModel)]=\"animRecClipName\" [placeholder]=\"t('scene3d.animrec.clip_name_ph', 'Clip 1')\" />\r\n </div>\r\n <div class=\"scene3d-prop-group\">\r\n <label class=\"scene3d-prop-label\" for=\"scene3d-animrec-time\">{{ t('scene3d.animrec.time', 'Tempo T (s)') }}</label>\r\n <div class=\"scene3d-vector-row\">\r\n <input id=\"scene3d-animrec-time\" type=\"number\" step=\"0.1\" min=\"0\" [(ngModel)]=\"animRecTime\" />\r\n <p-button\r\n icon=\"pi pi-plus\"\r\n [label]=\"t('scene3d.animrec.add', 'Aggiungi keyframe')\"\r\n size=\"small\"\r\n (onClick)=\"addRecKeyframe()\">\r\n </p-button>\r\n </div>\r\n </div>\r\n\r\n @if (animRecKeyframes.length > 0) {\r\n <div class=\"scene3d-kf-list\">\r\n @for (kf of animRecKeyframes; track kf.time; let i = $index) {\r\n <div class=\"scene3d-kf-row\">\r\n <button type=\"button\" class=\"scene3d-kf-apply\" (click)=\"applyRecKeyframe(i)\"\r\n [title]=\"t('scene3d.animrec.apply', 'Applica questa posa all\\'oggetto')\">\r\n <i class=\"pi pi-eye\"></i> t = {{ kf.time }}s\r\n </button>\r\n <button type=\"button\" class=\"scene3d-kf-delete\" (click)=\"removeRecKeyframe(i)\"\r\n [title]=\"t('designer.remove', 'Rimuovi')\">\r\n <i class=\"pi pi-trash\"></i>\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <p-button\r\n icon=\"pi pi-video\"\r\n [label]=\"t('scene3d.animrec.generate', 'Genera clip')\"\r\n size=\"small\"\r\n [disabled]=\"animRecKeyframes.length < 2\"\r\n (onClick)=\"generateRecClip()\">\r\n </p-button>\r\n <div class=\"scene3d-binding-hint\">\r\n {{ t('scene3d.animrec.hint', 'Posa l\\'oggetto coi gizmo, aggiungi un keyframe al tempo T, ripeti. La clip generata usa il transport qui sopra e persiste col salvataggio (autoplay nel viewer).') }}\r\n </div>\r\n }\r\n </div>\r\n\r\n <div class=\"scene3d-gizmo-hint\">\r\n {{ t('scene3d.props.gizmo_hint', 'g = sposta, r = ruota, s = scala') }}\r\n </div>\r\n } @else {\r\n <div class=\"scene3d-props-empty\">\r\n {{ t('scene3d.props.empty', 'Nessun oggetto selezionato') }}\r\n </div>\r\n }\r\n </div>\r\n\r\n </div>\r\n\r\n <!-- Footer componenti scena (mirror del footer del designer 2D):\r\n click = seleziona, right-click = ctx menu con Rimuovi. -->\r\n <div class=\"scene3d-footer\">\r\n <span class=\"scene3d-footer-label\">{{ t('scene3d.footer.title', 'Scena') }}:</span>\r\n <div class=\"scene3d-footer-list\">\r\n @for (item of sceneObjects; track item.uuid) {\r\n <button\r\n type=\"button\"\r\n class=\"scene3d-footer-item\"\r\n [class.active]=\"item.uuid === selectedObjectUuid\"\r\n [title]=\"item.label\"\r\n (click)=\"selectFromFooter(item.uuid)\"\r\n (contextmenu)=\"showFooterContextMenu($event, item.uuid, sceneFooterCtxMenu)\">\r\n {{ item.label }}\r\n </button>\r\n } @empty {\r\n <span class=\"scene3d-footer-empty\">{{ t('scene3d.footer.empty', 'Nessun componente nella scena') }}</span>\r\n }\r\n </div>\r\n <p-contextMenu #sceneFooterCtxMenu [model]=\"footerCtxItems\" appendTo=\"body\" />\r\n </div>\r\n</div>\r\n", styles: [":host{display:block;width:100%;height:100%;min-height:0}.scene3d-designer-shell{display:flex;flex-direction:column;width:100%;height:100%;min-height:480px}.scene3d-toolbar{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid var(--surface-border, #dee2e6);flex:0 0 auto;flex-wrap:wrap}.scene3d-load-select{min-width:14rem}.scene3d-body{display:flex;flex:1 1 auto;min-height:0}.scene3d-palette{flex:0 0 12rem;display:flex;flex-direction:column;gap:.25rem;padding:.5rem;overflow-y:auto;border-right:1px solid var(--surface-border, #dee2e6)}.scene3d-panel-title{font-weight:600;font-size:.85rem;text-transform:uppercase;opacity:.7;margin-bottom:.25rem}.scene3d-palette-item{display:flex;align-items:center;gap:.5rem;padding:.4rem .5rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;background:var(--surface-card, #fff);cursor:pointer;text-align:left;font-size:.85rem;color:inherit}.scene3d-palette-item:hover:not(:disabled){background:var(--surface-hover, #f5f5f5)}.scene3d-palette-item:disabled{opacity:.45;cursor:not-allowed}.scene3d-palette-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.scene3d-viewport{flex:1 1 auto;min-width:0;position:relative;overflow:hidden;background:#1e1e28}.scene3d-viewport canvas{display:block;width:100%;height:100%}.scene3d-properties{flex:0 0 16rem;display:flex;flex-direction:column;gap:.6rem;padding:.75rem;overflow-y:auto;border-left:1px solid var(--surface-border, #dee2e6)}.scene3d-prop-group{display:flex;flex-direction:column;gap:.25rem}.scene3d-prop-label{font-size:.8rem;font-weight:600;opacity:.8}.scene3d-prop-readonly{font-size:.85rem;opacity:.7;word-break:break-all}.scene3d-vector-row{display:flex;gap:.25rem}.scene3d-vector-row input{width:100%;min-width:0;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem}.scene3d-props-empty,.scene3d-gizmo-hint{font-size:.8rem;opacity:.6}.scene3d-prop-checkbox{display:inline-flex;align-items:center;gap:.4rem;cursor:pointer}.scene3d-kf-list{display:flex;flex-direction:column;gap:3px;max-height:160px;overflow-y:auto;border:1px dashed var(--surface-border, #dee2e6);border-radius:4px;padding:4px}.scene3d-kf-row{display:flex;align-items:center;justify-content:space-between;gap:6px}.scene3d-kf-apply,.scene3d-kf-delete{border:1px solid var(--surface-border, #dee2e6);background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.78rem;cursor:pointer;display:inline-flex;align-items:center;gap:4px}.scene3d-kf-apply{flex:1 1 auto;justify-content:flex-start}.scene3d-kf-apply:hover{border-color:#3b82f6}.scene3d-kf-delete:hover{border-color:#ef4444;color:#ef4444}.scene3d-anim-section input[type=text],.scene3d-anim-section input[type=number]{width:100%;min-width:0;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem}.scene3d-material-section{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6)}.scene3d-material-section input[type=range]{width:100%}.scene3d-material-select{width:100%;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem;background:var(--surface-card, #fff);color:inherit}.scene3d-shader-toggle{cursor:pointer;-webkit-user-select:none;user-select:none}.scene3d-shader-src{width:100%;min-width:0;resize:vertical;padding:.35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-family:Consolas,Monaco,monospace;font-size:.72rem;line-height:1.35;background:#14141c;color:#d1d5db}.scene3d-pt-hud{position:absolute;top:8px;left:50%;transform:translate(-50%);z-index:25;display:flex;align-items:center;gap:12px;padding:4px 12px;border-radius:6px;background:#14141cd9;color:#e5e7eb;font-size:.78rem;border:1px solid #3b82f6}.scene3d-pt-scale{display:inline-flex;align-items:center;gap:6px}.scene3d-pt-scale input[type=range]{width:90px}.scene3d-pt-exit{border:1px solid #3b82f6;background:transparent;color:#93c5fd;border-radius:4px;padding:2px 8px;font-size:.75rem;cursor:pointer}.scene3d-cam-preview{position:absolute;z-index:20;width:320px;border:1px solid #3b82f6;border-radius:6px;background:#14141c;box-shadow:0 6px 24px #00000073;overflow:hidden}.scene3d-cam-preview-header{display:flex;align-items:center;justify-content:space-between;padding:3px 8px;background:#1f2937;color:#e5e7eb;font-size:.75rem;cursor:move;-webkit-user-select:none;user-select:none}.scene3d-cam-preview-close{border:none;background:transparent;color:#e5e7eb;font-size:1rem;line-height:1;cursor:pointer;padding:0 2px}.scene3d-cam-preview-body{width:320px;height:192px}.scene3d-cam-preview-body canvas{display:block}.scene3d-gizmo-hint{margin-top:.5rem;padding-top:.5rem;border-top:1px dashed var(--surface-border, #dee2e6)}.scene3d-binding-section{display:flex;flex-direction:column;gap:.6rem;margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6)}.scene3d-panel-subtitle{font-weight:600;font-size:.78rem;text-transform:uppercase;opacity:.7}.scene3d-panel-subtitle.scene3d-collapsible{display:flex;align-items:center;gap:.4rem;cursor:pointer;-webkit-user-select:none;user-select:none;padding:.15rem 0;opacity:.85}.scene3d-panel-subtitle.scene3d-collapsible:hover{opacity:1}.scene3d-panel-subtitle.scene3d-collapsible>.pi{font-size:.7rem;opacity:.8}.scene3d-field-map{display:flex;flex-direction:column;gap:.5rem;padding:.5rem;border:1px dashed var(--surface-border, #dee2e6);border-radius:4px}.scene3d-binding-select{width:100%}.scene3d-binding-section input[type=text]{width:100%;min-width:0;padding:.25rem .35rem;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;font-size:.8rem}.scene3d-open-record-btn{align-self:flex-start}.scene3d-binding-hint{font-size:.78rem;opacity:.6}.scene3d-anim-section{display:flex;flex-direction:column;gap:.6rem;margin-top:.5rem;padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6)}.scene3d-anim-transport{display:flex;gap:.35rem}.scene3d-anim-scrubber{width:100%;min-width:0}.scene3d-anim-time{font-size:.78rem;opacity:.6;text-align:right}.scene3d-footer{display:flex;align-items:center;gap:8px;padding:6px 8px;border-top:1px solid var(--surface-border, #dee2e6);min-height:36px;flex:0 0 auto;width:100%;overflow:hidden}.scene3d-footer-label{font-size:.85rem;font-weight:600;white-space:nowrap}.scene3d-footer-list{display:flex;align-items:center;gap:6px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;width:100%}.scene3d-footer-item{border:1px solid var(--surface-border, #dee2e6);background:transparent;color:inherit;border-radius:4px;padding:2px 8px;font-size:.8rem;line-height:1.25rem;cursor:pointer;white-space:nowrap;display:inline-flex;align-items:center;gap:4px}.scene3d-footer-item.active{border-color:#3b82f6;color:#1d4ed8}.scene3d-footer-empty{font-size:.8rem;opacity:.6}.scene3d-scene-option{display:flex;align-items:center;gap:10px;min-width:0}.scene3d-scene-option-thumb{width:72px;height:44px;flex:0 0 auto;border-radius:4px;object-fit:cover;border:1px solid #cbd5e1;background:#1e1e28}.scene3d-scene-option-thumb-empty{display:inline-flex;align-items:center;justify-content:center;color:#64748b}.scene3d-scene-option-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"] }]
|
|
4799
4799
|
}], ctorParameters: () => [{ type: DataProviderService }, { type: MetadataProviderService }, { type: i1.Router }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: ChatbotHostRegistryService }], propDecorators: { viewportContainer: [{
|
|
4800
4800
|
type: ViewChild,
|
|
4801
4801
|
args: ['viewportContainer', { static: false }]
|
|
@@ -4810,4 +4810,4 @@ var scene3dDesigner_component = /*#__PURE__*/Object.freeze({
|
|
|
4810
4810
|
});
|
|
4811
4811
|
|
|
4812
4812
|
export { WUIC_BAKED_FLAG as W, isBakeableMesh as i, scene3dDesigner_component as s };
|
|
4813
|
-
//# sourceMappingURL=wuic-framework-lib-scene3d-designer.component-
|
|
4813
|
+
//# sourceMappingURL=wuic-framework-lib-scene3d-designer.component-LhC0KzWO.mjs.map
|