wuic-framework-lib 1.5.0 → 1.7.0

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.
Files changed (26) hide show
  1. package/fesm2022/{wuic-framework-lib-chart-list.component-0J1ghIrS.mjs → wuic-framework-lib-chart-list.component-Bl424n7x.mjs} +50 -38
  2. package/fesm2022/{wuic-framework-lib-code-editor.component-C21mvjyr.mjs → wuic-framework-lib-code-editor.component-N6HMl2mf.mjs} +174 -274
  3. package/fesm2022/{wuic-framework-lib-designer.component-x10ZzUEy.mjs → wuic-framework-lib-designer.component-Bd2fwl6H.mjs} +499 -581
  4. package/fesm2022/{wuic-framework-lib-first-run-wizard.component-BuSxs_CM.mjs → wuic-framework-lib-first-run-wizard.component-NN6cWVHL.mjs} +22 -24
  5. package/fesm2022/{wuic-framework-lib-framework-docs.component-xTGLbEnp.mjs → wuic-framework-lib-framework-docs.component-B867JAYe.mjs} +2912 -294
  6. package/fesm2022/{wuic-framework-lib-pivot-builder.component-BkFFjPm4.mjs → wuic-framework-lib-pivot-builder.component-DWukH0L-.mjs} +580 -68
  7. package/fesm2022/{wuic-framework-lib-prompt-dialog-fallback-host.component-Bi-TrJB6.mjs → wuic-framework-lib-prompt-dialog-fallback-host.component-DWO1Rcgv.mjs} +2 -2
  8. package/fesm2022/wuic-framework-lib-report-designer.component-QcrU81m6.mjs +380 -0
  9. package/fesm2022/wuic-framework-lib-report-scaffold-dialog.component-U-zdOoRV.mjs +431 -0
  10. package/fesm2022/{wuic-framework-lib-report-viewer.component-BR-BfI2r.mjs → wuic-framework-lib-report-viewer.component-B4LhA3SL.mjs} +8 -13
  11. package/fesm2022/{wuic-framework-lib-scene3d-designer.component-BQzFxSXP.mjs → wuic-framework-lib-scene3d-designer.component-DjOfzKLo.mjs} +323 -88
  12. package/fesm2022/{wuic-framework-lib-scene3d-light-baker-gpu-0LjqhSNn.mjs → wuic-framework-lib-scene3d-light-baker-gpu-C96T9uIW.mjs} +2 -2
  13. package/fesm2022/{wuic-framework-lib-scene3d-shader-effect-CQLWkuh1.mjs → wuic-framework-lib-scene3d-shader-effect-DjHyCAFK.mjs} +612 -32
  14. package/fesm2022/{wuic-framework-lib-scene3d-viewer.component-imoV3HOs.mjs → wuic-framework-lib-scene3d-viewer.component-Dboz1fjl.mjs} +149 -18
  15. package/fesm2022/{wuic-framework-lib-scheduler-list.component-xzlTEkoq.mjs → wuic-framework-lib-scheduler-list.component--wY21R3o.mjs} +169 -53
  16. package/fesm2022/{wuic-framework-lib-spreadsheet-list-sf.component-CrS6fOGj.mjs → wuic-framework-lib-spreadsheet-list-sf.component-Ek30J8v-.mjs} +149 -108
  17. package/fesm2022/{wuic-framework-lib-stimulsoft-license.service-BIyxB4pO.mjs → wuic-framework-lib-stimulsoft-license.service-CsNNSFYs.mjs} +3 -3
  18. package/fesm2022/wuic-framework-lib-timeline-list.component-aqCH4P0G.mjs +805 -0
  19. package/fesm2022/{wuic-framework-lib-workflow-designer.component-5xtQ7-6z.mjs → wuic-framework-lib-workflow-designer.component-DMM8BeXm.mjs} +180 -150
  20. package/fesm2022/{wuic-framework-lib-wuic-framework-lib-3TZ-j-yL.mjs → wuic-framework-lib-wuic-framework-lib-dfLTqam5.mjs} +6761 -2347
  21. package/fesm2022/wuic-framework-lib.mjs +1 -1
  22. package/package.json +4 -2
  23. package/styles/_wuic-tokens.scss +82 -0
  24. package/styles/styles.scss +160 -0
  25. package/types/wuic-framework-lib.d.ts +4264 -2691
  26. package/fesm2022/wuic-framework-lib-report-designer.component-ZvB46-LV.mjs +0 -194
@@ -4,11 +4,11 @@ import * as i1$1 from '@angular/forms';
4
4
  import { FormsModule } from '@angular/forms';
5
5
  import * as i4 from 'primeng/button';
6
6
  import { ButtonModule } from 'primeng/button';
7
- import * as i6 from 'primeng/select';
7
+ import * as i5 from 'primeng/select';
8
8
  import { SelectModule } from 'primeng/select';
9
9
  import * as i12 from 'primeng/splitbutton';
10
10
  import { SplitButtonModule } from 'primeng/splitbutton';
11
- import * as i10 from 'primeng/contextmenu';
11
+ import * as i14 from 'primeng/contextmenu';
12
12
  import { ContextMenuModule } from 'primeng/contextmenu';
13
13
  import { TranslateModule } from '@ngx-translate/core';
14
14
  import * as THREE from 'three';
@@ -17,30 +17,11 @@ 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 parseShaderEffect, S as SCENE3D_SHADER_EFFECT_DEFAULT, t as SCENE3D_SHADER_EFFECT_SCHEMA, u as buildEffectNodeMaterial, v as buildEffectShaderMaterial, w as getMeshRepeaterConfig, x as WUIC_REPEATER_CHILD, C as CarouselController, y as buildUploadTextureResolver, z as applyRepeaterTextureTo, A as defaultRepeaterTextureApply, B as regenerateRepeater, D as getPhysicsDef, E as createPhysicsWorld, F as WUIC_PHYSICS_KEY, G as WUIC_OBJECT_TYPE_KEY, H as carouselArrowDir, I as WUIC_BINDING_KEY, J as swapNodeEffectMaterialsForSerialization, K as serializeScene, L as deserializeScene } from './wuic-framework-lib-scene3d-shader-effect-CQLWkuh1.mjs';
21
- import { W as WtoolboxService, f as DataProviderService, M as MetadataProviderService, C as ChatbotHostRegistryService } from './wuic-framework-lib-wuic-framework-lib-3TZ-j-yL.mjs';
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-DjHyCAFK.mjs';
21
+ import { W as WtoolboxService, h as DataProviderService, M as MetadataProviderService, C as ChatbotHostRegistryService } from './wuic-framework-lib-wuic-framework-lib-dfLTqam5.mjs';
22
22
  import * as i1 from '@angular/router';
23
23
  import * as i3 from 'primeng/api';
24
24
 
25
- /** Estensioni accettate dall'import (allineate all'allowlist upload backend, F4). */
26
- const SCENE3D_ASSET_EXTENSIONS = {
27
- '.gltf': 'gltf',
28
- '.glb': 'glb',
29
- '.obj': 'obj',
30
- '.fbx': 'fbx',
31
- '.stl': 'stl',
32
- '.dae': 'dae',
33
- };
34
- /** Risolve il formato asset da un filename, o null se l'estensione non e' supportata. */
35
- function resolveAssetFormat(fileName) {
36
- const dot = fileName.lastIndexOf('.');
37
- if (dot < 0) {
38
- return null;
39
- }
40
- const ext = fileName.slice(dot).toLowerCase();
41
- return SCENE3D_ASSET_EXTENSIONS[ext] ?? null;
42
- }
43
-
44
25
  /** Marker su `userData` delle mesh bakate (per unbake e per riconoscerle). */
45
26
  const WUIC_BAKED_FLAG = 'wuicBakedLighting';
46
27
  const _pos = new THREE.Vector3();
@@ -66,7 +47,7 @@ function gatherLight(point, normal, lights, occluders, shadows) {
66
47
  }
67
48
  let lambert = 0;
68
49
  let attenuation = 1;
69
- let shadowDistance = Infinity;
50
+ let shadowDistance;
70
51
  if (light.isDirectionalLight) {
71
52
  const dl = light;
72
53
  dl.getWorldPosition(_lightPos);
@@ -133,7 +114,7 @@ function isBakeableMesh(obj) {
133
114
  const mesh = obj;
134
115
  return !!mesh.isMesh
135
116
  && obj.name !== 'wuicSelectionProxy'
136
- && !(obj.userData && obj.userData['wuicTransient'] === true)
117
+ && obj.userData?.['wuicTransient'] !== true
137
118
  && !!mesh.geometry?.getAttribute?.('position');
138
119
  }
139
120
  /**
@@ -193,7 +174,7 @@ function removeVertexBake(content) {
193
174
  let restored = 0;
194
175
  content.traverse((obj) => {
195
176
  const mesh = obj;
196
- if (!mesh.isMesh || !(obj.userData && obj.userData[WUIC_BAKED_FLAG] === true)) {
177
+ if (!mesh.isMesh || obj.userData?.[WUIC_BAKED_FLAG] !== true) {
197
178
  return;
198
179
  }
199
180
  const geometry = mesh.geometry;
@@ -304,6 +285,8 @@ class Scene3dDesignerComponent {
304
285
  contentGroup;
305
286
  raycaster = new THREE.Raycaster();
306
287
  pointer = new THREE.Vector2();
288
+ /** Hook per-frame del popup item (billboard del pannello 3D verso la camera). */
289
+ popupFrameHook = null;
307
290
  /** Helper direzionali per le luci utente (uuid luce -> helper in scena, transient). */
308
291
  lightHelpers = new Map();
309
292
  /** Luci di viewport di default (transient), spegnibili dal toggle in toolbar. */
@@ -314,6 +297,12 @@ class Scene3dDesignerComponent {
314
297
  shadowsEnabled = true;
315
298
  /** Toggle toolbar: guide di scena (proxy luci/camera + linee helper/frustum). */
316
299
  helpersVisible = true;
300
+ /** Toggle toolbar: rigenera automaticamente il repeater ad ogni cambio proprieta'
301
+ * (con debounce), senza premere "Rigenera". Vale per l'oggetto selezionato. */
302
+ autoRegen = false;
303
+ _autoRegenTimer = null;
304
+ /** Richiesta di regen arrivata mentre un altro regen era in corso (coalescing). */
305
+ _regenPending = false;
317
306
  // --- Renderer WebGPU (opt-in, iter node-materials) ---
318
307
  /** Modalita' renderer richiesta (persistita in viewport.rendererMode al save). */
319
308
  rendererMode = 'webgl';
@@ -381,6 +370,7 @@ class Scene3dDesignerComponent {
381
370
  // --- bound listeners (per removeEventListener in ngOnDestroy) ---
382
371
  onCanvasClick = (event) => this.handlePick(event);
383
372
  onCanvasDblClick = (event) => this.handleDoubleClick(event);
373
+ onCanvasContextMenu = (event) => this.handleRightClick(event);
384
374
  onKeyDown = (event) => this.handleKeyDown(event);
385
375
  // --- stato UI (property panel / toolbar) ---
386
376
  /** Palette: built-in + eventuali tipi custom da metadata (iter2). */
@@ -476,7 +466,8 @@ class Scene3dDesignerComponent {
476
466
  if (mode === 'line') {
477
467
  return ['x'];
478
468
  }
479
- const n = mode === 'cube' ? 3 : mode === 'grid' ? 2 : 0;
469
+ const gridAxesCount = mode === 'grid' ? 2 : 0;
470
+ const n = mode === 'cube' ? 3 : gridAxesCount;
480
471
  const order = String(this.repeaterCfg.layout?.order || (mode === 'cube' ? 'xyz' : 'xy'));
481
472
  return order.slice(0, n).split('');
482
473
  }
@@ -612,6 +603,8 @@ class Scene3dDesignerComponent {
612
603
  selectedIsMesh = false;
613
604
  selectedHasCustomShader = false;
614
605
  shaderSectionOpen = false;
606
+ /** Sezione callback interazione del repeater: collassata di default (in fondo al pannello). */
607
+ callbacksSectionOpen = false;
615
608
  shaderVertexSrc = SCENE3D_SHADER_VERTEX_TEMPLATE;
616
609
  shaderFragmentSrc = SCENE3D_SHADER_FRAGMENT_TEMPLATE;
617
610
  /** ShaderMaterial vivi con uniform uTime da animare nel render loop. */
@@ -833,6 +826,7 @@ class Scene3dDesignerComponent {
833
826
  this.zone.runOutsideAngular(() => {
834
827
  renderer.domElement.addEventListener('click', this.onCanvasClick);
835
828
  renderer.domElement.addEventListener('dblclick', this.onCanvasDblClick);
829
+ renderer.domElement.addEventListener('contextmenu', this.onCanvasContextMenu);
836
830
  });
837
831
  }
838
832
  /**
@@ -873,7 +867,8 @@ class Scene3dDesignerComponent {
873
867
  if (canvas) {
874
868
  canvas.removeEventListener('click', this.onCanvasClick);
875
869
  canvas.removeEventListener('dblclick', this.onCanvasDblClick);
876
- canvas.parentElement?.removeChild(canvas);
870
+ canvas.removeEventListener('contextmenu', this.onCanvasContextMenu);
871
+ canvas.remove();
877
872
  }
878
873
  this.renderer?.dispose();
879
874
  this.renderer = undefined;
@@ -1066,6 +1061,9 @@ class Scene3dDesignerComponent {
1066
1061
  }
1067
1062
  else {
1068
1063
  const activeCamera = (this.viewingThroughCamera && this.userViewCamera) ? this.userViewCamera : this.camera;
1064
+ if (this.popupFrameHook) {
1065
+ this.popupFrameHook();
1066
+ } // billboard pannello popup 3D
1069
1067
  this.renderer.render(this.scene, activeCamera);
1070
1068
  }
1071
1069
  }
@@ -1172,6 +1170,7 @@ class Scene3dDesignerComponent {
1172
1170
  if (canvas) {
1173
1171
  canvas.removeEventListener('click', this.onCanvasClick);
1174
1172
  canvas.removeEventListener('dblclick', this.onCanvasDblClick);
1173
+ canvas.removeEventListener('contextmenu', this.onCanvasContextMenu);
1175
1174
  }
1176
1175
  globalThis.removeEventListener('keydown', this.onKeyDown);
1177
1176
  this.transform?.detach();
@@ -1200,7 +1199,7 @@ class Scene3dDesignerComponent {
1200
1199
  }
1201
1200
  this.renderer?.dispose();
1202
1201
  if (canvas?.parentElement) {
1203
- canvas.parentElement.removeChild(canvas);
1202
+ canvas.remove();
1204
1203
  }
1205
1204
  this.renderer = undefined;
1206
1205
  this.scene = undefined;
@@ -1284,14 +1283,14 @@ class Scene3dDesignerComponent {
1284
1283
  /** Compone "<base> <n>" col primo progressivo libero fra i figli diretti. */
1285
1284
  uniqueObjectName(base) {
1286
1285
  const siblings = this.contentGroup?.children || [];
1287
- const esc = base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1286
+ const esc = base.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
1288
1287
  const rx = new RegExp('^' + esc + ' (\\d+)$');
1289
1288
  let max = 0;
1290
1289
  for (const c of siblings) {
1291
1290
  const name = c.name || '';
1292
- const m = name.match(rx);
1291
+ const m = rx.exec(name);
1293
1292
  if (m) {
1294
- max = Math.max(max, parseInt(m[1], 10));
1293
+ max = Math.max(max, Number.parseInt(m[1], 10));
1295
1294
  }
1296
1295
  else if (name === base) {
1297
1296
  max = Math.max(max, 1);
@@ -1420,8 +1419,8 @@ class Scene3dDesignerComponent {
1420
1419
  input.accept = '.glb,.gltf,.obj,.stl';
1421
1420
  input.style.display = 'none';
1422
1421
  input.addEventListener('change', () => {
1423
- const file = input.files && input.files[0];
1424
- document.body.removeChild(input);
1422
+ const file = input.files?.[0];
1423
+ input.remove();
1425
1424
  if (file) {
1426
1425
  void this.loadAssetFile(file);
1427
1426
  }
@@ -1578,7 +1577,7 @@ class Scene3dDesignerComponent {
1578
1577
  const targets = (this.contentGroup?.children || []).filter((c) => c.isLight === true || c.isCamera === true);
1579
1578
  const liveUuids = new Set(targets.map((l) => l.uuid));
1580
1579
  // Rimuovi helper orfani.
1581
- for (const [uuid, helper] of [...this.lightHelpers]) {
1580
+ for (const [uuid, helper] of this.lightHelpers) {
1582
1581
  if (!liveUuids.has(uuid)) {
1583
1582
  this.scene.remove(helper);
1584
1583
  helper.dispose?.();
@@ -1779,7 +1778,7 @@ class Scene3dDesignerComponent {
1779
1778
  const canvas = this.previewRenderer?.domElement;
1780
1779
  this.previewRenderer?.dispose();
1781
1780
  if (canvas?.parentElement) {
1782
- canvas.parentElement.removeChild(canvas);
1781
+ canvas.remove();
1783
1782
  }
1784
1783
  this.previewRenderer = undefined;
1785
1784
  }
@@ -2017,8 +2016,8 @@ class Scene3dDesignerComponent {
2017
2016
  material.flatShading = !!this.propMaterial.flatShading;
2018
2017
  material.needsUpdate = true; // flatShading richiede la ricompilazione
2019
2018
  }
2020
- const side = this.propMaterial.side === 'double' ? THREE.DoubleSide
2021
- : this.propMaterial.side === 'back' ? THREE.BackSide : THREE.FrontSide;
2019
+ const backOrFrontSide = this.propMaterial.side === 'back' ? THREE.BackSide : THREE.FrontSide;
2020
+ const side = this.propMaterial.side === 'double' ? THREE.DoubleSide : backOrFrontSide;
2022
2021
  if (material.side !== side) {
2023
2022
  material.side = side;
2024
2023
  material.needsUpdate = true;
@@ -2239,6 +2238,8 @@ class Scene3dDesignerComponent {
2239
2238
  this.selectedHasCustomShader = !!material?.isShaderMaterial && !material?.wuicShaderEffect;
2240
2239
  this.selectedHasShaderEffect = !!mesh?.isMesh && !!mesh.userData?.[WUIC_SHADER_EFFECT_KEY];
2241
2240
  if (this.selectedHasStandardMaterial) {
2241
+ const sideBackOrFront = material.side === THREE.BackSide ? 'back' : 'front';
2242
+ const attenuationDistanceValue = Number.isFinite(material.attenuationDistance) ? material.attenuationDistance : 0.5;
2242
2243
  this.propMaterial = {
2243
2244
  metalness: this.round(material.metalness ?? 0),
2244
2245
  roughness: this.round(material.roughness ?? 1),
@@ -2247,14 +2248,14 @@ class Scene3dDesignerComponent {
2247
2248
  opacity: this.round(material.opacity ?? 1),
2248
2249
  wireframe: !!material.wireframe,
2249
2250
  flatShading: !!material.flatShading,
2250
- side: material.side === THREE.DoubleSide ? 'double' : material.side === THREE.BackSide ? 'back' : 'front',
2251
+ side: material.side === THREE.DoubleSide ? 'double' : sideBackOrFront,
2251
2252
  transmission: material.isMeshPhysicalMaterial ? this.round(material.transmission ?? 0) : 0,
2252
2253
  ior: material.isMeshPhysicalMaterial ? this.round(material.ior ?? 1.5) : 1.5,
2253
2254
  thickness: material.isMeshPhysicalMaterial ? this.round(material.thickness ?? 0.5) : 0.5,
2254
2255
  attenuationColor: material.isMeshPhysicalMaterial
2255
2256
  ? `#${material.attenuationColor?.getHexString?.() || 'ffffff'}` : '#ffffff',
2256
2257
  attenuationDistance: material.isMeshPhysicalMaterial
2257
- ? this.round(Number.isFinite(material.attenuationDistance) ? material.attenuationDistance : 0.5) : 0.5
2258
+ ? this.round(attenuationDistanceValue) : 0.5
2258
2259
  };
2259
2260
  }
2260
2261
  if (this.selectedHasCustomShader) {
@@ -2322,7 +2323,7 @@ class Scene3dDesignerComponent {
2322
2323
  // triangle soup): ordini di grandezza piu' veloce del raycaster CPU
2323
2324
  // sulle scene dense. Fallback CPU trasparente su qualsiasi errore.
2324
2325
  try {
2325
- const { bakeVertexLightingGpu } = await import('./wuic-framework-lib-scene3d-light-baker-gpu-0LjqhSNn.mjs');
2326
+ const { bakeVertexLightingGpu } = await import('./wuic-framework-lib-scene3d-light-baker-gpu-C96T9uIW.mjs');
2326
2327
  result = await bakeVertexLightingGpu(this.renderer, this.contentGroup, lights, bakeOptions);
2327
2328
  engine = 'gpu';
2328
2329
  }
@@ -2381,7 +2382,7 @@ class Scene3dDesignerComponent {
2381
2382
  return codice !== null ? this.createCustomObject(codice) : this.createObject(type);
2382
2383
  }
2383
2384
  /** Dipendenze iniettate nel modulo repeater (factory/parser del componente). */
2384
- buildRepeaterDeps(resolveTextureSrc) {
2385
+ buildRepeaterDeps(resolveTextureSrc, resolveAssetSrc) {
2385
2386
  return {
2386
2387
  createMesh: (t) => this.createRepeaterMesh(t),
2387
2388
  applyColor: (obj, val) => this.applyColorString(obj, val),
@@ -2390,9 +2391,74 @@ class Scene3dDesignerComponent {
2390
2391
  createTextMesh: (text) => makeTextSpriteMesh(text),
2391
2392
  resolveRecordId: (record) => this.resolveRecordPk(record),
2392
2393
  unwrap: (v) => this.unwrapValue(v),
2393
- resolveTextureSrc
2394
+ resolveTextureSrc,
2395
+ resolveAssetSrc,
2396
+ loadAsset: (url) => this.loadRepeaterAsset(url)
2394
2397
  };
2395
2398
  }
2399
+ /** Cache per-URL degli asset del repeater (template clonabili). */
2400
+ repeaterAssetCache = new Map();
2401
+ /**
2402
+ * Carica (fetch + parser three) un asset 3D da URL per il mesh-repeater e lo
2403
+ * mette in cache per-URL (dedup tra record che puntano allo stesso file).
2404
+ * Ritorna un template da clonare, o `null` se il load/parse fallisce (il
2405
+ * repeater ricade su una primitiva). Riusa gli stessi loader dell'import manuale.
2406
+ */
2407
+ async loadRepeaterAsset(url) {
2408
+ if (this.repeaterAssetCache.has(url)) {
2409
+ return this.repeaterAssetCache.get(url);
2410
+ }
2411
+ let obj = null;
2412
+ try {
2413
+ const format = resolveAssetFormat(url.split('?')[0]);
2414
+ const resp = await fetch(url);
2415
+ if (!resp.ok) {
2416
+ throw new Error('HTTP ' + resp.status);
2417
+ }
2418
+ if (format === 'glb' || format === 'gltf') {
2419
+ const buffer = await resp.arrayBuffer();
2420
+ const gltf = await new Promise((res, rej) => new GLTFLoader().parse(buffer, '', res, rej));
2421
+ obj = gltf?.scene || gltf?.scenes?.[0] || null;
2422
+ }
2423
+ else if (format === 'obj') {
2424
+ obj = new OBJLoader().parse(await resp.text());
2425
+ }
2426
+ else if (format === 'stl') {
2427
+ const geometry = new STLLoader().parse(await resp.arrayBuffer());
2428
+ geometry.computeVertexNormals();
2429
+ obj = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0xb8bcc4, metalness: 0.15, roughness: 0.7 }));
2430
+ }
2431
+ }
2432
+ catch (e) {
2433
+ console.warn('[Scene3D] repeater asset load fallito', url, e);
2434
+ obj = null;
2435
+ }
2436
+ if (obj) {
2437
+ obj = normalizeAssetToUnit(obj);
2438
+ }
2439
+ this.repeaterAssetCache.set(url, obj);
2440
+ return obj;
2441
+ }
2442
+ /**
2443
+ * Costruisce (async) il resolver dell'asset mesh per un repeater in modalita'
2444
+ * `meshSource.mode='asset'`: se la colonna e' un upload compone l'URL
2445
+ * (filesystem/blob), altrimenti usa il valore as-is (URL diretto). Riusa
2446
+ * l'helper upload condiviso con la texture.
2447
+ */
2448
+ async buildAssetResolver(cfg) {
2449
+ const col = cfg.meshSource?.mode === 'asset' ? cfg.meshSource.column : '';
2450
+ if (!cfg.route || !col) {
2451
+ return undefined;
2452
+ }
2453
+ try {
2454
+ const columns = await this.metaSrv.getMetadati(cfg.route);
2455
+ const app = { uploadPath: WtoolboxService.appSettings?.upload_path || '', apiUrl: WtoolboxService.appSettings?.api_url || '' };
2456
+ return buildUploadTextureResolver(columns || [], cfg.route, col, app, (v) => this.unwrapValue(v)) || ((_rec, raw) => raw);
2457
+ }
2458
+ catch {
2459
+ return (_rec, raw) => raw;
2460
+ }
2461
+ }
2396
2462
  /**
2397
2463
  * Costruisce (async) il resolver texture per un repeater: se la colonna
2398
2464
  * `texture` e' un upload compone l'URL (filesystem/blob) come il framework
@@ -2491,6 +2557,26 @@ class Scene3dDesignerComponent {
2491
2557
  if (!group || !getMeshRepeaterConfig(group)) {
2492
2558
  return;
2493
2559
  }
2560
+ this.persistRepeaterCfg(group);
2561
+ // Auto-rigenera (toggle globale): debounce per non scatenare fetch/regen
2562
+ // concorrenti ad ogni tick di slider (la race sovrascriverebbe l'output).
2563
+ if (this.autoRegen) {
2564
+ if (this._autoRegenTimer) {
2565
+ clearTimeout(this._autoRegenTimer);
2566
+ }
2567
+ this._autoRegenTimer = setTimeout(() => {
2568
+ this._autoRegenTimer = null;
2569
+ void this.regenerateSelectedRepeater();
2570
+ }, 250);
2571
+ }
2572
+ }
2573
+ /**
2574
+ * Persiste la config corrente nel userData del gruppo + markDirty, SENZA
2575
+ * schedulare l'auto-regen. Separato da onRepeaterConfigChange perche'
2576
+ * regenerateSelectedRepeater deve salvare la cfg mentre rigenera senza
2577
+ * ri-armare il timer di auto-regen (che altrimenti si auto-richiama all'infinito).
2578
+ */
2579
+ persistRepeaterCfg(group) {
2494
2580
  group.userData[WUIC_MESH_REPEATER_KEY] = JSON.parse(JSON.stringify(this.repeaterCfg));
2495
2581
  this.markDirty();
2496
2582
  }
@@ -2512,7 +2598,20 @@ class Scene3dDesignerComponent {
2512
2598
  if (!group || !cfg) {
2513
2599
  return;
2514
2600
  }
2515
- this.onRepeaterConfigChange();
2601
+ // Regen gia' in corso: registra la richiesta e lascia che il finally la rilanci
2602
+ // una volta sola (coalescing: vince l'ultima modifica, niente overlap async).
2603
+ if (this.repeaterRegenerating) {
2604
+ this._regenPending = true;
2605
+ return;
2606
+ }
2607
+ // Stiamo rigenerando ORA: salva la cfg e annulla un auto-regen eventualmente in
2608
+ // coda. NON chiamare onRepeaterConfigChange qui: ri-armerebbe il timer di
2609
+ // auto-regen ad ogni regen → loop infinito con il bottone eternamente disabled.
2610
+ if (this._autoRegenTimer) {
2611
+ clearTimeout(this._autoRegenTimer);
2612
+ this._autoRegenTimer = null;
2613
+ }
2614
+ this.persistRepeaterCfg(group);
2516
2615
  if (!this.repeaterCfg.route) {
2517
2616
  addRepeaterPlaceholder(group);
2518
2617
  this.repeaterCount = 0;
@@ -2522,11 +2621,12 @@ class Scene3dDesignerComponent {
2522
2621
  this.repeaterRegenerating = true;
2523
2622
  this.cdr.markForCheck();
2524
2623
  try {
2525
- const [rows, resolver] = await Promise.all([
2624
+ const [rows, resolver, assetResolver] = await Promise.all([
2526
2625
  this.fetchRepeaterRows(this.repeaterCfg.route, this.repeaterCfg.maxRecords),
2527
- this.buildTextureResolver(this.repeaterCfg)
2626
+ this.buildTextureResolver(this.repeaterCfg),
2627
+ this.buildAssetResolver(this.repeaterCfg)
2528
2628
  ]);
2529
- this.repeaterCount = regenerateRepeater(group, this.repeaterCfg, rows, this.buildRepeaterDeps(resolver));
2629
+ this.repeaterCount = await regenerateRepeater(group, this.repeaterCfg, rows, this.buildRepeaterDeps(resolver, assetResolver));
2530
2630
  this.rebuildCarouselControllers();
2531
2631
  this.markDirty();
2532
2632
  }
@@ -2536,6 +2636,11 @@ class Scene3dDesignerComponent {
2536
2636
  finally {
2537
2637
  this.repeaterRegenerating = false;
2538
2638
  this.cdr.markForCheck();
2639
+ // Modifiche arrivate durante il regen: rilancia UNA volta con la cfg finale.
2640
+ if (this._regenPending) {
2641
+ this._regenPending = false;
2642
+ void this.regenerateSelectedRepeater();
2643
+ }
2539
2644
  }
2540
2645
  }
2541
2646
  /** Fetch righe rotta (prima pagina). Riuso del data-path selectByRoute. */
@@ -2566,11 +2671,12 @@ class Scene3dDesignerComponent {
2566
2671
  continue;
2567
2672
  }
2568
2673
  try {
2569
- const [rows, resolver] = await Promise.all([
2674
+ const [rows, resolver, assetResolver] = await Promise.all([
2570
2675
  this.fetchRepeaterRows(cfg.route, cfg.maxRecords),
2571
- this.buildTextureResolver(cfg)
2676
+ this.buildTextureResolver(cfg),
2677
+ this.buildAssetResolver(cfg)
2572
2678
  ]);
2573
- regenerateRepeater(group, cfg, rows, this.buildRepeaterDeps(resolver));
2679
+ await regenerateRepeater(group, cfg, rows, this.buildRepeaterDeps(resolver, assetResolver));
2574
2680
  }
2575
2681
  catch { /* best-effort per repeater */ }
2576
2682
  }
@@ -2587,16 +2693,16 @@ class Scene3dDesignerComponent {
2587
2693
  this.repeaterCfg = JSON.parse(JSON.stringify(cfg));
2588
2694
  this.ensureTextureApply(); // config legacy senza textureApply
2589
2695
  // Merge coi default: riempie i campi carousel/chart nuovi su config legacy.
2590
- this.repeaterCfg.carousel = { ...defaultCarouselConfig(), ...(this.repeaterCfg.carousel || {}) };
2591
- this.repeaterCfg.chart = { ...defaultChartConfig(), ...(this.repeaterCfg.chart || {}) };
2592
- this.repeaterCfg.surface = { ...defaultSurfaceConfig(), ...(this.repeaterCfg.surface || {}) };
2593
- this.repeaterCfg.gauge = { ...defaultGaugeConfig(), ...(this.repeaterCfg.gauge || {}) };
2594
- this.repeaterCfg.graph = { ...defaultGraphConfig(), ...(this.repeaterCfg.graph || {}) };
2595
- this.repeaterCfg.map = { ...defaultMapConfig(), ...(this.repeaterCfg.map || {}) };
2596
- this.repeaterCfg.warehouse = { ...defaultWarehouseConfig(), ...(this.repeaterCfg.warehouse || {}) };
2597
- this.repeaterCfg.kanban = { ...defaultKanbanConfig(), ...(this.repeaterCfg.kanban || {}) };
2598
- this.repeaterCfg.calendar = { ...defaultCalendarConfig(), ...(this.repeaterCfg.calendar || {}) };
2599
- this.repeaterCfg.stacked = { ...defaultStackedConfig(), ...(this.repeaterCfg.stacked || {}) };
2696
+ this.repeaterCfg.carousel = { ...defaultCarouselConfig(), ...(this.repeaterCfg.carousel) };
2697
+ this.repeaterCfg.chart = { ...defaultChartConfig(), ...(this.repeaterCfg.chart) };
2698
+ this.repeaterCfg.surface = { ...defaultSurfaceConfig(), ...(this.repeaterCfg.surface) };
2699
+ this.repeaterCfg.gauge = { ...defaultGaugeConfig(), ...(this.repeaterCfg.gauge) };
2700
+ this.repeaterCfg.graph = { ...defaultGraphConfig(), ...(this.repeaterCfg.graph) };
2701
+ this.repeaterCfg.map = { ...defaultMapConfig(), ...(this.repeaterCfg.map) };
2702
+ this.repeaterCfg.warehouse = { ...defaultWarehouseConfig(), ...(this.repeaterCfg.warehouse) };
2703
+ this.repeaterCfg.kanban = { ...defaultKanbanConfig(), ...(this.repeaterCfg.kanban) };
2704
+ this.repeaterCfg.calendar = { ...defaultCalendarConfig(), ...(this.repeaterCfg.calendar) };
2705
+ this.repeaterCfg.stacked = { ...defaultStackedConfig(), ...(this.repeaterCfg.stacked) };
2600
2706
  this.repeaterCount = (group.children || []).filter((c) => c.name === 'wuicRepeaterItem').length;
2601
2707
  // Tipi mesh fissi disponibili: primitive + custom da metadata.
2602
2708
  const primitives = ['box', 'sphere', 'plane', 'cylinder', 'cone', 'torus', 'text3d'];
@@ -3084,7 +3190,7 @@ class Scene3dDesignerComponent {
3084
3190
  }
3085
3191
  try {
3086
3192
  if (/^0x[0-9a-fA-F]{6}$/.test(str)) {
3087
- color.setHex(parseInt(str.slice(2), 16));
3193
+ color.setHex(Number.parseInt(str.slice(2), 16));
3088
3194
  }
3089
3195
  else if (/^#?[0-9a-fA-F]{6}$/.test(str) || /^#?[0-9a-fA-F]{3}$/.test(str)) {
3090
3196
  color.set(str.startsWith('#') ? str : `#${str}`);
@@ -3134,6 +3240,15 @@ class Scene3dDesignerComponent {
3134
3240
  return;
3135
3241
  }
3136
3242
  const picked = this.resolveTopLevel(hits[0].object);
3243
+ // Callback "click" di un item del repeater (se configurato): eseguilo, poi
3244
+ // prosegui comunque con la normale selezione del repeater (per editarlo).
3245
+ this.maybeRunItemCallback(hits[0].object, event, 'click');
3246
+ // In vista-camera il click e' "preview": il callback gira, ma NON si
3247
+ // seleziona nulla → niente gizmo sposta/ruota (l'editing avviene solo in
3248
+ // vista editor).
3249
+ if (this.viewingThroughCamera) {
3250
+ return;
3251
+ }
3137
3252
  // Click nel vuoto: NON deselezionare (mantieni la selezione corrente). Solo
3138
3253
  // il click su un ALTRO oggetto cambia la selezione. La deselezione avviene
3139
3254
  // via delete o "Nuova"/load.
@@ -3153,6 +3268,102 @@ class Scene3dDesignerComponent {
3153
3268
  }
3154
3269
  return current;
3155
3270
  }
3271
+ /**
3272
+ * Se l'oggetto raycastato appartiene a un item di un Mesh Repeater con un
3273
+ * callback JS per l'evento `kind`, lo esegue con `ctx` (record, mesh, wuic...).
3274
+ * Ritorna true se un callback e' stato eseguito.
3275
+ */
3276
+ maybeRunItemCallback(hit, event, kind) {
3277
+ // item = ancestor con nome WUIC_REPEATER_CHILD
3278
+ let item = hit;
3279
+ while (item && item.name !== WUIC_REPEATER_CHILD) {
3280
+ item = item.parent;
3281
+ }
3282
+ if (!item) {
3283
+ return false;
3284
+ }
3285
+ // repeater group = ancestor con la config
3286
+ let grp = item.parent;
3287
+ while (grp && !getMeshRepeaterConfig(grp)) {
3288
+ grp = grp.parent;
3289
+ }
3290
+ const cfg = grp ? getMeshRepeaterConfig(grp) : null;
3291
+ if (!cfg) {
3292
+ return false;
3293
+ }
3294
+ const dblOrRightJs = kind === 'dblclick' ? cfg.onDoubleClickJs : cfg.onRightClickJs;
3295
+ const js = kind === 'click' ? cfg.onClickJs : dblOrRightJs;
3296
+ if (!js || !js.trim()) {
3297
+ return false;
3298
+ }
3299
+ const record = (item.userData?.[WUIC_REPEATER_RECORD]) || {};
3300
+ const ctx = this.buildRepeaterCallbackCtx(record, event, item);
3301
+ this.zone.run(() => runRepeaterCallback(js, ctx));
3302
+ return true;
3303
+ }
3304
+ /** Costruisce il `ctx` iniettato nei callback del repeater (con l'helper `wuic`). */
3305
+ buildRepeaterCallbackCtx(record, event, mesh) {
3306
+ const activeCamera = (this.viewingThroughCamera && this.userViewCamera) ? this.userViewCamera : this.camera;
3307
+ return {
3308
+ record, event, mesh,
3309
+ scene: this.scene, camera: activeCamera, THREE,
3310
+ wuic: {
3311
+ showItemPopup: (opts = {}) => showItemPopup({ record, mesh, ...opts }, {
3312
+ scene: this.scene, camera: activeCamera, canvas: this.renderer.domElement,
3313
+ onFrame: (cb) => { this.popupFrameHook = cb; }
3314
+ }),
3315
+ closePopup: () => { this.popupFrameHook = null; closeItemPopup(); }
3316
+ }
3317
+ };
3318
+ }
3319
+ /** true se il callback dell'evento `kind` e' valorizzato (per il badge UI). */
3320
+ repeaterCallbackIsSet(kind) {
3321
+ const dblOrRightJs = kind === 'dblclick' ? this.repeaterCfg.onDoubleClickJs : this.repeaterCfg.onRightClickJs;
3322
+ const js = kind === 'click' ? this.repeaterCfg.onClickJs : dblOrRightJs;
3323
+ return !!(js?.trim());
3324
+ }
3325
+ /**
3326
+ * Apre l'editor Monaco (promptDialog `code_editor`, textarea->toggle codice) per
3327
+ * il callback JS dell'evento `kind` del repeater selezionato e salva il risultato.
3328
+ */
3329
+ async editRepeaterCallback(kind) {
3330
+ const dblOrRightCur = kind === 'dblclick' ? this.repeaterCfg.onDoubleClickJs : this.repeaterCfg.onRightClickJs;
3331
+ const cur = kind === 'click' ? this.repeaterCfg.onClickJs : dblOrRightCur;
3332
+ let title;
3333
+ if (kind === 'click') {
3334
+ title = this.t('scene3d.repeater.cb_click_title', 'Callback CLICK item');
3335
+ }
3336
+ else if (kind === 'dblclick') {
3337
+ title = this.t('scene3d.repeater.cb_dblclick_title', 'Callback DOPPIO-CLICK item');
3338
+ }
3339
+ else {
3340
+ title = this.t('scene3d.repeater.cb_rightclick_title', 'Callback TASTO-DESTRO item');
3341
+ }
3342
+ const resp = await WtoolboxService.promptDialog(title, [{
3343
+ name: 'cb_js',
3344
+ caption: this.t('scene3d.repeater.cb_caption', 'Corpo JS — ctx: record, mesh, event, scene, camera, THREE, wuic'),
3345
+ type: 'code_editor',
3346
+ value: String(cur || ''),
3347
+ required: false,
3348
+ tooltip: this.t('scene3d.repeater.cb_tooltip', 'Es.: wuic.showItemPopup({ mode:1, fields:["StockItemName","UnitPrice"], title: record.StockItemName })'),
3349
+ propsBag: { customEditorConfig: { editorOptions: { language: 'javascript' }, startInCodeView: true } }
3350
+ }], '820px', '600px');
3351
+ if (!resp) {
3352
+ return;
3353
+ }
3354
+ const val = String(resp?.cb_js?.value ?? '');
3355
+ if (kind === 'click') {
3356
+ this.repeaterCfg.onClickJs = val;
3357
+ }
3358
+ else if (kind === 'dblclick') {
3359
+ this.repeaterCfg.onDoubleClickJs = val;
3360
+ }
3361
+ else {
3362
+ this.repeaterCfg.onRightClickJs = val;
3363
+ }
3364
+ this.onRepeaterConfigChange();
3365
+ this.cdr.markForCheck();
3366
+ }
3156
3367
  selectObject(object) {
3157
3368
  // Buffer di registrazione keyframe legato all'oggetto: reset se cambia target.
3158
3369
  if (this.animRecTargetUuid !== object.uuid) {
@@ -3450,7 +3661,7 @@ class Scene3dDesignerComponent {
3450
3661
  /** Rimuove il binding dall'oggetto selezionato (route sganciata). */
3451
3662
  removeSelectedBinding() {
3452
3663
  const obj = this.selectedObject;
3453
- if (obj && obj.userData && obj.userData[WUIC_BINDING_KEY]) {
3664
+ if (obj?.userData?.[WUIC_BINDING_KEY]) {
3454
3665
  delete obj.userData[WUIC_BINDING_KEY];
3455
3666
  }
3456
3667
  }
@@ -3498,6 +3709,11 @@ class Scene3dDesignerComponent {
3498
3709
  this.pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
3499
3710
  this.raycaster.setFromCamera(this.pointer, this.camera);
3500
3711
  const hits = this.raycaster.intersectObjects(this.contentGroup.children, true);
3712
+ // Callback "dblclick" di un item del repeater: se presente sovrascrive
3713
+ // l'apertura CRUD di default.
3714
+ if (hits.length > 0 && this.maybeRunItemCallback(hits[0].object, event, 'dblclick')) {
3715
+ return;
3716
+ }
3501
3717
  const picked = hits.length > 0 ? this.resolveTopLevel(hits[0].object) : undefined;
3502
3718
  const binding = picked?.userData?.[WUIC_BINDING_KEY];
3503
3719
  if (!picked || !binding || !String(binding.route || '').trim()) {
@@ -3509,6 +3725,22 @@ class Scene3dDesignerComponent {
3509
3725
  this.cdr.markForCheck();
3510
3726
  });
3511
3727
  }
3728
+ /** Right-click sul canvas: se colpisce un item con `onRightClickJs`, esegue il
3729
+ * callback e sopprime il menu contestuale del browser. */
3730
+ handleRightClick(event) {
3731
+ if (!this.renderer || !this.camera || !this.contentGroup) {
3732
+ return;
3733
+ }
3734
+ const rect = this.renderer.domElement.getBoundingClientRect();
3735
+ this.pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
3736
+ this.pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
3737
+ const activeCamera = (this.viewingThroughCamera && this.userViewCamera) ? this.userViewCamera : this.camera;
3738
+ this.raycaster.setFromCamera(this.pointer, activeCamera);
3739
+ const hits = this.raycaster.intersectObjects(this.contentGroup.children, true);
3740
+ if (hits.length > 0 && this.maybeRunItemCallback(hits[0].object, event, 'rightclick')) {
3741
+ event.preventDefault();
3742
+ }
3743
+ }
3512
3744
  // ===========================================================================
3513
3745
  // Iter2 F2: animazioni / timeline (AnimationMixer + transport nel panel)
3514
3746
  // ===========================================================================
@@ -3716,7 +3948,7 @@ class Scene3dDesignerComponent {
3716
3948
  get sceneObjects() {
3717
3949
  const children = this.contentGroup?.children || [];
3718
3950
  return children
3719
- .filter((c) => !(c.userData && c.userData[WUIC_TRANSIENT_FLAG] === true))
3951
+ .filter((c) => c.userData?.[WUIC_TRANSIENT_FLAG] !== true)
3720
3952
  .map((c, i) => ({
3721
3953
  uuid: c.uuid,
3722
3954
  label: c.name || `${c.type} ${i + 1}`
@@ -3725,10 +3957,18 @@ class Scene3dDesignerComponent {
3725
3957
  /** Click su un chip: seleziona l'oggetto corrispondente (gizmo inclusi). */
3726
3958
  selectFromFooter(uuid) {
3727
3959
  const obj = (this.contentGroup?.children || []).find((c) => c.uuid === uuid);
3728
- if (obj) {
3960
+ if (!obj) {
3961
+ return;
3962
+ }
3963
+ // Toggle: ri-cliccare il chip dell'oggetto GIA' selezionato lo deseleziona
3964
+ // (utile per nascondere il gizmo e vedere la scena pulita).
3965
+ if (this.selectedObject === obj) {
3966
+ this.clearSelection();
3967
+ }
3968
+ else {
3729
3969
  this.selectObject(obj);
3730
- this.cdr.markForCheck();
3731
3970
  }
3971
+ this.cdr.markForCheck();
3732
3972
  }
3733
3973
  /** Right-click su un chip: context menu con Rimuovi (+ azioni gruppo se target Group). */
3734
3974
  showFooterContextMenu(event, uuid, menu) {
@@ -3743,7 +3983,7 @@ class Scene3dDesignerComponent {
3743
3983
  const canAdd = !!selected
3744
3984
  && selected.uuid !== uuid
3745
3985
  && selected.parent === this.contentGroup; // solo top-level, niente nesting ricorsivo per ora
3746
- const groupChildren = target.children.filter((c) => !(c.userData && c.userData[WUIC_TRANSIENT_FLAG] === true));
3986
+ const groupChildren = target.children.filter((c) => c.userData?.[WUIC_TRANSIENT_FLAG] !== true);
3747
3987
  items.push({
3748
3988
  label: this.t('scene3d.group.add_selected', 'Aggiungi selezionato al gruppo'),
3749
3989
  icon: 'pi pi-sign-in',
@@ -3792,7 +4032,7 @@ class Scene3dDesignerComponent {
3792
4032
  if (!group || !group.isGroup || !this.contentGroup) {
3793
4033
  return;
3794
4034
  }
3795
- const children = group.children.filter((c) => !(c.userData && c.userData[WUIC_TRANSIENT_FLAG] === true));
4035
+ const children = group.children.filter((c) => c.userData?.[WUIC_TRANSIENT_FLAG] !== true);
3796
4036
  for (const child of children) {
3797
4037
  this.contentGroup.attach(child);
3798
4038
  }
@@ -3929,7 +4169,7 @@ class Scene3dDesignerComponent {
3929
4169
  }
3930
4170
  this.syncLightHelpers();
3931
4171
  if (injected.length) {
3932
- this.selectObject(injected[injected.length - 1]);
4172
+ this.selectObject(injected.at(-1));
3933
4173
  }
3934
4174
  this.markDirty();
3935
4175
  this.cdr.markForCheck();
@@ -3987,7 +4227,7 @@ class Scene3dDesignerComponent {
3987
4227
  }
3988
4228
  const fresh = makeTextSpriteMesh(String(value ?? ''));
3989
4229
  mesh.geometry?.dispose();
3990
- mesh.geometry = fresh.geometry;
4230
+ mesh.geometry = (fresh).geometry;
3991
4231
  break;
3992
4232
  }
3993
4233
  case 'visible': {
@@ -4049,7 +4289,7 @@ class Scene3dDesignerComponent {
4049
4289
  return "target_name mancante";
4050
4290
  }
4051
4291
  const children = (this.contentGroup?.children || [])
4052
- .filter((c) => !(c.userData && c.userData[WUIC_TRANSIENT_FLAG] === true));
4292
+ .filter((c) => c.userData?.[WUIC_TRANSIENT_FLAG] !== true);
4053
4293
  // 1) nome esatto (case-insensitive come tie-break).
4054
4294
  const byName = children.filter((c) => (c.name || '') === name);
4055
4295
  if (byName.length === 1) {
@@ -4078,7 +4318,7 @@ class Scene3dDesignerComponent {
4078
4318
  return `'${name}' e' ambiguo (candidati: ${amb}): usa il nome completo o lo uuid breve tra []`;
4079
4319
  }
4080
4320
  // 3) prefisso uuid (dallo state dump `[a1b2c3d4]`).
4081
- const byUuid = children.filter((c) => c.uuid.replace(/-/g, '').toLowerCase().startsWith(name.replace(/-/g, '').toLowerCase()));
4321
+ const byUuid = children.filter((c) => c.uuid.replaceAll('-', '').toLowerCase().startsWith(name.replaceAll('-', '').toLowerCase()));
4082
4322
  if (byUuid.length === 1) {
4083
4323
  return byUuid[0];
4084
4324
  }
@@ -4109,7 +4349,7 @@ class Scene3dDesignerComponent {
4109
4349
  /** Applica/aggiorna `userData.wuicBinding` da uno spec {route, recordId?, fieldMap?}. */
4110
4350
  applyScene3dBindingSpec(object, spec) {
4111
4351
  if (spec == null || spec === '' || (typeof spec === 'object' && !String(spec.route || '').trim())) {
4112
- if (object.userData && object.userData[WUIC_BINDING_KEY]) {
4352
+ if (object.userData?.[WUIC_BINDING_KEY]) {
4113
4353
  delete object.userData[WUIC_BINDING_KEY];
4114
4354
  }
4115
4355
  return;
@@ -4129,7 +4369,7 @@ class Scene3dDesignerComponent {
4129
4369
  mergeScene3dRepeaterConfig(base, patch) {
4130
4370
  const out = JSON.parse(JSON.stringify(base));
4131
4371
  for (const key of Object.keys(patch || {})) {
4132
- const v = patch[key];
4372
+ const v = (patch)[key];
4133
4373
  if (v && typeof v === 'object' && !Array.isArray(v) && out[key] && typeof out[key] === 'object' && !Array.isArray(out[key])) {
4134
4374
  Object.assign(out[key], v);
4135
4375
  }
@@ -4163,7 +4403,7 @@ class Scene3dDesignerComponent {
4163
4403
  buildCompactSceneStateDump() {
4164
4404
  const lines = [];
4165
4405
  const children = (this.contentGroup?.children || [])
4166
- .filter((c) => !(c.userData && c.userData[WUIC_TRANSIENT_FLAG] === true));
4406
+ .filter((c) => c.userData?.[WUIC_TRANSIENT_FLAG] !== true);
4167
4407
  const MAX_NODES = 60;
4168
4408
  for (const obj of children.slice(0, MAX_NODES)) {
4169
4409
  const details = [];
@@ -4197,7 +4437,7 @@ class Scene3dDesignerComponent {
4197
4437
  if (!obj.visible) {
4198
4438
  details.push('hidden');
4199
4439
  }
4200
- lines.push(`${obj.name || obj.type} [${obj.uuid.replace(/-/g, '').slice(0, 8)}] (${details.join(', ')})`);
4440
+ lines.push(`${obj.name || obj.type} [${obj.uuid.replaceAll('-', '').slice(0, 8)}] (${details.join(', ')})`);
4201
4441
  }
4202
4442
  if (children.length > MAX_NODES) {
4203
4443
  lines.push(`... (${children.length - MAX_NODES} altri oggetti non mostrati)`);
@@ -4446,12 +4686,7 @@ class Scene3dDesignerComponent {
4446
4686
  * Mirror di DesignerComponent.confirmNavigationWithPendingChanges.
4447
4687
  */
4448
4688
  async confirmNavigationWithPendingChanges() {
4449
- if (!this.sceneDirty) {
4450
- return true;
4451
- }
4452
- return await WtoolboxService.confirm({
4453
- message: this.t('scene3d.discard_confirm', 'Ci sono modifiche non salvate. Continuare e scartarle?')
4454
- });
4689
+ return this.confirmDiscardIfDirty();
4455
4690
  }
4456
4691
  // ===========================================================================
4457
4692
  // Helpers
@@ -4494,7 +4729,7 @@ class Scene3dDesignerComponent {
4494
4729
  return group;
4495
4730
  }
4496
4731
  generateSceneKey() {
4497
- const stamp = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14);
4732
+ const stamp = new Date().toISOString().replaceAll(/[-:.TZ]/g, '').slice(0, 14);
4498
4733
  const rand = Math.random().toString(36).slice(2, 8);
4499
4734
  return `s3_${stamp}_${rand}`;
4500
4735
  }
@@ -4519,8 +4754,8 @@ class Scene3dDesignerComponent {
4519
4754
  return value;
4520
4755
  }
4521
4756
  }
4522
- if (value && typeof value === 'object' && 'value' in value && typeof value.subscribe === 'function') {
4523
- return value.value;
4757
+ if (value && typeof value === 'object' && 'value' in value && typeof (value).subscribe === 'function') {
4758
+ return (value).value;
4524
4759
  }
4525
4760
  return value;
4526
4761
  }
@@ -4550,17 +4785,17 @@ class Scene3dDesignerComponent {
4550
4785
  /** Traduzione formattata con placeholder `{0}`, `{1}`, ... */
4551
4786
  tf(resource, fallback, ...args) {
4552
4787
  const template = this.t(resource, fallback);
4553
- return template.replace(/\{(\d+)\}/g, (match, idx) => {
4788
+ return template.replaceAll(/\{(\d+)\}/g, (match, idx) => {
4554
4789
  const value = args[Number(idx)];
4555
4790
  return value !== undefined && value !== null ? String(value) : match;
4556
4791
  });
4557
4792
  }
4558
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 });
4559
- 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 <!-- 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\">{{ t('scene3d.props.name', 'Nome') }}</label>\n <input 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 <label class=\"scene3d-prop-label\">{{ t('scene3d.props.camera_id', 'ID camera (viewer)') }}</label>\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\">{{ t('scene3d.props.position', 'Posizione') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.props.rotation', 'Rotazione (deg)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.props.scale', 'Scala') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.props.color', 'Colore') }}</label>\n <input 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\">{{ t('scene3d.material.metalness', 'Metalness') }} ({{ propMaterial.metalness }})</label>\n <input 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\">{{ t('scene3d.material.roughness', 'Roughness') }} ({{ propMaterial.roughness }})</label>\n <input 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\">{{ t('scene3d.material.opacity', 'Opacita\\'') }} ({{ propMaterial.opacity }})</label>\n <input 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\">{{ t('scene3d.material.emissive', 'Emissivo') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.material.side', 'Facce') }}</label>\n <select 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\">{{ t('scene3d.material.transmission', 'Trasmissione') }} ({{ propMaterial.transmission }})</label>\n <input 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\">{{ t('scene3d.material.ior', 'IOR') }} ({{ propMaterial.ior }})</label>\n <input 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\">{{ t('scene3d.material.thickness', 'Spessore') }} ({{ propMaterial.thickness }})</label>\n <input 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\">{{ t('scene3d.material.attenuation_color', 'Colore assorbimento') }}</label>\n <div class=\"scene3d-vector-row\">\n <input type=\"color\" [(ngModel)]=\"propMaterial.attenuationColor\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\">{{ t('scene3d.material.attenuation_distance', 'Distanza assorbimento') }} ({{ propMaterial.attenuationDistance }})</label>\n <input 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\">{{ t('scene3d.shader.vertex', 'Vertex shader') }}</label>\n <textarea 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\">{{ t('scene3d.shader.fragment', 'Fragment shader') }}</label>\n <textarea 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\">{{ t('scene3d.binding.route', 'Rotta') }}</label>\n <p-select\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\">{{ t('scene3d.binding.record_id', 'Record ID (opzionale)') }}</label>\n <input\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\">{{ t('scene3d.binding.map_label', 'Etichetta') }}</label>\n <p-select\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\">{{ t('scene3d.binding.map_color', 'Colore') }}</label>\n <p-select\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\">{{ t('scene3d.binding.map_visible', 'Visibile') }}</label>\n <p-select\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\">{{ t('scene3d.anim.clip', 'Clip') }}</label>\n <p-select\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\">{{ t('scene3d.anim.time', 'Tempo (s)') }}</label>\n <input\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\">{{ t('scene3d.repeater.route', 'Rotta') }}</label>\n <p-select\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\">{{ t('scene3d.repeater.max', 'Max record') }}</label>\n <input 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\">{{ t('scene3d.repeater.mesh_source', 'Sorgente mesh') }}</label>\n <select 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 </select>\n </div>\n @if (repeaterCfg.meshSource.mode === 'fixed') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\">{{ t('scene3d.repeater.mesh_type', 'Tipo mesh') }}</label>\n <select 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\">{{ t('scene3d.repeater.mesh_column', 'Colonna tipo') }}</label>\n <p-select [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\">{{ f.label }}</label>\n <p-select [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\">{{ t('scene3d.repeater.texture_source', 'Sorgente') }}</label>\n <select 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\">{{ t('scene3d.repeater.texture_apply', 'Applicazione') }}</label>\n <select 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\">{{ t('scene3d.repeater.texture_repeat_xy', 'Ripetizioni X / Y') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.repeater.texture_text_colors', 'Colori testo (sfondo / testo)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.repeater.layout', 'Layout (senza colonne posizione)') }}</label>\n <select 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\">{{ t('scene3d.repeater.carousel_visible', 'Item visibili') }} ({{ carouselCfg.visible }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_radius', 'Curvatura (raggio)') }} ({{ carouselCfg.radius }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_slot_angle', 'Angolo tra slot') }} ({{ carouselCfg.slotAngleDeg }}\u00B0)</label>\n <input 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\">{{ t('scene3d.repeater.carousel_scales', 'Scala centro / lati') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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 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\">{{ t('scene3d.repeater.carousel_disc_size', 'Dimensione disco') }} ({{ carouselCfg.baseSize }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_base_color', 'Colore base') }}</label>\n <div class=\"scene3d-vector-row\">\n <input type=\"color\" [(ngModel)]=\"carouselCfg.baseColor\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\">{{ t('scene3d.repeater.carousel_metalness', 'Metalness') }} ({{ carouselCfg.baseMetalness }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_roughness', 'Roughness') }} ({{ carouselCfg.baseRoughness }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_clearcoat', 'Clearcoat') }} ({{ carouselCfg.baseClearcoat }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_arrow_size', 'Dimensione frecce') }} ({{ carouselCfg.arrowSize }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_arrow_color', 'Colore frecce') }}</label>\n <div class=\"scene3d-vector-row\">\n <input type=\"color\" [(ngModel)]=\"carouselCfg.arrowColor\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\">{{ t('scene3d.repeater.carousel_arrow_glow', 'Bagliore (emissive)') }} ({{ carouselCfg.arrowEmissive }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_metalness', 'Metalness') }} ({{ carouselCfg.arrowMetalness }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_roughness', 'Roughness') }} ({{ carouselCfg.arrowRoughness }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_clearcoat', 'Clearcoat') }} ({{ carouselCfg.arrowClearcoat }})</label>\n <input 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\">{{ t('scene3d.repeater.chart_type', 'Tipo chart') }}</label>\n <select 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\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_x', 'Colonna X') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_y', 'Colonna Y (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_z', 'Colonna Z (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_size', 'Colonna dimensione (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_color_col', 'Colonna colore (opz.)') }}</label>\n <p-select [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\">{{ chartCfg.type === 'scatter' ? t('scene3d.repeater.chart_span', 'Estensione') : t('scene3d.repeater.chart_height', 'Altezza') }} ({{ chartCfg.height }})</label>\n <input 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\">{{ t('scene3d.repeater.chart_spacing', 'Passo') }} ({{ chartCfg.spacing }})</label>\n <input 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\">{{ chartCfg.type === 'scatter' ? t('scene3d.repeater.chart_bubble', 'Raggio bolla') : t('scene3d.repeater.chart_bar_radius', 'Raggio barra') }} ({{ chartCfg.barRadius }})</label>\n <input 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\">{{ t('scene3d.repeater.chart_radius', 'Raggio') }} ({{ chartCfg.radius }})</label>\n <input 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\">{{ chartCfg.type === 'pie' ? t('scene3d.repeater.chart_tube', 'Spessore donut') : t('scene3d.repeater.chart_depth', 'Profondita\\' area') }} ({{ chartCfg.thickness }})</label>\n <input 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\">{{ t('scene3d.repeater.chart_label_col', 'Colonna etichetta (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.surf_row', 'Colonna riga') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.surf_col', 'Colonna colonna') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.surf_height', 'Scala altezza') }} ({{ surfaceCfg.heightScale }})</label>\n <input 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\">{{ t('scene3d.repeater.surf_cell', 'Dimensione cella') }} ({{ surfaceCfg.cellSize }})</label>\n <input 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\">{{ t('scene3d.repeater.surf_colors', 'Colori (basso / alto)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.gauge_label', 'Colonna etichetta (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.gauge_range', 'Min / Max') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.repeater.chart_radius', 'Raggio') }} ({{ gaugeCfg.radius }})</label>\n <input 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\">{{ t('scene3d.repeater.graph_id', 'Colonna ID') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.graph_parent', 'Colonna padre') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.graph_label', 'Colonna etichetta') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.graph_value', 'Colonna valore (dim. nodo)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.graph_nodesize', 'Dimensione nodo') }} ({{ graphCfg.nodeSize }})</label>\n <input 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\">{{ t('scene3d.repeater.graph_spacing', 'Spaziatura') }} ({{ graphCfg.spacing }})</label>\n <input 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\">{{ t('scene3d.repeater.graph_linkcolor', 'Colore archi') }}</label>\n <div class=\"scene3d-vector-row\"><input 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\">{{ t('scene3d.repeater.map_lat', 'Colonna latitudine') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.map_lon', 'Colonna longitudine') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.map_label', 'Colonna etichetta (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.map_size', 'Dimensione mappa') }} ({{ mapCfg.planeSize }})</label>\n <input 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\">{{ t('scene3d.repeater.map_pincolor', 'Colore pin (senza valore)') }}</label>\n <div class=\"scene3d-vector-row\"><input 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\">{{ t('scene3d.repeater.wh_aisle', 'Colonna corsia') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.wh_shelf', 'Colonna ripiano') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.wh_qty', 'Colonna quantit\u00E0') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.wh_label', 'Colonna articolo (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.wh_box', 'Dimensione scatola') }} ({{ warehouseCfg.boxSize }})</label>\n <input 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\">{{ t('scene3d.repeater.kb_status', 'Colonna stato') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.kb_label', 'Colonna titolo (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.kb_card', 'Larghezza card') }} ({{ kanbanCfg.cardWidth }})</label>\n <input 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\">{{ t('scene3d.repeater.cal_date', 'Colonna data') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.surf_height', 'Scala altezza') }} ({{ calendarCfg.heightScale }})</label>\n <input 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\">{{ t('scene3d.repeater.surf_cell', 'Dimensione cella') }} ({{ calendarCfg.cellSize }})</label>\n <input 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\">{{ t('scene3d.repeater.st_category', 'Colonna categoria') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.st_series', 'Colonna serie') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_height', 'Altezza') }} ({{ stackedCfg.height }})</label>\n <input 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\">{{ t('scene3d.repeater.chart_bar_radius', 'Larghezza barra') }} ({{ stackedCfg.barWidth }})</label>\n <input 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\">{{ t('scene3d.repeater.order', 'Ordine di flusso') }}</label>\n <select 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\">{{ axisDirLabel(ax) }}</label>\n <select class=\"scene3d-material-select\" [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\">{{ t('scene3d.repeater.pos_range', 'Range posizione') }} ({{ repeaterCfg.normalize.posRange }})</label>\n <input 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\">{{ t('scene3d.repeater.scale_range', 'Scala min/max') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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 </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\">{{ t('scene3d.physics.body', 'Corpo') }}</label>\n <select 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\">{{ t('scene3d.physics.shape', 'Forma collider') }}</label>\n <select 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\">{{ t('scene3d.physics.mass', 'Massa (kg)') }}</label>\n <input 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\">{{ t('scene3d.physics.friction', 'Attrito') }} ({{ propPhysics.friction }})</label>\n <input 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\">{{ t('scene3d.physics.restitution', 'Rimbalzo') }} ({{ propPhysics.restitution }})</label>\n <input 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-shader-toggle\" (click)=\"animRecSectionOpen = !animRecSectionOpen\">\n {{ t('scene3d.animrec.title', 'Registra animazione') }} {{ animRecSectionOpen ? '\u25BE' : '\u25B8' }}\n </div>\n @if (animRecSectionOpen) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\">{{ t('scene3d.animrec.clip_name', 'Nome clip') }}</label>\n <input 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\">{{ t('scene3d.animrec.time', 'Tempo T (s)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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: i6.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: i10.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 +\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 });
4560
4795
  }
4561
4796
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: Scene3dDesignerComponent, decorators: [{
4562
4797
  type: Component,
4563
- 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 <!-- 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\">{{ t('scene3d.props.name', 'Nome') }}</label>\n <input 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 <label class=\"scene3d-prop-label\">{{ t('scene3d.props.camera_id', 'ID camera (viewer)') }}</label>\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\">{{ t('scene3d.props.position', 'Posizione') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.props.rotation', 'Rotazione (deg)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.props.scale', 'Scala') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.props.color', 'Colore') }}</label>\n <input 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\">{{ t('scene3d.material.metalness', 'Metalness') }} ({{ propMaterial.metalness }})</label>\n <input 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\">{{ t('scene3d.material.roughness', 'Roughness') }} ({{ propMaterial.roughness }})</label>\n <input 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\">{{ t('scene3d.material.opacity', 'Opacita\\'') }} ({{ propMaterial.opacity }})</label>\n <input 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\">{{ t('scene3d.material.emissive', 'Emissivo') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.material.side', 'Facce') }}</label>\n <select 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\">{{ t('scene3d.material.transmission', 'Trasmissione') }} ({{ propMaterial.transmission }})</label>\n <input 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\">{{ t('scene3d.material.ior', 'IOR') }} ({{ propMaterial.ior }})</label>\n <input 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\">{{ t('scene3d.material.thickness', 'Spessore') }} ({{ propMaterial.thickness }})</label>\n <input 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\">{{ t('scene3d.material.attenuation_color', 'Colore assorbimento') }}</label>\n <div class=\"scene3d-vector-row\">\n <input type=\"color\" [(ngModel)]=\"propMaterial.attenuationColor\" (ngModelChange)=\"onMaterialInputChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\">{{ t('scene3d.material.attenuation_distance', 'Distanza assorbimento') }} ({{ propMaterial.attenuationDistance }})</label>\n <input 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\">{{ t('scene3d.shader.vertex', 'Vertex shader') }}</label>\n <textarea 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\">{{ t('scene3d.shader.fragment', 'Fragment shader') }}</label>\n <textarea 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\">{{ t('scene3d.binding.route', 'Rotta') }}</label>\n <p-select\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\">{{ t('scene3d.binding.record_id', 'Record ID (opzionale)') }}</label>\n <input\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\">{{ t('scene3d.binding.map_label', 'Etichetta') }}</label>\n <p-select\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\">{{ t('scene3d.binding.map_color', 'Colore') }}</label>\n <p-select\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\">{{ t('scene3d.binding.map_visible', 'Visibile') }}</label>\n <p-select\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\">{{ t('scene3d.anim.clip', 'Clip') }}</label>\n <p-select\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\">{{ t('scene3d.anim.time', 'Tempo (s)') }}</label>\n <input\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\">{{ t('scene3d.repeater.route', 'Rotta') }}</label>\n <p-select\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\">{{ t('scene3d.repeater.max', 'Max record') }}</label>\n <input 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\">{{ t('scene3d.repeater.mesh_source', 'Sorgente mesh') }}</label>\n <select 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 </select>\n </div>\n @if (repeaterCfg.meshSource.mode === 'fixed') {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\">{{ t('scene3d.repeater.mesh_type', 'Tipo mesh') }}</label>\n <select 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\">{{ t('scene3d.repeater.mesh_column', 'Colonna tipo') }}</label>\n <p-select [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\">{{ f.label }}</label>\n <p-select [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\">{{ t('scene3d.repeater.texture_source', 'Sorgente') }}</label>\n <select 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\">{{ t('scene3d.repeater.texture_apply', 'Applicazione') }}</label>\n <select 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\">{{ t('scene3d.repeater.texture_repeat_xy', 'Ripetizioni X / Y') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.repeater.texture_text_colors', 'Colori testo (sfondo / testo)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.repeater.layout', 'Layout (senza colonne posizione)') }}</label>\n <select 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\">{{ t('scene3d.repeater.carousel_visible', 'Item visibili') }} ({{ carouselCfg.visible }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_radius', 'Curvatura (raggio)') }} ({{ carouselCfg.radius }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_slot_angle', 'Angolo tra slot') }} ({{ carouselCfg.slotAngleDeg }}\u00B0)</label>\n <input 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\">{{ t('scene3d.repeater.carousel_scales', 'Scala centro / lati') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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 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\">{{ t('scene3d.repeater.carousel_disc_size', 'Dimensione disco') }} ({{ carouselCfg.baseSize }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_base_color', 'Colore base') }}</label>\n <div class=\"scene3d-vector-row\">\n <input type=\"color\" [(ngModel)]=\"carouselCfg.baseColor\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\">{{ t('scene3d.repeater.carousel_metalness', 'Metalness') }} ({{ carouselCfg.baseMetalness }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_roughness', 'Roughness') }} ({{ carouselCfg.baseRoughness }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_clearcoat', 'Clearcoat') }} ({{ carouselCfg.baseClearcoat }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_arrow_size', 'Dimensione frecce') }} ({{ carouselCfg.arrowSize }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_arrow_color', 'Colore frecce') }}</label>\n <div class=\"scene3d-vector-row\">\n <input type=\"color\" [(ngModel)]=\"carouselCfg.arrowColor\" (ngModelChange)=\"onRepeaterConfigChange()\" />\n </div>\n </div>\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\">{{ t('scene3d.repeater.carousel_arrow_glow', 'Bagliore (emissive)') }} ({{ carouselCfg.arrowEmissive }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_metalness', 'Metalness') }} ({{ carouselCfg.arrowMetalness }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_roughness', 'Roughness') }} ({{ carouselCfg.arrowRoughness }})</label>\n <input 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\">{{ t('scene3d.repeater.carousel_clearcoat', 'Clearcoat') }} ({{ carouselCfg.arrowClearcoat }})</label>\n <input 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\">{{ t('scene3d.repeater.chart_type', 'Tipo chart') }}</label>\n <select 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\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_x', 'Colonna X') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_y', 'Colonna Y (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_z', 'Colonna Z (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_size', 'Colonna dimensione (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_color_col', 'Colonna colore (opz.)') }}</label>\n <p-select [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\">{{ chartCfg.type === 'scatter' ? t('scene3d.repeater.chart_span', 'Estensione') : t('scene3d.repeater.chart_height', 'Altezza') }} ({{ chartCfg.height }})</label>\n <input 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\">{{ t('scene3d.repeater.chart_spacing', 'Passo') }} ({{ chartCfg.spacing }})</label>\n <input 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\">{{ chartCfg.type === 'scatter' ? t('scene3d.repeater.chart_bubble', 'Raggio bolla') : t('scene3d.repeater.chart_bar_radius', 'Raggio barra') }} ({{ chartCfg.barRadius }})</label>\n <input 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\">{{ t('scene3d.repeater.chart_radius', 'Raggio') }} ({{ chartCfg.radius }})</label>\n <input 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\">{{ chartCfg.type === 'pie' ? t('scene3d.repeater.chart_tube', 'Spessore donut') : t('scene3d.repeater.chart_depth', 'Profondita\\' area') }} ({{ chartCfg.thickness }})</label>\n <input 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\">{{ t('scene3d.repeater.chart_label_col', 'Colonna etichetta (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.surf_row', 'Colonna riga') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.surf_col', 'Colonna colonna') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.surf_height', 'Scala altezza') }} ({{ surfaceCfg.heightScale }})</label>\n <input 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\">{{ t('scene3d.repeater.surf_cell', 'Dimensione cella') }} ({{ surfaceCfg.cellSize }})</label>\n <input 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\">{{ t('scene3d.repeater.surf_colors', 'Colori (basso / alto)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.gauge_label', 'Colonna etichetta (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.gauge_range', 'Min / Max') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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\">{{ t('scene3d.repeater.chart_radius', 'Raggio') }} ({{ gaugeCfg.radius }})</label>\n <input 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\">{{ t('scene3d.repeater.graph_id', 'Colonna ID') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.graph_parent', 'Colonna padre') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.graph_label', 'Colonna etichetta') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.graph_value', 'Colonna valore (dim. nodo)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.graph_nodesize', 'Dimensione nodo') }} ({{ graphCfg.nodeSize }})</label>\n <input 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\">{{ t('scene3d.repeater.graph_spacing', 'Spaziatura') }} ({{ graphCfg.spacing }})</label>\n <input 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\">{{ t('scene3d.repeater.graph_linkcolor', 'Colore archi') }}</label>\n <div class=\"scene3d-vector-row\"><input 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\">{{ t('scene3d.repeater.map_lat', 'Colonna latitudine') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.map_lon', 'Colonna longitudine') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.map_label', 'Colonna etichetta (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.map_size', 'Dimensione mappa') }} ({{ mapCfg.planeSize }})</label>\n <input 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\">{{ t('scene3d.repeater.map_pincolor', 'Colore pin (senza valore)') }}</label>\n <div class=\"scene3d-vector-row\"><input 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\">{{ t('scene3d.repeater.wh_aisle', 'Colonna corsia') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.wh_shelf', 'Colonna ripiano') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.wh_qty', 'Colonna quantit\u00E0') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.wh_label', 'Colonna articolo (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.wh_box', 'Dimensione scatola') }} ({{ warehouseCfg.boxSize }})</label>\n <input 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\">{{ t('scene3d.repeater.kb_status', 'Colonna stato') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.kb_label', 'Colonna titolo (opz.)') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.kb_card', 'Larghezza card') }} ({{ kanbanCfg.cardWidth }})</label>\n <input 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\">{{ t('scene3d.repeater.cal_date', 'Colonna data') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.surf_height', 'Scala altezza') }} ({{ calendarCfg.heightScale }})</label>\n <input 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\">{{ t('scene3d.repeater.surf_cell', 'Dimensione cella') }} ({{ calendarCfg.cellSize }})</label>\n <input 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\">{{ t('scene3d.repeater.st_category', 'Colonna categoria') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.st_series', 'Colonna serie') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_value', 'Colonna valore') }}</label>\n <p-select [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\">{{ t('scene3d.repeater.chart_height', 'Altezza') }} ({{ stackedCfg.height }})</label>\n <input 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\">{{ t('scene3d.repeater.chart_bar_radius', 'Larghezza barra') }} ({{ stackedCfg.barWidth }})</label>\n <input 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\">{{ t('scene3d.repeater.order', 'Ordine di flusso') }}</label>\n <select 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\">{{ axisDirLabel(ax) }}</label>\n <select class=\"scene3d-material-select\" [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\">{{ t('scene3d.repeater.pos_range', 'Range posizione') }} ({{ repeaterCfg.normalize.posRange }})</label>\n <input 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\">{{ t('scene3d.repeater.scale_range', 'Scala min/max') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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 </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\">{{ t('scene3d.physics.body', 'Corpo') }}</label>\n <select 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\">{{ t('scene3d.physics.shape', 'Forma collider') }}</label>\n <select 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\">{{ t('scene3d.physics.mass', 'Massa (kg)') }}</label>\n <input 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\">{{ t('scene3d.physics.friction', 'Attrito') }} ({{ propPhysics.friction }})</label>\n <input 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\">{{ t('scene3d.physics.restitution', 'Rimbalzo') }} ({{ propPhysics.restitution }})</label>\n <input 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-shader-toggle\" (click)=\"animRecSectionOpen = !animRecSectionOpen\">\n {{ t('scene3d.animrec.title', 'Registra animazione') }} {{ animRecSectionOpen ? '\u25BE' : '\u25B8' }}\n </div>\n @if (animRecSectionOpen) {\n <div class=\"scene3d-prop-group\">\n <label class=\"scene3d-prop-label\">{{ t('scene3d.animrec.clip_name', 'Nome clip') }}</label>\n <input 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\">{{ t('scene3d.animrec.time', 'Tempo T (s)') }}</label>\n <div class=\"scene3d-vector-row\">\n <input 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 +\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"] }]
4564
4799
  }], ctorParameters: () => [{ type: DataProviderService }, { type: MetadataProviderService }, { type: i1.Router }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: ChatbotHostRegistryService }], propDecorators: { viewportContainer: [{
4565
4800
  type: ViewChild,
4566
4801
  args: ['viewportContainer', { static: false }]
@@ -4575,4 +4810,4 @@ var scene3dDesigner_component = /*#__PURE__*/Object.freeze({
4575
4810
  });
4576
4811
 
4577
4812
  export { WUIC_BAKED_FLAG as W, isBakeableMesh as i, scene3dDesigner_component as s };
4578
- //# sourceMappingURL=wuic-framework-lib-scene3d-designer.component-BQzFxSXP.mjs.map
4813
+ //# sourceMappingURL=wuic-framework-lib-scene3d-designer.component-DjOfzKLo.mjs.map