wuic-framework-lib 1.3.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/fesm2022/{wuic-framework-lib-chart-list.component-DAP-TPJR.mjs → wuic-framework-lib-chart-list.component-0J1ghIrS.mjs} +4 -4
  2. package/fesm2022/{wuic-framework-lib-code-editor.component-CLMDvOWs.mjs → wuic-framework-lib-code-editor.component-C21mvjyr.mjs} +277 -13
  3. package/fesm2022/{wuic-framework-lib-designer.component-DA7abiLW.mjs → wuic-framework-lib-designer.component-x10ZzUEy.mjs} +1583 -194
  4. package/fesm2022/{wuic-framework-lib-first-run-wizard.component-BLrUrfZ9.mjs → wuic-framework-lib-first-run-wizard.component-BuSxs_CM.mjs} +7 -7
  5. package/fesm2022/{wuic-framework-lib-framework-docs.component-DphKhTef.mjs → wuic-framework-lib-framework-docs.component-xTGLbEnp.mjs} +1289 -82
  6. package/fesm2022/{wuic-framework-lib-pivot-builder.component-BqUUwrJM.mjs → wuic-framework-lib-pivot-builder.component-BkFFjPm4.mjs} +6 -6
  7. package/fesm2022/{wuic-framework-lib-prompt-dialog-fallback-host.component-DZrhwMH5.mjs → wuic-framework-lib-prompt-dialog-fallback-host.component-Bi-TrJB6.mjs} +2 -2
  8. package/fesm2022/{wuic-framework-lib-report-designer.component-D_qbefud.mjs → wuic-framework-lib-report-designer.component-ZvB46-LV.mjs} +2 -2
  9. package/fesm2022/{wuic-framework-lib-report-viewer.component-CIN2fY0P.mjs → wuic-framework-lib-report-viewer.component-BR-BfI2r.mjs} +2 -2
  10. package/fesm2022/wuic-framework-lib-scene3d-designer.component-BQzFxSXP.mjs +4578 -0
  11. package/fesm2022/wuic-framework-lib-scene3d-light-baker-gpu-0LjqhSNn.mjs +303 -0
  12. package/fesm2022/wuic-framework-lib-scene3d-shader-effect-CQLWkuh1.mjs +2227 -0
  13. package/fesm2022/wuic-framework-lib-scene3d-viewer.component-imoV3HOs.mjs +999 -0
  14. package/fesm2022/{wuic-framework-lib-scheduler-list.component-CdcI1Uor.mjs → wuic-framework-lib-scheduler-list.component-xzlTEkoq.mjs} +2 -2
  15. package/fesm2022/{wuic-framework-lib-spreadsheet-list-sf.component-j1HTRI_P.mjs → wuic-framework-lib-spreadsheet-list-sf.component-CrS6fOGj.mjs} +4 -4
  16. package/fesm2022/wuic-framework-lib-workflow-designer.component-5xtQ7-6z.mjs +9474 -0
  17. package/fesm2022/{wuic-framework-lib-wuic-framework-lib-BRujEwpR.mjs → wuic-framework-lib-wuic-framework-lib-3TZ-j-yL.mjs} +2615 -719
  18. package/fesm2022/wuic-framework-lib.mjs +1 -1
  19. package/package.json +2 -1
  20. package/types/wuic-framework-lib.d.ts +740 -8
  21. package/fesm2022/wuic-framework-lib-workflow-designer.component-SE_7ZogW.mjs +0 -5114
@@ -0,0 +1,999 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Input, ViewChild, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import * as THREE from 'three';
4
+ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
5
+ import { W as WtoolboxService, f as DataProviderService, M as MetadataProviderService } from './wuic-framework-lib-wuic-framework-lib-3TZ-j-yL.mjs';
6
+ import { L as deserializeScene, i as isWebGpuSupported, n as createScene3dRenderer, o as WUIC_SHADER_EFFECT_KEY, s as parseShaderEffect, u as buildEffectNodeMaterial, v as buildEffectShaderMaterial, w as getMeshRepeaterConfig, B as regenerateRepeater, x as WUIC_REPEATER_CHILD, C as CarouselController, b as defaultCarouselConfig, y as buildUploadTextureResolver, p as makeTextSpriteMesh, z as applyRepeaterTextureTo, E as createPhysicsWorld, D as getPhysicsDef, I as WUIC_BINDING_KEY, H as carouselArrowDir } from './wuic-framework-lib-scene3d-shader-effect-CQLWkuh1.mjs';
7
+ import * as i1 from '@angular/router';
8
+
9
+ /**
10
+ * Scene3D Viewer (F7) — runtime read-only della scena 3D autorata nel
11
+ * Scene3D Designer. Specchio "sola-lettura" di `Scene3dDesignerComponent`:
12
+ * stesso stack three.js (WebGLRenderer high-performance + OrbitControls +
13
+ * luci), stessa deserializzazione via {@link deserializeScene}, MA senza
14
+ * gizmo/palette/property-panel — solo visualizzazione + risoluzione binding.
15
+ *
16
+ * Viene caricato SOLO tramite `Scene3dViewerRouteComponent` (lazy) così l'import
17
+ * di three.js resta in un chunk code-split (mai importato dal barrel o dal route
18
+ * wrapper) — stesso pattern del designer.
19
+ *
20
+ * Runtime "augment": per ogni oggetto con `userData[WUIC_BINDING_KEY]` risolve i
21
+ * dati della rotta bindata via `DataProviderService.selectByRoute(route)` (il
22
+ * data-path del framework, lo stesso che alimenta la `<wuic-list-grid>` /
23
+ * `DataSourceComponent`), legge le colonne mappate da `fieldMap` e applica
24
+ * `color`/`visible` all'`Object3D`. Click su un oggetto bindato -> apre la CRUD
25
+ * della rotta con la stessa navigazione router del designer (F5).
26
+ */
27
+ class Scene3dViewerComponent {
28
+ dataSrv;
29
+ metaSrv;
30
+ route;
31
+ router;
32
+ cdr;
33
+ zone;
34
+ viewportContainer;
35
+ /**
36
+ * Chiave della scena da caricare. Se non passata come input, viene letta dal
37
+ * route param `scene_key` (mirror del `graph-id` del workflow-runner).
38
+ */
39
+ sceneKey = '';
40
+ /**
41
+ * Camera della scena da usare come punto di vista (uuid o `name` di una
42
+ * PerspectiveCamera autorata). Se non passata come input, viene letta dal
43
+ * route param `camera_id`. Vuota = prima camera della scena; nessuna camera
44
+ * in scena = viewpoint di default con OrbitControls.
45
+ */
46
+ cameraId = '';
47
+ /** true finche' la scena non e' stata caricata/parsata; pilota lo stato "loading" del template. */
48
+ loading = true;
49
+ /** Messaggio d'errore user-facing (scena non trovata / json invalido); '' = nessun errore. */
50
+ errorMessage = '';
51
+ // --- three.js runtime (mai serializzati) ---
52
+ renderer;
53
+ scene;
54
+ camera;
55
+ /** Camera AUTORATA scelta come punto di vista (null = default orbit). */
56
+ viewCamera;
57
+ /** Luci di viewport di default (pilotate da `viewport.defaultLightsEnabled` del payload). */
58
+ defaultLights = [];
59
+ orbit;
60
+ /** Gruppo che contiene SOLO il contenuto autorato (deserializzato). */
61
+ contentGroup;
62
+ raycaster = new THREE.Raycaster();
63
+ pointer = new THREE.Vector2();
64
+ animationFrameId = null;
65
+ resizeObserver;
66
+ /** Iter2: mixer per nodo animato (asset glTF/GLB con clip) — runtime-only, auto-play in loop. */
67
+ animMixers = [];
68
+ /** Clock che pilota l'avanzamento dei mixer nel render loop. */
69
+ animClock = new THREE.Clock();
70
+ /** ShaderMaterial (custom GLSL autorati) con uniform uTime da animare. */
71
+ shaderMaterials = new Set();
72
+ shaderClock = new THREE.Clock();
73
+ /** Fisica runtime (autoplay se la scena e' salvata con physics.autoPlayInViewer). */
74
+ physicsWorld;
75
+ /** true quando la view e' montata: consente al load async di innescare il build della scena. */
76
+ viewReady = false;
77
+ /** Payload gia' caricato ma in attesa che la view sia pronta (race load-vs-view). */
78
+ pendingPayload;
79
+ // --- bound listener (per removeEventListener in ngOnDestroy) ---
80
+ onCanvasClick = (event) => this.handleClick(event);
81
+ constructor(dataSrv, metaSrv, route, router, cdr, zone) {
82
+ this.dataSrv = dataSrv;
83
+ this.metaSrv = metaSrv;
84
+ this.route = route;
85
+ this.router = router;
86
+ this.cdr = cdr;
87
+ this.zone = zone;
88
+ }
89
+ /** Sub reattiva ai route param (unsubscribe in ngOnDestroy). */
90
+ routeParamSub;
91
+ ngOnInit() {
92
+ // Input ha priorita' (uso embedded): nessuna dipendenza dai route param.
93
+ if (String(this.sceneKey || '').trim()) {
94
+ void this.loadSceneByKey();
95
+ return;
96
+ }
97
+ // Route param REATTIVI: navigando tra scene diverse
98
+ // (#/scene3d_viewer/A -> #/scene3d_viewer/B) Angular RIUSA questo
99
+ // componente; il solo snapshot mostrerebbe per sempre la prima scena
100
+ // (bug scoperto dal worker thumbnail: 27 catture identiche).
101
+ const inputCam = String(this.cameraId || '').trim();
102
+ this.routeParamSub = this.route.paramMap.subscribe((params) => {
103
+ const nextKey = String(params.get('scene_key') || params.get('sceneKey') || '').trim();
104
+ const nextCam = inputCam || String(params.get('camera_id') || params.get('cameraId') || '').trim();
105
+ const changed = nextKey !== String(this.sceneKey || '') || nextCam !== String(this.cameraId || '');
106
+ this.sceneKey = nextKey;
107
+ this.cameraId = nextCam;
108
+ if (nextKey && changed) {
109
+ this.loading = true;
110
+ this.errorMessage = '';
111
+ this.cdr.markForCheck();
112
+ void this.loadSceneByKey();
113
+ }
114
+ });
115
+ }
116
+ ngAfterViewInit() {
117
+ this.viewReady = true;
118
+ this.initThree();
119
+ // Se il payload e' gia' arrivato prima del mount, costruiscilo ora.
120
+ if (this.pendingPayload) {
121
+ const payload = this.pendingPayload;
122
+ this.pendingPayload = undefined;
123
+ void this.buildScene(payload);
124
+ }
125
+ }
126
+ ngOnDestroy() {
127
+ this.routeParamSub?.unsubscribe();
128
+ this.disposeThree();
129
+ }
130
+ // ===========================================================================
131
+ // Load scena (read-only)
132
+ // ===========================================================================
133
+ /**
134
+ * Carica la scena per `sceneKey` via `DataProviderService.loadScene3d`,
135
+ * parsa `scene_json` e la costruisce (o la mette in pending se la view non e'
136
+ * ancora montata). Mirror sola-lettura di `Scene3dDesignerComponent.loadScene`.
137
+ */
138
+ async loadSceneByKey() {
139
+ const key = String(this.sceneKey || '').trim();
140
+ if (!key) {
141
+ this.loading = false;
142
+ this.errorMessage = 'scene_key mancante';
143
+ this.cdr.markForCheck();
144
+ return;
145
+ }
146
+ this.loading = true;
147
+ this.errorMessage = '';
148
+ this.cdr.markForCheck();
149
+ try {
150
+ const raw = await this.dataSrv.loadScene3d({ user_id: '', scene_key: key });
151
+ const record = Array.isArray(raw) ? raw[0] : raw;
152
+ const sceneJson = this.pickFirstDefined(record, ['scene_json', 'sceneJson']);
153
+ if (sceneJson == null) {
154
+ this.loading = false;
155
+ this.errorMessage = 'Scena non trovata';
156
+ this.cdr.markForCheck();
157
+ return;
158
+ }
159
+ const parsed = typeof sceneJson === 'string' ? JSON.parse(sceneJson) : sceneJson;
160
+ if (this.viewReady) {
161
+ await this.buildScene(parsed);
162
+ }
163
+ else {
164
+ // View non ancora montata: costruisci in ngAfterViewInit.
165
+ this.pendingPayload = parsed;
166
+ }
167
+ }
168
+ catch (err) {
169
+ this.loading = false;
170
+ this.errorMessage = 'Errore nel caricamento della scena';
171
+ this.cdr.markForCheck();
172
+ }
173
+ }
174
+ // ===========================================================================
175
+ // three.js bootstrap / teardown (read-only: NO gizmo, NO palette)
176
+ // ===========================================================================
177
+ initThree() {
178
+ const container = this.viewportContainer?.nativeElement;
179
+ if (!container) {
180
+ return;
181
+ }
182
+ const width = Math.max(1, container.clientWidth);
183
+ const height = Math.max(1, container.clientHeight);
184
+ const scene = new THREE.Scene();
185
+ scene.background = new THREE.Color(0x1e1e28);
186
+ this.scene = scene;
187
+ const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 2000);
188
+ camera.position.set(6, 6, 10);
189
+ camera.lookAt(0, 0, 0);
190
+ this.camera = camera;
191
+ const renderer = new THREE.WebGLRenderer({ powerPreference: 'high-performance', antialias: true });
192
+ renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio || 1, 2));
193
+ renderer.setSize(width, height);
194
+ // Fedelta' col designer: le luci autorate hanno castShadow -> shadow map attiva
195
+ // (il payload puo' disattivarla via viewport.shadowsEnabled).
196
+ renderer.shadowMap.enabled = true;
197
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
198
+ container.appendChild(renderer.domElement);
199
+ this.renderer = renderer;
200
+ // Luci di viewport (read-only: come nel designer ma senza il flag transient,
201
+ // qui nulla viene mai serializzato). Intensita' tenue come nel designer.
202
+ const ambient = new THREE.AmbientLight(0xffffff, 0.35);
203
+ scene.add(ambient);
204
+ const directional = new THREE.DirectionalLight(0xffffff, 0.4);
205
+ directional.position.set(5, 10, 7);
206
+ scene.add(directional);
207
+ this.defaultLights = [ambient, directional];
208
+ // OrbitControls: sola navigazione camera (NO TransformControls / gizmo).
209
+ const orbit = new OrbitControls(camera, renderer.domElement);
210
+ orbit.enableDamping = true;
211
+ orbit.dampingFactor = 0.08;
212
+ this.orbit = orbit;
213
+ // Click -> apri CRUD della rotta bindata (F5 pattern), fuori da Angular.
214
+ this.zone.runOutsideAngular(() => {
215
+ renderer.domElement.addEventListener('click', this.onCanvasClick);
216
+ });
217
+ this.resizeObserver = new ResizeObserver(() => this.handleResize());
218
+ this.resizeObserver.observe(container);
219
+ this.zone.runOutsideAngular(() => this.startRenderLoop());
220
+ }
221
+ startRenderLoop() {
222
+ const tick = () => {
223
+ this.animationFrameId = globalThis.requestAnimationFrame(tick);
224
+ this.orbit?.update();
225
+ // Iter2: avanza le animazioni auto-play (mixer per nodo animato).
226
+ const delta = this.animClock.getDelta();
227
+ for (const mixer of this.animMixers) {
228
+ mixer.update(delta);
229
+ }
230
+ // Fisica autoplay: step a timestep fisso + sync dei corpi sulle mesh.
231
+ if (this.physicsWorld) {
232
+ this.physicsWorld.step(delta);
233
+ }
234
+ // Carousel: anima lo scroll verso il target e ridispone gli item sull'arco.
235
+ for (const carousel of this.carousels) {
236
+ carousel.update(delta);
237
+ }
238
+ // Shader custom autorati: avanza la uniform uTime.
239
+ if (this.shaderMaterials.size > 0) {
240
+ const elapsed = this.shaderClock.getElapsedTime();
241
+ this.shaderMaterials.forEach((m) => { if (m.uniforms?.['uTime']) {
242
+ m.uniforms['uTime'].value = elapsed;
243
+ } });
244
+ }
245
+ if (this.renderer && this.scene && this.camera) {
246
+ this.renderer.render(this.scene, this.viewCamera || this.camera);
247
+ }
248
+ };
249
+ this.animationFrameId = globalThis.requestAnimationFrame(tick);
250
+ }
251
+ disposeThree() {
252
+ if (this.animationFrameId !== null) {
253
+ globalThis.cancelAnimationFrame(this.animationFrameId);
254
+ this.animationFrameId = null;
255
+ }
256
+ this.resizeObserver?.disconnect();
257
+ this.resizeObserver = undefined;
258
+ const canvas = this.renderer?.domElement;
259
+ if (canvas) {
260
+ canvas.removeEventListener('click', this.onCanvasClick);
261
+ }
262
+ this.orbit?.dispose();
263
+ this.orbit = undefined;
264
+ // Pausa/rilascio sicuro dei mixer PRIMA di smontare il contenuto.
265
+ this.disposeAnimationMixers();
266
+ this.physicsWorld?.dispose();
267
+ this.physicsWorld = undefined;
268
+ if (this.contentGroup) {
269
+ this.disposeObjectTree(this.contentGroup);
270
+ }
271
+ this.renderer?.dispose();
272
+ if (canvas?.parentElement) {
273
+ canvas.parentElement.removeChild(canvas);
274
+ }
275
+ this.renderer = undefined;
276
+ this.scene = undefined;
277
+ this.camera = undefined;
278
+ this.contentGroup = undefined;
279
+ }
280
+ /** Rilascia geometrie/materiali di un sotto-albero per evitare leak GPU. */
281
+ disposeObjectTree(root) {
282
+ root.traverse((obj) => {
283
+ const mesh = obj;
284
+ if (mesh.geometry && typeof mesh.geometry.dispose === 'function') {
285
+ mesh.geometry.dispose();
286
+ }
287
+ const material = mesh.material;
288
+ if (Array.isArray(material)) {
289
+ material.forEach((m) => m?.dispose?.());
290
+ }
291
+ else if (material && typeof material.dispose === 'function') {
292
+ material.dispose();
293
+ }
294
+ });
295
+ }
296
+ handleResize() {
297
+ const container = this.viewportContainer?.nativeElement;
298
+ if (!container || !this.renderer || !this.camera) {
299
+ return;
300
+ }
301
+ const width = Math.max(1, container.clientWidth);
302
+ const height = Math.max(1, container.clientHeight);
303
+ this.camera.aspect = width / height;
304
+ this.camera.updateProjectionMatrix();
305
+ if (this.viewCamera) {
306
+ this.viewCamera.aspect = width / height;
307
+ this.viewCamera.updateProjectionMatrix();
308
+ }
309
+ this.renderer.setSize(width, height);
310
+ }
311
+ // ===========================================================================
312
+ // Costruzione scena + risoluzione binding (runtime augment)
313
+ // ===========================================================================
314
+ /**
315
+ * Deserializza il payload, lo aggancia alla scena e risolve i binding.
316
+ * Mai lancia nel render path: ogni errore di binding e' isolato.
317
+ */
318
+ async buildScene(payload) {
319
+ if (!this.scene) {
320
+ return;
321
+ }
322
+ let root;
323
+ try {
324
+ root = deserializeScene(payload);
325
+ }
326
+ catch (err) {
327
+ this.loading = false;
328
+ this.errorMessage = 'Scena non valida';
329
+ this.cdr.markForCheck();
330
+ return;
331
+ }
332
+ // Rimpiazza l'eventuale contenuto precedente.
333
+ if (this.contentGroup) {
334
+ this.scene.remove(this.contentGroup);
335
+ this.disposeObjectTree(this.contentGroup);
336
+ }
337
+ root.name = root.name || 'wuicSceneContent';
338
+ this.scene.add(root);
339
+ this.contentGroup = root;
340
+ // Stato di viewport salvato con la scena (luci default / ombre).
341
+ this.applyViewportSettings(payload.viewport);
342
+ // Punto di vista: camera autorata (per id/nome o la prima), se presente.
343
+ this.applySceneCamera(root);
344
+ // Iter2: auto-play (loop) delle clip presenti nel contenuto deserializzato.
345
+ this.setupAnimationPlayback(root);
346
+ // Shader custom ripristinati dal payload: riaggancia l'animazione di uTime.
347
+ this.shaderMaterials.clear();
348
+ root.traverse((obj) => {
349
+ const material = obj.material;
350
+ const single = Array.isArray(material) ? material[0] : material;
351
+ if (single?.isShaderMaterial && single.uniforms?.uTime) {
352
+ this.shaderMaterials.add(single);
353
+ }
354
+ });
355
+ // Effetti JSON: il materiale serializzato e' un placeholder -> rigenera
356
+ // dal JSON in userData per il backend corrente (di nuovo dopo l'eventuale
357
+ // upgrade WebGPU asincrono innescato da applyViewportSettings).
358
+ void this.regenerateShaderEffects(root);
359
+ this.loading = false;
360
+ this.cdr.markForCheck();
361
+ // Mesh Repeater: figlie non persistite -> rigenera dai dati (non blocca il render).
362
+ void this.setupRepeaters(root);
363
+ // Fisica autoplay (se la scena la persiste): lazy, non blocca il primo render.
364
+ void this.setupPhysicsAutoplay(payload, root);
365
+ // Risoluzione binding: NON blocca il primo render (la scena e' gia' visibile).
366
+ void this.resolveBindings(root);
367
+ }
368
+ /**
369
+ * Applica i toggle di viewport persistiti col save del designer: luci default
370
+ * on/off e shadow map on/off. "Guide" non esiste nel viewer (nessun helper).
371
+ * No-op sui campi assenti (scene salvate prima della feature).
372
+ */
373
+ applyViewportSettings(settings) {
374
+ if (!settings || typeof settings !== 'object') {
375
+ return;
376
+ }
377
+ if (typeof settings.defaultLightsEnabled === 'boolean') {
378
+ for (const light of this.defaultLights) {
379
+ light.visible = settings.defaultLightsEnabled;
380
+ }
381
+ }
382
+ if (this.renderer && typeof settings.shadowsEnabled === 'boolean') {
383
+ this.renderer.shadowMap.enabled = settings.shadowsEnabled;
384
+ }
385
+ // Scena salvata col renderer WebGPU: upgrade del renderer (chunk lazy).
386
+ // Se WebGPU non e' disponibile il viewer resta su WebGL (fedele comunque).
387
+ if (settings.rendererMode === 'webgpu' && isWebGpuSupported()) {
388
+ void this.upgradeRendererToWebGpu();
389
+ }
390
+ }
391
+ /**
392
+ * Sostituisce il WebGLRenderer col WebGPURenderer (scena/camera preservate):
393
+ * usato quando `viewport.rendererMode === 'webgpu'` nel payload. Gli
394
+ * ShaderMaterial GLSL autorati non sono supportati dal renderer WebGPU e
395
+ * vengono rimpiazzati da un materiale standard neutro (il designer mostra
396
+ * lo stesso fallback in modalita' WebGPU).
397
+ */
398
+ async upgradeRendererToWebGpu() {
399
+ const container = this.viewportContainer?.nativeElement;
400
+ const old = this.renderer;
401
+ if (!container || !old) {
402
+ return;
403
+ }
404
+ this.replaceCustomShadersWithFallback();
405
+ const oldCanvas = old.domElement;
406
+ oldCanvas.removeEventListener('click', this.onCanvasClick);
407
+ this.orbit?.dispose();
408
+ this.orbit = undefined;
409
+ const shadowsEnabled = old.shadowMap.enabled;
410
+ old.dispose();
411
+ oldCanvas.parentElement?.removeChild(oldCanvas);
412
+ this.renderer = undefined;
413
+ const { renderer } = await createScene3dRenderer('webgpu', {
414
+ width: container.clientWidth,
415
+ height: container.clientHeight,
416
+ shadowsEnabled
417
+ });
418
+ container.appendChild(renderer.domElement);
419
+ // Effetti JSON ricompilati come node-material PRIMA di attivare il nuovo
420
+ // renderer (this.renderer ancora undefined -> il loop salta i frame).
421
+ this.rendererKind = 'webgpu';
422
+ if (this.contentGroup) {
423
+ await this.regenerateShaderEffects(this.contentGroup);
424
+ }
425
+ this.renderer = renderer;
426
+ const orbit = new OrbitControls(this.camera, renderer.domElement);
427
+ orbit.enableDamping = true;
428
+ orbit.dampingFactor = 0.08;
429
+ // Con una camera autorata come punto di vista l'orbit resta disabilitato.
430
+ orbit.enabled = !this.viewCamera;
431
+ this.orbit = orbit;
432
+ this.zone.runOutsideAngular(() => {
433
+ renderer.domElement.addEventListener('click', this.onCanvasClick);
434
+ });
435
+ }
436
+ /** ShaderMaterial GLSL -> materiale standard neutro (WebGPU non li compila). */
437
+ replaceCustomShadersWithFallback() {
438
+ this.shaderMaterials.clear();
439
+ this.scene?.traverse((obj) => {
440
+ const mesh = obj;
441
+ const single = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
442
+ // Le mesh con effetto JSON non degradano: vengono rigenerate come node-material.
443
+ if (mesh.isMesh && single?.isShaderMaterial && !mesh.userData?.[WUIC_SHADER_EFFECT_KEY]) {
444
+ mesh.material = new THREE.MeshStandardMaterial({ color: 0x9aa4b0, metalness: 0.1, roughness: 0.75 });
445
+ }
446
+ });
447
+ }
448
+ /** Percorso renderer attivo per la compilazione degli effetti JSON. */
449
+ rendererKind = 'webgl';
450
+ /**
451
+ * Rigenera i materiali delle mesh con effetto JSON (`userData.wuicShaderEffect`)
452
+ * per il backend corrente: GLSL (uTime nel loop) o node-material TSL.
453
+ */
454
+ async regenerateShaderEffects(root) {
455
+ const targets = [];
456
+ root.traverse((obj) => {
457
+ const mesh = obj;
458
+ const effect = mesh.isMesh ? parseShaderEffect(mesh.userData?.[WUIC_SHADER_EFFECT_KEY]) : null;
459
+ if (effect) {
460
+ targets.push({ mesh, effect });
461
+ }
462
+ });
463
+ if (targets.length === 0) {
464
+ return;
465
+ }
466
+ const webgpu = this.rendererKind === 'webgpu' ? await import('three/webgpu') : null;
467
+ for (const { mesh, effect } of targets) {
468
+ const current = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
469
+ if (current?.isShaderMaterial) {
470
+ this.shaderMaterials.delete(current);
471
+ }
472
+ const material = webgpu ? buildEffectNodeMaterial(effect, webgpu) : buildEffectShaderMaterial(effect);
473
+ mesh.material = material;
474
+ if (material.isShaderMaterial && material.uniforms?.['uTime']) {
475
+ this.shaderMaterials.add(material);
476
+ }
477
+ }
478
+ }
479
+ /**
480
+ * Sceglie la camera-punto-di-vista tra le PerspectiveCamera autorate nella
481
+ * scena (traverse: anche dentro i gruppi):
482
+ * - nessuna camera -> viewpoint di default con OrbitControls (design-free);
483
+ * - `cameraId` valorizzato -> match per `uuid` o `name` (case-insensitive),
484
+ * con fallback alla prima camera se non trovato (warn in console);
485
+ * - altrimenti -> prima camera trovata.
486
+ * Con una camera autorata l'orbit viene disabilitato (inquadratura fissa).
487
+ */
488
+ applySceneCamera(root) {
489
+ const cameras = [];
490
+ root.traverse((obj) => {
491
+ if (obj.isPerspectiveCamera) {
492
+ cameras.push(obj);
493
+ }
494
+ });
495
+ if (cameras.length === 0) {
496
+ this.viewCamera = undefined;
497
+ if (this.orbit) {
498
+ this.orbit.enabled = true;
499
+ }
500
+ return;
501
+ }
502
+ const wanted = String(this.cameraId || '').trim().toLowerCase();
503
+ let chosen = cameras[0];
504
+ if (wanted) {
505
+ const match = cameras.find((c) => String(c.uuid || '').toLowerCase() === wanted
506
+ || String(c.name || '').trim().toLowerCase() === wanted);
507
+ if (match) {
508
+ chosen = match;
509
+ }
510
+ else {
511
+ console.warn(`[Scene3dViewer] camera '${this.cameraId}' non trovata nella scena: uso la prima (${cameras[0].uuid})`);
512
+ }
513
+ }
514
+ // Aspect allineato al viewport (le camere autorate nascono con aspect=1).
515
+ const container = this.viewportContainer?.nativeElement;
516
+ if (container) {
517
+ chosen.aspect = Math.max(1, container.clientWidth) / Math.max(1, container.clientHeight);
518
+ chosen.updateProjectionMatrix();
519
+ }
520
+ this.viewCamera = chosen;
521
+ // Inquadratura fissa della camera autorata: niente orbit.
522
+ if (this.orbit) {
523
+ this.orbit.enabled = false;
524
+ }
525
+ }
526
+ /** Cache texture per-URL (repeater). */
527
+ repeaterTextureCache = new Map();
528
+ /** Controller dei carousel attivi (scroll animato + click frecce). Runtime-only. */
529
+ carousels = [];
530
+ /**
531
+ * Rigenera le mesh figlie di ogni Mesh Repeater dai dati della rotta (le
532
+ * figlie non sono persistite). Ogni figlia riceve il binding -> click-through
533
+ * verso la CRUD del record gia' funzionante nel viewer.
534
+ */
535
+ async setupRepeaters(root) {
536
+ const repeaters = [];
537
+ root.traverse((obj) => { if (getMeshRepeaterConfig(obj)) {
538
+ repeaters.push(obj);
539
+ } });
540
+ if (repeaters.length === 0) {
541
+ return;
542
+ }
543
+ this.carousels = [];
544
+ for (const group of repeaters) {
545
+ const cfg = getMeshRepeaterConfig(group);
546
+ if (!cfg.route) {
547
+ continue;
548
+ }
549
+ try {
550
+ const [rows, resolver] = await Promise.all([
551
+ this.fetchRouteRows(cfg.route),
552
+ this.buildTextureResolver(cfg)
553
+ ]);
554
+ regenerateRepeater(group, cfg, rows, this.buildRepeaterDeps(resolver));
555
+ // Archetipo carousel: attacca il controller (scroll animato + click frecce).
556
+ if (cfg.layout?.mode === 'carousel') {
557
+ const items = group.children.filter((c) => c.name === WUIC_REPEATER_CHILD);
558
+ this.carousels.push(new CarouselController(group, items, cfg.carousel || defaultCarouselConfig()));
559
+ }
560
+ }
561
+ catch { /* best-effort per repeater */ }
562
+ }
563
+ this.cdr.markForCheck();
564
+ }
565
+ /**
566
+ * Resolver texture per un repeater: colonna upload → URL (filesystem/blob)
567
+ * come `GetSrcUploadPreviewPipe`; altrimenti `undefined` (valore as-is).
568
+ */
569
+ async buildTextureResolver(cfg) {
570
+ const texCol = cfg.fieldMap?.texture;
571
+ if (!cfg.route || !texCol) {
572
+ return undefined;
573
+ }
574
+ try {
575
+ const columns = await this.metaSrv.getMetadati(cfg.route);
576
+ const app = { uploadPath: WtoolboxService.appSettings?.upload_path || '', apiUrl: WtoolboxService.appSettings?.api_url || '' };
577
+ return buildUploadTextureResolver(columns || [], cfg.route, texCol, app, (v) => this.unwrap(v)) || undefined;
578
+ }
579
+ catch {
580
+ return undefined;
581
+ }
582
+ }
583
+ /** Factory read-only per le mesh figlie del repeater (primitive standard). */
584
+ buildRepeaterDeps(resolveTextureSrc) {
585
+ const geom = (type) => {
586
+ switch (type) {
587
+ case 'sphere': return new THREE.SphereGeometry(0.75, 24, 12);
588
+ case 'plane': return new THREE.PlaneGeometry(1.5, 1.5);
589
+ case 'cylinder': return new THREE.CylinderGeometry(0.5, 0.5, 1.2, 24);
590
+ case 'cone': return new THREE.ConeGeometry(0.6, 1.2, 24);
591
+ case 'torus': return new THREE.TorusGeometry(0.6, 0.22, 12, 32);
592
+ default: return new THREE.BoxGeometry(1, 1, 1); // box + fallback custom
593
+ }
594
+ };
595
+ return {
596
+ createMesh: (type) => new THREE.Mesh(geom(String(type).replace(/^custom:/, '')), new THREE.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.1, roughness: 0.75 })),
597
+ applyColor: (obj, val) => { this.applyColorValue(obj, val); },
598
+ applyShadow: (obj) => { const m = obj; m.castShadow = true; m.receiveShadow = true; },
599
+ applyTexture: (obj, value, opts) => this.applyRepeaterTexture(obj, value, opts),
600
+ createTextMesh: (text) => makeTextSpriteMesh(text),
601
+ resolveRecordId: (record) => this.resolveRecordPk(record),
602
+ unwrap: (v) => this.unwrap(v),
603
+ resolveTextureSrc
604
+ };
605
+ }
606
+ applyRepeaterTexture(obj, value, opts) {
607
+ applyRepeaterTextureTo(obj, value, opts, this.repeaterTextureCache, (url) => {
608
+ const t = new THREE.TextureLoader().load(url, () => this.cdr.markForCheck(), undefined, () => { });
609
+ t.colorSpace = THREE.SRGBColorSpace;
610
+ return t;
611
+ }, () => this.cdr.markForCheck());
612
+ }
613
+ resolveRecordPk(record) {
614
+ if (!record || typeof record !== 'object') {
615
+ return '';
616
+ }
617
+ for (const cand of ['id', 'Id', 'ID', 'CityID', 'code', 'codice']) {
618
+ if (record[cand] != null) {
619
+ return String(this.unwrap(record[cand]));
620
+ }
621
+ }
622
+ const first = Object.keys(record)[0];
623
+ return first ? String(this.unwrap(record[first])) : '';
624
+ }
625
+ /**
626
+ * Avvia la simulazione fisica se la scena e' stata salvata con
627
+ * `physics.enabled && autoPlayInViewer` (import Rapier lazy). Best-effort:
628
+ * ogni errore e' isolato dal render path.
629
+ */
630
+ async setupPhysicsAutoplay(payload, root) {
631
+ this.physicsWorld?.dispose();
632
+ this.physicsWorld = undefined;
633
+ const physics = payload.physics;
634
+ if (!physics?.enabled || physics.autoPlayInViewer === false) {
635
+ return;
636
+ }
637
+ try {
638
+ const gravity = Array.isArray(physics.gravity) && physics.gravity.length === 3
639
+ ? [Number(physics.gravity[0]) || 0, Number(physics.gravity[1]) || -9.81, Number(physics.gravity[2]) || 0]
640
+ : [0, -9.81, 0];
641
+ const world = await createPhysicsWorld(gravity);
642
+ if (physics.ground !== false) {
643
+ world.addGround();
644
+ }
645
+ root.traverse((obj) => {
646
+ const def = getPhysicsDef(obj);
647
+ if (def && obj.isMesh) {
648
+ world.addBody(obj, def);
649
+ }
650
+ });
651
+ if (world.bodyCount > 0) {
652
+ this.physicsWorld = world;
653
+ }
654
+ else {
655
+ world.dispose();
656
+ }
657
+ }
658
+ catch (err) {
659
+ console.warn('[Scene3dViewer] fisica autoplay non avviata', err);
660
+ }
661
+ }
662
+ // ===========================================================================
663
+ // Iter2: animazioni auto-play (read-only, nessun transport)
664
+ // ===========================================================================
665
+ /**
666
+ * Per ogni nodo del contenuto che porta `Object3D.animations` (clip di asset
667
+ * glTF/GLB importati, ripristinate da `ObjectLoader.parse`) crea un
668
+ * `THREE.AnimationMixer` e avvia TUTTE le clip in loop (`LoopRepeat`).
669
+ * Playback read-only: nessun transport; pausa sicura in `ngOnDestroy` via
670
+ * {@link disposeAnimationMixers}. Mixer/clock sono runtime-only (mai serializzati).
671
+ */
672
+ setupAnimationPlayback(root) {
673
+ this.disposeAnimationMixers();
674
+ root.traverse((obj) => {
675
+ const clips = obj.animations;
676
+ if (!Array.isArray(clips) || clips.length === 0) {
677
+ return;
678
+ }
679
+ const mixer = new THREE.AnimationMixer(obj);
680
+ let started = false;
681
+ for (const clip of clips) {
682
+ if (!clip) {
683
+ continue;
684
+ }
685
+ const action = mixer.clipAction(clip);
686
+ action.setLoop(THREE.LoopRepeat, Infinity);
687
+ action.play();
688
+ started = true;
689
+ }
690
+ if (started) {
691
+ this.animMixers.push(mixer);
692
+ }
693
+ });
694
+ }
695
+ /** Ferma tutte le action e rilascia i mixer (destroy / rebuild della scena). */
696
+ disposeAnimationMixers() {
697
+ for (const mixer of this.animMixers) {
698
+ mixer.stopAllAction();
699
+ mixer.uncacheRoot(mixer.getRoot());
700
+ }
701
+ this.animMixers = [];
702
+ }
703
+ /**
704
+ * Per ogni oggetto con `userData[WUIC_BINDING_KEY]` risolve i dati della rotta
705
+ * bindata e applica le colonne mappate (`color` -> materiale, `visible` ->
706
+ * `object.visible`). Robusto: se rotta/dati non disponibili, salta quel
707
+ * binding senza mai lanciare.
708
+ */
709
+ async resolveBindings(root) {
710
+ // Raccogli gli oggetti bindati (traverse e' sincrono; l'apply e' async).
711
+ const bound = [];
712
+ root.traverse((obj) => {
713
+ const binding = obj.userData ? obj.userData[WUIC_BINDING_KEY] : undefined;
714
+ if (binding && typeof binding.route === 'string' && binding.route.trim().length > 0) {
715
+ bound.push({ obj, binding });
716
+ }
717
+ });
718
+ if (bound.length === 0) {
719
+ return;
720
+ }
721
+ // Cache per rotta: un solo fetch anche se piu' oggetti bindano la stessa rotta.
722
+ const rowsByRoute = new Map();
723
+ for (const { obj, binding } of bound) {
724
+ try {
725
+ const route = binding.route.trim();
726
+ if (!rowsByRoute.has(route)) {
727
+ rowsByRoute.set(route, await this.fetchRouteRows(route));
728
+ }
729
+ const rows = rowsByRoute.get(route) || [];
730
+ const record = this.pickRecord(rows, binding.recordId);
731
+ if (record) {
732
+ this.applyBindingToObject(obj, binding, record);
733
+ }
734
+ }
735
+ catch {
736
+ // Binding fallito: salta (mai propagare nel render path).
737
+ }
738
+ }
739
+ this.cdr.markForCheck();
740
+ }
741
+ /**
742
+ * Recupera le righe di una rotta scaffoldata via il data-path del framework
743
+ * `DataProviderService.selectByRoute(route)` — lo stesso metodo che alimenta
744
+ * `<wuic-list-grid>` / `DataSourceComponent` (che a runtime chiama
745
+ * `DataProviderService.select(scope, ...)`). Qui non serve un `DataSourceComponent`:
746
+ * `selectByRoute` incapsula l'user id corrente e ritorna un `ResultInfo` con
747
+ * `.dato` = array di record. Prima pagina (fino a 200 righe): sufficiente a
748
+ * risolvere il binding di rotta o del singolo record.
749
+ * @returns Array di record (mai lancia; `[]` se la rotta/i dati non sono disponibili).
750
+ */
751
+ async fetchRouteRows(route) {
752
+ try {
753
+ const result = await this.dataSrv.selectByRoute(route, [], [], 200, 1);
754
+ const rows = (result && Array.isArray(result.dato)) ? result.dato : [];
755
+ return rows;
756
+ }
757
+ catch {
758
+ return [];
759
+ }
760
+ }
761
+ /**
762
+ * Seleziona il record target: per `recordId` (match case-insensitive su
763
+ * qualunque campo il cui valore uguaglia l'id, tipicamente la PK); altrimenti
764
+ * la prima riga (binding di rotta).
765
+ */
766
+ pickRecord(rows, recordId) {
767
+ if (!Array.isArray(rows) || rows.length === 0) {
768
+ return undefined;
769
+ }
770
+ if (recordId == null || String(recordId).trim() === '') {
771
+ return rows[0];
772
+ }
773
+ const target = String(recordId).trim().toLowerCase();
774
+ for (const row of rows) {
775
+ if (!row || typeof row !== 'object') {
776
+ continue;
777
+ }
778
+ for (const key of Object.keys(row)) {
779
+ const value = this.unwrap(row[key]);
780
+ if (value != null && String(value).trim().toLowerCase() === target) {
781
+ return row;
782
+ }
783
+ }
784
+ }
785
+ return undefined;
786
+ }
787
+ /**
788
+ * Applica le colonne mappate del `fieldMap` all'`Object3D`:
789
+ * - colonna `color` -> colore del materiale/luce (hex `#rrggbb`/`0xRRGGBB` o
790
+ * nome css tramite `THREE.Color`);
791
+ * - colonna `visible` -> `object.visible` in base alla truthiness del valore.
792
+ * Mai lancia.
793
+ */
794
+ applyBindingToObject(obj, binding, record) {
795
+ const fieldMap = binding.fieldMap || {};
796
+ // color
797
+ const colorColumn = String(fieldMap['color'] || '').trim();
798
+ if (colorColumn) {
799
+ const rawColor = this.unwrap(record[colorColumn]);
800
+ const applied = this.applyColorValue(obj, rawColor);
801
+ if (!applied) {
802
+ // valore non parsabile: lascia il colore autorato invariato.
803
+ }
804
+ }
805
+ // visible
806
+ const visibleColumn = String(fieldMap['visible'] || '').trim();
807
+ if (visibleColumn) {
808
+ const rawVisible = this.unwrap(record[visibleColumn]);
809
+ obj.visible = this.toBoolean(rawVisible);
810
+ }
811
+ }
812
+ /** Applica un valore-colonna come colore all'oggetto; true se ha applicato qualcosa. */
813
+ applyColorValue(obj, rawValue) {
814
+ if (rawValue == null) {
815
+ return false;
816
+ }
817
+ const color = this.getObjectColor(obj);
818
+ if (!color) {
819
+ return false;
820
+ }
821
+ const str = String(rawValue).trim();
822
+ if (!str) {
823
+ return false;
824
+ }
825
+ try {
826
+ if (/^0x[0-9a-fA-F]{6}$/.test(str)) {
827
+ color.setHex(parseInt(str.slice(2), 16));
828
+ }
829
+ else if (/^#?[0-9a-fA-F]{6}$/.test(str) || /^#?[0-9a-fA-F]{3}$/.test(str)) {
830
+ color.set(str.startsWith('#') ? str : `#${str}`);
831
+ }
832
+ else {
833
+ // Nome css (es. 'red') o 'rgb(...)': delega a THREE.Color.
834
+ color.set(str);
835
+ }
836
+ return true;
837
+ }
838
+ catch {
839
+ return false;
840
+ }
841
+ }
842
+ /** Ritorna il `THREE.Color` editabile dell'oggetto (materiale mesh o colore luce), se presente. */
843
+ getObjectColor(object) {
844
+ if (!object) {
845
+ return undefined;
846
+ }
847
+ const material = object.material;
848
+ if (material) {
849
+ const single = Array.isArray(material) ? material[0] : material;
850
+ if (single && single.color instanceof THREE.Color) {
851
+ return single.color;
852
+ }
853
+ }
854
+ const lightColor = object.color;
855
+ if (lightColor instanceof THREE.Color) {
856
+ return lightColor;
857
+ }
858
+ return undefined;
859
+ }
860
+ /** Coercizione truthiness robusta (bool/numero/stringa 'true'/'1'/'y'/'si'/'yes'). */
861
+ toBoolean(value) {
862
+ if (typeof value === 'boolean') {
863
+ return value;
864
+ }
865
+ if (typeof value === 'number') {
866
+ return value !== 0;
867
+ }
868
+ const s = String(value ?? '').trim().toLowerCase();
869
+ if (s === '') {
870
+ return false;
871
+ }
872
+ return !['0', 'false', 'no', 'n', 'off', 'null', 'undefined'].includes(s);
873
+ }
874
+ /** Srotola un valore che potrebbe essere un BehaviorSubject (record framework) o un plain value. */
875
+ unwrap(value) {
876
+ if (value && typeof value === 'object' && typeof value.getValue === 'function') {
877
+ try {
878
+ return value.getValue();
879
+ }
880
+ catch {
881
+ return value;
882
+ }
883
+ }
884
+ if (value && typeof value === 'object' && 'value' in value && typeof value.subscribe === 'function') {
885
+ return value.value;
886
+ }
887
+ return value;
888
+ }
889
+ // ===========================================================================
890
+ // Click -> apri CRUD della rotta bindata (F5 pattern)
891
+ // ===========================================================================
892
+ handleClick(event) {
893
+ if (!this.renderer || !this.camera || !this.contentGroup) {
894
+ return;
895
+ }
896
+ const rect = this.renderer.domElement.getBoundingClientRect();
897
+ this.pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
898
+ this.pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
899
+ this.raycaster.setFromCamera(this.pointer, this.viewCamera || this.camera);
900
+ const hits = this.raycaster.intersectObjects([this.contentGroup], true);
901
+ if (hits.length === 0) {
902
+ return;
903
+ }
904
+ // Click su una freccia nav del carousel: scorri, non aprire nessun record.
905
+ const dir = carouselArrowDir(hits[0].object);
906
+ if (dir) {
907
+ const controller = this.carousels.find((c) => this.isDescendant(c.group, hits[0].object));
908
+ if (controller) {
909
+ if (dir === 'next') {
910
+ controller.next();
911
+ }
912
+ else {
913
+ controller.prev();
914
+ }
915
+ }
916
+ return;
917
+ }
918
+ const picked = this.resolveBoundAncestor(hits[0].object);
919
+ const binding = picked?.userData?.[WUIC_BINDING_KEY];
920
+ if (!binding || !String(binding.route || '').trim()) {
921
+ return;
922
+ }
923
+ this.zone.run(() => this.openRecordForBinding(binding));
924
+ }
925
+ /**
926
+ * Dal mesh colpito risale al primo antenato (incluso se stesso) che porta un
927
+ * binding, così il click su un sub-mesh apre comunque la CRUD dell'oggetto bindato.
928
+ */
929
+ resolveBoundAncestor(object) {
930
+ let current = object;
931
+ while (current && current !== this.contentGroup) {
932
+ if (current.userData && current.userData[WUIC_BINDING_KEY]) {
933
+ return current;
934
+ }
935
+ current = current.parent;
936
+ }
937
+ return undefined;
938
+ }
939
+ /** true se `node` e' `ancestor` o un suo discendente (per associare la freccia al carousel). */
940
+ isDescendant(ancestor, node) {
941
+ let cur = node;
942
+ while (cur) {
943
+ if (cur === ancestor) {
944
+ return true;
945
+ }
946
+ cur = cur.parent;
947
+ }
948
+ return false;
949
+ }
950
+ /**
951
+ * Naviga alla CRUD della rotta bindata — STESSA navigazione router del designer
952
+ * (F5): `['/', route, 'edit', recordId]` se `recordId` valorizzato, altrimenti
953
+ * `['/', route, 'list']`. Le permission della rotta sono enforced server-side
954
+ * dalla CRUD aperta.
955
+ */
956
+ openRecordForBinding(binding) {
957
+ const route = String(binding?.route || '').trim();
958
+ if (!route) {
959
+ return;
960
+ }
961
+ const recordId = binding?.recordId != null ? String(binding.recordId).trim() : '';
962
+ if (recordId) {
963
+ void this.router.navigate(['/', route, 'edit', recordId]);
964
+ }
965
+ else {
966
+ void this.router.navigate(['/', route, 'list']);
967
+ }
968
+ }
969
+ // ===========================================================================
970
+ // Helpers
971
+ // ===========================================================================
972
+ pickFirstDefined(source, keys) {
973
+ if (!source) {
974
+ return undefined;
975
+ }
976
+ for (const key of keys) {
977
+ if (source[key] !== undefined && source[key] !== null) {
978
+ return source[key];
979
+ }
980
+ }
981
+ return undefined;
982
+ }
983
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: Scene3dViewerComponent, deps: [{ token: DataProviderService }, { token: MetadataProviderService }, { token: i1.ActivatedRoute }, { token: i1.Router }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
984
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: Scene3dViewerComponent, isStandalone: true, selector: "wuic-scene3d-viewer", inputs: { sceneKey: "sceneKey", cameraId: "cameraId" }, viewQueries: [{ propertyName: "viewportContainer", first: true, predicate: ["viewportContainer"], descendants: true }], ngImport: i0, template: "<!-- F7: viewer WebGL read-only (canvas three.js). NO palette, NO gizmo,\n NO property panel: solo viewport + overlay stato (loading / errore). -->\n<div class=\"scene3d-viewer-shell\">\n\n <!-- Viewport (canvas three.js) -->\n <div #viewportContainer class=\"scene3d-viewer-viewport\"></div>\n\n <!-- Overlay stato: loading -->\n @if (loading) {\n <div class=\"scene3d-viewer-overlay\">\n <i class=\"pi pi-spin pi-spinner scene3d-viewer-spinner\"></i>\n <span>Caricamento scena...</span>\n </div>\n }\n\n <!-- Overlay stato: errore -->\n @if (!loading && errorMessage) {\n <div class=\"scene3d-viewer-overlay scene3d-viewer-error\">\n <i class=\"pi pi-exclamation-triangle\"></i>\n <span>{{ errorMessage }}</span>\n </div>\n }\n\n</div>\n", styles: [".scene3d-viewer-shell{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:480px}.scene3d-viewer-viewport{flex:1 1 auto;min-width:0;min-height:0;position:relative;overflow:hidden;background:#1e1e28}.scene3d-viewer-viewport canvas{display:block;width:100%;height:100%}.scene3d-viewer-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.6rem;font-size:.9rem;color:#e8e8f0;background:#1e1e288c;pointer-events:none}.scene3d-viewer-spinner{font-size:1.6rem}.scene3d-viewer-error{color:#ffb4b4}.scene3d-viewer-error i{font-size:1.6rem}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
985
+ }
986
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: Scene3dViewerComponent, decorators: [{
987
+ type: Component,
988
+ args: [{ selector: 'wuic-scene3d-viewer', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- F7: viewer WebGL read-only (canvas three.js). NO palette, NO gizmo,\n NO property panel: solo viewport + overlay stato (loading / errore). -->\n<div class=\"scene3d-viewer-shell\">\n\n <!-- Viewport (canvas three.js) -->\n <div #viewportContainer class=\"scene3d-viewer-viewport\"></div>\n\n <!-- Overlay stato: loading -->\n @if (loading) {\n <div class=\"scene3d-viewer-overlay\">\n <i class=\"pi pi-spin pi-spinner scene3d-viewer-spinner\"></i>\n <span>Caricamento scena...</span>\n </div>\n }\n\n <!-- Overlay stato: errore -->\n @if (!loading && errorMessage) {\n <div class=\"scene3d-viewer-overlay scene3d-viewer-error\">\n <i class=\"pi pi-exclamation-triangle\"></i>\n <span>{{ errorMessage }}</span>\n </div>\n }\n\n</div>\n", styles: [".scene3d-viewer-shell{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:480px}.scene3d-viewer-viewport{flex:1 1 auto;min-width:0;min-height:0;position:relative;overflow:hidden;background:#1e1e28}.scene3d-viewer-viewport canvas{display:block;width:100%;height:100%}.scene3d-viewer-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.6rem;font-size:.9rem;color:#e8e8f0;background:#1e1e288c;pointer-events:none}.scene3d-viewer-spinner{font-size:1.6rem}.scene3d-viewer-error{color:#ffb4b4}.scene3d-viewer-error i{font-size:1.6rem}\n"] }]
989
+ }], ctorParameters: () => [{ type: DataProviderService }, { type: MetadataProviderService }, { type: i1.ActivatedRoute }, { type: i1.Router }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }], propDecorators: { viewportContainer: [{
990
+ type: ViewChild,
991
+ args: ['viewportContainer', { static: false }]
992
+ }], sceneKey: [{
993
+ type: Input
994
+ }], cameraId: [{
995
+ type: Input
996
+ }] } });
997
+
998
+ export { Scene3dViewerComponent };
999
+ //# sourceMappingURL=wuic-framework-lib-scene3d-viewer.component-imoV3HOs.mjs.map