wuic-framework-lib 1.5.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/{wuic-framework-lib-chart-list.component-0J1ghIrS.mjs → wuic-framework-lib-chart-list.component-Dq9IEKsY.mjs} +50 -38
- package/fesm2022/{wuic-framework-lib-code-editor.component-C21mvjyr.mjs → wuic-framework-lib-code-editor.component-B6IpTSo8.mjs} +174 -274
- package/fesm2022/{wuic-framework-lib-designer.component-x10ZzUEy.mjs → wuic-framework-lib-designer.component-D6cbqNFg.mjs} +499 -581
- package/fesm2022/wuic-framework-lib-first-run-wizard.component-D1ZQUl-q.mjs +1630 -0
- package/fesm2022/{wuic-framework-lib-framework-docs.component-xTGLbEnp.mjs → wuic-framework-lib-framework-docs.component-C_o5r7pg.mjs} +4320 -1255
- package/fesm2022/{wuic-framework-lib-pivot-builder.component-BkFFjPm4.mjs → wuic-framework-lib-pivot-builder.component-DkWGzJpr.mjs} +580 -68
- package/fesm2022/{wuic-framework-lib-prompt-dialog-fallback-host.component-Bi-TrJB6.mjs → wuic-framework-lib-prompt-dialog-fallback-host.component-D29go5yA.mjs} +2 -2
- package/fesm2022/wuic-framework-lib-report-designer.component-DcpCWghx.mjs +380 -0
- package/fesm2022/wuic-framework-lib-report-scaffold-dialog.component-CgUjG7Bi.mjs +431 -0
- package/fesm2022/{wuic-framework-lib-report-viewer.component-BR-BfI2r.mjs → wuic-framework-lib-report-viewer.component-C4Opgl_M.mjs} +8 -13
- package/fesm2022/{wuic-framework-lib-scene3d-designer.component-BQzFxSXP.mjs → wuic-framework-lib-scene3d-designer.component-8l3vsyEb.mjs} +323 -88
- package/fesm2022/{wuic-framework-lib-scene3d-light-baker-gpu-0LjqhSNn.mjs → wuic-framework-lib-scene3d-light-baker-gpu-C1w-8Aai.mjs} +2 -2
- package/fesm2022/{wuic-framework-lib-scene3d-shader-effect-CQLWkuh1.mjs → wuic-framework-lib-scene3d-shader-effect-DjHyCAFK.mjs} +612 -32
- package/fesm2022/{wuic-framework-lib-scene3d-viewer.component-imoV3HOs.mjs → wuic-framework-lib-scene3d-viewer.component-nMSl5jbe.mjs} +149 -18
- package/fesm2022/{wuic-framework-lib-scheduler-list.component-xzlTEkoq.mjs → wuic-framework-lib-scheduler-list.component-Cvk3Zg0Q.mjs} +169 -53
- package/fesm2022/{wuic-framework-lib-spreadsheet-list-sf.component-CrS6fOGj.mjs → wuic-framework-lib-spreadsheet-list-sf.component-C1U7JPiq.mjs} +149 -108
- package/fesm2022/{wuic-framework-lib-stimulsoft-license.service-BIyxB4pO.mjs → wuic-framework-lib-stimulsoft-license.service-CsNNSFYs.mjs} +3 -3
- package/fesm2022/wuic-framework-lib-timeline-list.component-cZbPgNh1.mjs +805 -0
- package/fesm2022/{wuic-framework-lib-workflow-designer.component-5xtQ7-6z.mjs → wuic-framework-lib-workflow-designer.component-C_4RQGa0.mjs} +180 -150
- package/fesm2022/{wuic-framework-lib-wuic-framework-lib-3TZ-j-yL.mjs → wuic-framework-lib-wuic-framework-lib-BZZwta8b.mjs} +6785 -2347
- package/fesm2022/wuic-framework-lib.mjs +1 -1
- package/package.json +21 -2
- package/styles/_wuic-tokens.scss +82 -0
- package/styles/styles.scss +160 -0
- package/types/wuic-framework-lib.d.ts +4269 -2691
- package/fesm2022/wuic-framework-lib-first-run-wizard.component-BuSxs_CM.mjs +0 -1316
- 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
|
|
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
|
|
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,
|
|
21
|
-
import { W as WtoolboxService,
|
|
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-BZZwta8b.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
|
|
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
|
-
&&
|
|
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 ||
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
|
1424
|
-
|
|
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
|
|
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.
|
|
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
|
|
2021
|
-
|
|
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' :
|
|
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(
|
|
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-
|
|
2326
|
+
const { bakeVertexLightingGpu } = await import('./wuic-framework-lib-scene3d-light-baker-gpu-C1w-8Aai.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
|
-
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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) =>
|
|
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.
|
|
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
|
|
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) =>
|
|
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.
|
|
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
|
-
|
|
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().
|
|
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.
|
|
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-
|
|
4813
|
+
//# sourceMappingURL=wuic-framework-lib-scene3d-designer.component-8l3vsyEb.mjs.map
|