wuic-framework-lib 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/{wuic-framework-lib-chart-list.component-0J1ghIrS.mjs → wuic-framework-lib-chart-list.component-Bl424n7x.mjs} +50 -38
- package/fesm2022/{wuic-framework-lib-code-editor.component-C21mvjyr.mjs → wuic-framework-lib-code-editor.component-N6HMl2mf.mjs} +174 -274
- package/fesm2022/{wuic-framework-lib-designer.component-x10ZzUEy.mjs → wuic-framework-lib-designer.component-Bd2fwl6H.mjs} +499 -581
- package/fesm2022/{wuic-framework-lib-first-run-wizard.component-BuSxs_CM.mjs → wuic-framework-lib-first-run-wizard.component-NN6cWVHL.mjs} +22 -24
- package/fesm2022/{wuic-framework-lib-framework-docs.component-xTGLbEnp.mjs → wuic-framework-lib-framework-docs.component-B867JAYe.mjs} +2912 -294
- package/fesm2022/{wuic-framework-lib-pivot-builder.component-BkFFjPm4.mjs → wuic-framework-lib-pivot-builder.component-DWukH0L-.mjs} +580 -68
- package/fesm2022/{wuic-framework-lib-prompt-dialog-fallback-host.component-Bi-TrJB6.mjs → wuic-framework-lib-prompt-dialog-fallback-host.component-DWO1Rcgv.mjs} +2 -2
- package/fesm2022/wuic-framework-lib-report-designer.component-QcrU81m6.mjs +380 -0
- package/fesm2022/wuic-framework-lib-report-scaffold-dialog.component-U-zdOoRV.mjs +431 -0
- package/fesm2022/{wuic-framework-lib-report-viewer.component-BR-BfI2r.mjs → wuic-framework-lib-report-viewer.component-B4LhA3SL.mjs} +8 -13
- package/fesm2022/{wuic-framework-lib-scene3d-designer.component-BQzFxSXP.mjs → wuic-framework-lib-scene3d-designer.component-DjOfzKLo.mjs} +323 -88
- package/fesm2022/{wuic-framework-lib-scene3d-light-baker-gpu-0LjqhSNn.mjs → wuic-framework-lib-scene3d-light-baker-gpu-C96T9uIW.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-Dboz1fjl.mjs} +149 -18
- package/fesm2022/{wuic-framework-lib-scheduler-list.component-xzlTEkoq.mjs → wuic-framework-lib-scheduler-list.component--wY21R3o.mjs} +169 -53
- package/fesm2022/{wuic-framework-lib-spreadsheet-list-sf.component-CrS6fOGj.mjs → wuic-framework-lib-spreadsheet-list-sf.component-Ek30J8v-.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-aqCH4P0G.mjs +805 -0
- package/fesm2022/{wuic-framework-lib-workflow-designer.component-5xtQ7-6z.mjs → wuic-framework-lib-workflow-designer.component-DMM8BeXm.mjs} +180 -150
- package/fesm2022/{wuic-framework-lib-wuic-framework-lib-3TZ-j-yL.mjs → wuic-framework-lib-wuic-framework-lib-dfLTqam5.mjs} +6761 -2347
- package/fesm2022/wuic-framework-lib.mjs +1 -1
- package/package.json +4 -2
- package/styles/_wuic-tokens.scss +82 -0
- package/styles/styles.scss +160 -0
- package/types/wuic-framework-lib.d.ts +4264 -2691
- package/fesm2022/wuic-framework-lib-report-designer.component-ZvB46-LV.mjs +0 -194
|
@@ -2,6 +2,25 @@ import * as THREE from 'three';
|
|
|
2
2
|
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js';
|
|
3
3
|
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js';
|
|
4
4
|
|
|
5
|
+
/** Estensioni accettate dall'import (allineate all'allowlist upload backend, F4). */
|
|
6
|
+
const SCENE3D_ASSET_EXTENSIONS = {
|
|
7
|
+
'.gltf': 'gltf',
|
|
8
|
+
'.glb': 'glb',
|
|
9
|
+
'.obj': 'obj',
|
|
10
|
+
'.fbx': 'fbx',
|
|
11
|
+
'.stl': 'stl',
|
|
12
|
+
'.dae': 'dae',
|
|
13
|
+
};
|
|
14
|
+
/** Risolve il formato asset da un filename, o null se l'estensione non e' supportata. */
|
|
15
|
+
function resolveAssetFormat(fileName) {
|
|
16
|
+
const dot = fileName.lastIndexOf('.');
|
|
17
|
+
if (dot < 0) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const ext = fileName.slice(dot).toLowerCase();
|
|
21
|
+
return SCENE3D_ASSET_EXTENSIONS[ext] ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
5
24
|
/**
|
|
6
25
|
* Physics (iter2) — wrapper Rapier per il Scene3D designer/viewer.
|
|
7
26
|
*
|
|
@@ -245,7 +264,7 @@ function serializeScene(root) {
|
|
|
245
264
|
// subito dopo, mantenendo la scena runtime intatta.
|
|
246
265
|
const detached = [];
|
|
247
266
|
root.traverse((obj) => {
|
|
248
|
-
if (obj !== root && obj.userData
|
|
267
|
+
if (obj !== root && obj.userData?.[WUIC_TRANSIENT_FLAG] === true && obj.parent) {
|
|
249
268
|
detached.push({ parent: obj.parent, child: obj });
|
|
250
269
|
}
|
|
251
270
|
});
|
|
@@ -264,7 +283,7 @@ function serializeScene(root) {
|
|
|
264
283
|
}
|
|
265
284
|
const bindings = {};
|
|
266
285
|
root.traverse((obj) => {
|
|
267
|
-
if (obj.userData
|
|
286
|
+
if (obj.userData?.[WUIC_TRANSIENT_FLAG] === true) {
|
|
268
287
|
return;
|
|
269
288
|
}
|
|
270
289
|
const binding = obj.userData ? obj.userData[WUIC_BINDING_KEY] : undefined;
|
|
@@ -320,7 +339,8 @@ const WUIC_CAROUSEL_BASE = 'wuicCarouselBase';
|
|
|
320
339
|
const WUIC_CAROUSEL_ARROW = 'wuicCarouselArrow';
|
|
321
340
|
function defaultCarouselConfig() {
|
|
322
341
|
return {
|
|
323
|
-
visible: 3, radius: 8, slotAngleDeg: 22, itemY: 0.9,
|
|
342
|
+
visible: 3, radius: 8, slotAngleDeg: 22, itemY: 0.9, meshYOffset: 0,
|
|
343
|
+
meshRotX: 0, meshRotY: 0, meshRotZ: 0,
|
|
324
344
|
focusScale: 1.35, sideScale: 0.75,
|
|
325
345
|
showBase: true, baseColor: '#3b4252', baseSize: 0.66,
|
|
326
346
|
baseMetalness: 0.15, baseRoughness: 0.4, baseClearcoat: 0.85,
|
|
@@ -370,9 +390,17 @@ function arrangeCarousel(items, cfg, s) {
|
|
|
370
390
|
const halfMax = Math.max(1e-6, (visible - 1) / 2);
|
|
371
391
|
const radius = Math.max(0.5, n$1(cfg.radius, 8));
|
|
372
392
|
const step = THREE.MathUtils.degToRad(n$1(cfg.slotAngleDeg, 22));
|
|
373
|
-
|
|
393
|
+
// itemY + meshYOffset: l'offset sposta SOLO i mesh su/giu' rispetto al disco
|
|
394
|
+
// (il disco/base in buildCarouselDecor usa itemY "puro", quindi resta fermo).
|
|
395
|
+
const itemY = n$1(cfg.itemY, 0.9) + n$1(cfg.meshYOffset, 0);
|
|
374
396
|
const focus = n$1(cfg.focusScale, 1.35);
|
|
375
397
|
const side = n$1(cfg.sideScale, 0.75);
|
|
398
|
+
// Rotazione base utente (gradi->rad) composta col coverflow via quaternione.
|
|
399
|
+
const rx = THREE.MathUtils.degToRad(n$1(cfg.meshRotX, 0));
|
|
400
|
+
const ry = THREE.MathUtils.degToRad(n$1(cfg.meshRotY, 0));
|
|
401
|
+
const rz = THREE.MathUtils.degToRad(n$1(cfg.meshRotZ, 0));
|
|
402
|
+
const hasUserRot = rx !== 0 || ry !== 0 || rz !== 0;
|
|
403
|
+
const userQuat = new THREE.Quaternion().setFromEuler(new THREE.Euler(rx, ry, rz));
|
|
376
404
|
for (let i = 0; i < items.length; i++) {
|
|
377
405
|
const it = items[i];
|
|
378
406
|
const slot = i - s;
|
|
@@ -384,7 +412,14 @@ function arrangeCarousel(items, cfg, s) {
|
|
|
384
412
|
const a = slot * step;
|
|
385
413
|
// Arco: centro in (0,itemY,0); i lati curvano verso -Z (lontano dalla camera).
|
|
386
414
|
it.position.set(Math.sin(a) * radius, itemY, Math.cos(a) * radius - radius);
|
|
387
|
-
|
|
415
|
+
// Coverflow (Y verso il fronte) composto con la rotazione base utente:
|
|
416
|
+
// coverflow * user -> il modello ruota di user, poi l'item si orienta al fronte.
|
|
417
|
+
if (hasUserRot) {
|
|
418
|
+
it.quaternion.setFromEuler(new THREE.Euler(0, -a, 0)).multiply(userQuat);
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
it.rotation.set(0, -a, 0);
|
|
422
|
+
}
|
|
388
423
|
const t = Math.min(Math.abs(slot) / halfMax, 1);
|
|
389
424
|
it.scale.setScalar(Math.max(0.01, focus + (side - focus) * t));
|
|
390
425
|
}
|
|
@@ -990,7 +1025,7 @@ function buildSurface(group, rows, cfg, route, deps) {
|
|
|
990
1025
|
if (!cfg.rowColumn || !cfg.colColumn || !cfg.valueColumn || rows.length === 0) {
|
|
991
1026
|
return 0;
|
|
992
1027
|
}
|
|
993
|
-
const uniq = (col) => Array.from(new Set(rows.map((r) => String(deps.unwrap(r[col]) ?? '')))).sort();
|
|
1028
|
+
const uniq = (col) => Array.from(new Set(rows.map((r) => String(deps.unwrap(r[col]) ?? '')))).sort((a, b) => a.localeCompare(b));
|
|
994
1029
|
const rlabels = uniq(cfg.rowColumn);
|
|
995
1030
|
const clabels = uniq(cfg.colColumn);
|
|
996
1031
|
const vals = rows.map((r) => n(deps.unwrap(r[cfg.valueColumn])));
|
|
@@ -1275,7 +1310,7 @@ function buildWarehouse(group, rows, cfg, route, deps) {
|
|
|
1275
1310
|
if (!cfg.aisleColumn || !cfg.shelfColumn || rows.length === 0) {
|
|
1276
1311
|
return 0;
|
|
1277
1312
|
}
|
|
1278
|
-
const uniq = (col) => Array.from(new Set(rows.map((r) => String(deps.unwrap(r[col]) ?? '')))).sort();
|
|
1313
|
+
const uniq = (col) => Array.from(new Set(rows.map((r) => String(deps.unwrap(r[col]) ?? '')))).sort((a, b) => a.localeCompare(b));
|
|
1279
1314
|
const aisles = uniq(cfg.aisleColumn);
|
|
1280
1315
|
const shelves = uniq(cfg.shelfColumn);
|
|
1281
1316
|
const qtys = rows.map((r) => n(deps.unwrap(r[cfg.qtyColumn])));
|
|
@@ -1320,7 +1355,7 @@ function buildWarehouse(group, rows, cfg, route, deps) {
|
|
|
1320
1355
|
// Kanban 3D, Calendar-wall, Stacked bars
|
|
1321
1356
|
// ===========================================================================
|
|
1322
1357
|
function uniqSorted(rows, col, deps) {
|
|
1323
|
-
return Array.from(new Set(rows.map((r) => String(deps.unwrap(r[col]) ?? '')))).sort();
|
|
1358
|
+
return Array.from(new Set(rows.map((r) => String(deps.unwrap(r[col]) ?? '')))).sort((a, b) => a.localeCompare(b));
|
|
1324
1359
|
}
|
|
1325
1360
|
function defaultKanbanConfig() { return { statusColumn: '', labelColumn: '', cardWidth: 2, showLabels: true }; }
|
|
1326
1361
|
/** Kanban: una colonna per stato; card impilate (una per record). */
|
|
@@ -1369,7 +1404,7 @@ function buildCalendar(group, rows, cfg, route, deps) {
|
|
|
1369
1404
|
if (!cfg.dateColumn || rows.length === 0) {
|
|
1370
1405
|
return 0;
|
|
1371
1406
|
}
|
|
1372
|
-
const items = rows.map((r) => ({ rec: r, d: new Date(String(deps.unwrap(r[cfg.dateColumn]))), v: cfg.valueColumn ? n(deps.unwrap(r[cfg.valueColumn])) : 1 })).filter((x) => !isNaN(x.d.getTime()));
|
|
1407
|
+
const items = rows.map((r) => ({ rec: r, d: new Date(String(deps.unwrap(r[cfg.dateColumn]))), v: cfg.valueColumn ? n(deps.unwrap(r[cfg.valueColumn])) : 1 })).filter((x) => !Number.isNaN(Number(x.d.getTime())));
|
|
1373
1408
|
if (items.length === 0) {
|
|
1374
1409
|
return 0;
|
|
1375
1410
|
}
|
|
@@ -1480,11 +1515,53 @@ const helvetikerFont = { "glyphs": { "ο": { "x_min": 0, "x_max": 712, "ha": 815
|
|
|
1480
1515
|
const WUIC_MESH_REPEATER_KEY = 'wuicMeshRepeater';
|
|
1481
1516
|
/** Nome della mesh figlia generata (per riconoscerle e ripulirle). */
|
|
1482
1517
|
const WUIC_REPEATER_CHILD = 'wuicRepeaterItem';
|
|
1518
|
+
/** userData key: record (unwrappato) bindato alla mesh figlia, per i callback
|
|
1519
|
+
* di interazione (click/dblclick/rightclick). Transient, non serializzato. */
|
|
1520
|
+
const WUIC_REPEATER_RECORD = 'wuicRepeaterRecord';
|
|
1483
1521
|
/** Nome del placeholder mostrato quando il repeater non è ancora bindato. */
|
|
1484
1522
|
const WUIC_REPEATER_PLACEHOLDER = 'wuicRepeaterPlaceholder';
|
|
1485
1523
|
function defaultRepeaterTextureApply() {
|
|
1486
1524
|
return { sourceKind: 'auto', wrap: 'stretch', repeatX: 1, repeatY: 1, textBg: '#334155', textFg: '#ffffff' };
|
|
1487
1525
|
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Normalizza un asset 3D importato (glTF/OBJ/STL, unità native arbitrarie) a una
|
|
1528
|
+
* dimensione ~1 unità WUIC, centrato su x/z e con la BASE appoggiata a y=0:
|
|
1529
|
+
* avvolge l'oggetto in due Group, centra l'inner sul suo bounding box (asse Y
|
|
1530
|
+
* ancorato al fondo) e scala l'inner a `1/maxDim`.
|
|
1531
|
+
* Necessario perché nel repeater/carousel modelli con scale native diverse
|
|
1532
|
+
* (es. Avocado ~5cm vs BoomBox ~50cm) apparirebbero enormemente disomogenei.
|
|
1533
|
+
* L'ancoraggio della base (non il centro) evita che, aumentando lo scale, il
|
|
1534
|
+
* modello affondi nel disco del carousel: cresce verso l'alto, la base resta ferma.
|
|
1535
|
+
*/
|
|
1536
|
+
function normalizeAssetToUnit(obj) {
|
|
1537
|
+
try {
|
|
1538
|
+
const box = new THREE.Box3().setFromObject(obj);
|
|
1539
|
+
if (box.isEmpty()) {
|
|
1540
|
+
return obj;
|
|
1541
|
+
}
|
|
1542
|
+
const size = box.getSize(new THREE.Vector3());
|
|
1543
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
1544
|
+
const maxDim = Math.max(size.x, size.y, size.z) || 1;
|
|
1545
|
+
// Centra x/z, ancora la base (box.min.y) a y=0: lo scale del carousel cresce
|
|
1546
|
+
// verso l'alto senza spingere il modello sotto il piano del disco.
|
|
1547
|
+
obj.position.set(-center.x, -box.min.y, -center.z);
|
|
1548
|
+
// Nesting a DUE livelli: l'INNER porta la normalizzazione (1/maxDim), che va
|
|
1549
|
+
// PROTETTA — il carousel (arrangeCarousel `it.scale.setScalar`) e il layout
|
|
1550
|
+
// data-driven (fm.scale) fanno `scale.setScalar` sul nodo TOP e altrimenti
|
|
1551
|
+
// sovrascriverebbero la normalizzazione, riportando il modello alla dimensione
|
|
1552
|
+
// nativa (tiny/disomogeneo). L'OUTER resta a scala 1 e assorbe lo scale del
|
|
1553
|
+
// repeater/carousel: dimensione finale = scaleCarousel x (1/maxDim) x modello.
|
|
1554
|
+
const inner = new THREE.Group();
|
|
1555
|
+
inner.add(obj);
|
|
1556
|
+
inner.scale.setScalar(1 / maxDim);
|
|
1557
|
+
const outer = new THREE.Group();
|
|
1558
|
+
outer.add(inner);
|
|
1559
|
+
return outer;
|
|
1560
|
+
}
|
|
1561
|
+
catch {
|
|
1562
|
+
return obj;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1488
1565
|
/** Direzioni di default (tutte positive = comportamento storico). */
|
|
1489
1566
|
function defaultRepeaterAxisDir() { return { x: 1, y: 1, z: 1 }; }
|
|
1490
1567
|
/** Config di default per un repeater appena droppato (route vuota = placeholder). */
|
|
@@ -1537,7 +1614,7 @@ function buildUploadSrc(col, route, pkName, record, rawValue, app, unwrap) {
|
|
|
1537
1614
|
}
|
|
1538
1615
|
let pk = unwrap(record ? record[pkName] : null);
|
|
1539
1616
|
if (pk && typeof pk === 'object' && 'value' in pk) {
|
|
1540
|
-
pk = pk.value;
|
|
1617
|
+
pk = (pk).value;
|
|
1541
1618
|
}
|
|
1542
1619
|
const pkStr = pk == null ? '' : String(pk);
|
|
1543
1620
|
if (col.isDBUpload) {
|
|
@@ -1634,7 +1711,7 @@ function remap(v, inMin, inMax, outMin, outMax) {
|
|
|
1634
1711
|
* Rigenera le mesh figlie del repeater dai record. Pura rispetto al three:
|
|
1635
1712
|
* usa solo le factory/parser passati in `deps`.
|
|
1636
1713
|
*/
|
|
1637
|
-
function regenerateRepeater(group, config, rows, deps) {
|
|
1714
|
+
async function regenerateRepeater(group, config, rows, deps) {
|
|
1638
1715
|
clearRepeaterChildren(group);
|
|
1639
1716
|
if (!config.route || !Array.isArray(rows) || rows.length === 0) {
|
|
1640
1717
|
if (!config.route) {
|
|
@@ -1689,21 +1766,67 @@ function regenerateRepeater(group, config, rows, deps) {
|
|
|
1689
1766
|
// il loop da arrangeCarousel/buildCarouselDecor, non dal layout per-item.
|
|
1690
1767
|
const isCarousel = config.layout?.mode === 'carousel';
|
|
1691
1768
|
const created = [];
|
|
1769
|
+
// Mesh ASSET per-record: pre-carica (dedup + cache) gli asset distinti PRIMA del
|
|
1770
|
+
// loop sincrono, poi ogni record clona il template. Fallback a primitiva se il
|
|
1771
|
+
// load fallisce o la colonna e' vuota. (Il loop resta sync: gli await stanno qui.)
|
|
1772
|
+
const assetCol = (config.meshSource?.mode === 'asset' && config.meshSource.column) ? config.meshSource.column : '';
|
|
1773
|
+
const assetTemplates = new Map();
|
|
1774
|
+
if (assetCol && deps.loadAsset && deps.resolveAssetSrc) {
|
|
1775
|
+
const urls = new Set();
|
|
1776
|
+
for (const record of limited) {
|
|
1777
|
+
const raw = String(deps.unwrap(record[assetCol]) ?? '').trim();
|
|
1778
|
+
if (!raw) {
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
const url = deps.resolveAssetSrc(record, raw);
|
|
1782
|
+
if (url) {
|
|
1783
|
+
urls.add(url);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
await Promise.all([...urls].map(async (u) => {
|
|
1787
|
+
try {
|
|
1788
|
+
const tpl = await deps.loadAsset(u);
|
|
1789
|
+
if (tpl) {
|
|
1790
|
+
assetTemplates.set(u, tpl);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
catch { /* skip: fallback primitiva */ }
|
|
1794
|
+
}));
|
|
1795
|
+
}
|
|
1692
1796
|
let generated = 0;
|
|
1693
1797
|
limited.forEach((record, i) => {
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
const raw = String(deps.unwrap(record[
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1798
|
+
let mesh;
|
|
1799
|
+
if (assetCol && deps.resolveAssetSrc) {
|
|
1800
|
+
// Mesh dall'asset della colonna (clone del template pre-caricato).
|
|
1801
|
+
const raw = String(deps.unwrap(record[assetCol]) ?? '').trim();
|
|
1802
|
+
const url = raw ? deps.resolveAssetSrc(record, raw) : '';
|
|
1803
|
+
const tpl = url ? assetTemplates.get(url) : undefined;
|
|
1804
|
+
mesh = tpl ? tpl.clone(true) : (deps.createMesh('box') || undefined);
|
|
1805
|
+
// Texture per-record su asset: clona i material del clone per non "sporcare"
|
|
1806
|
+
// gli altri (i clone condividono i material del template).
|
|
1807
|
+
if (mesh && fm.texture) {
|
|
1808
|
+
mesh.traverse((o) => {
|
|
1809
|
+
if (o.isMesh && o.material) {
|
|
1810
|
+
o.material = Array.isArray(o.material) ? o.material.map((m) => m.clone()) : o.material.clone();
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
else {
|
|
1816
|
+
// Tipo mesh: fisso oppure dalla colonna del record (fallback box).
|
|
1817
|
+
let type = 'box';
|
|
1818
|
+
if (config.meshSource?.mode === 'column' && config.meshSource.column) {
|
|
1819
|
+
const raw = String(deps.unwrap(record[config.meshSource.column]) ?? '').trim();
|
|
1820
|
+
type = raw || 'box';
|
|
1821
|
+
}
|
|
1822
|
+
else if (config.meshSource?.type) {
|
|
1823
|
+
type = config.meshSource.type;
|
|
1824
|
+
}
|
|
1825
|
+
// Testo 3D: contenuto dalla colonna `text` del record (canvas -> plane).
|
|
1826
|
+
mesh = (type === 'text3d' && deps.createTextMesh)
|
|
1827
|
+
? deps.createTextMesh(String(deps.unwrap(record[fm.text || '']) ?? ''))
|
|
1828
|
+
: (deps.createMesh(type) || deps.createMesh('box') || undefined);
|
|
1829
|
+
}
|
|
1707
1830
|
if (!mesh) {
|
|
1708
1831
|
return;
|
|
1709
1832
|
}
|
|
@@ -1748,6 +1871,14 @@ function regenerateRepeater(group, config, rows, deps) {
|
|
|
1748
1871
|
mesh.name = WUIC_REPEATER_CHILD;
|
|
1749
1872
|
mesh.userData[WUIC_TRANSIENT_FLAG] = true;
|
|
1750
1873
|
mesh.userData[WUIC_BINDING_KEY] = { route: config.route, recordId: deps.resolveRecordId(record) };
|
|
1874
|
+
// Record unwrappato sull'item, per i callback di interazione (ctx.record).
|
|
1875
|
+
{
|
|
1876
|
+
const plain = {};
|
|
1877
|
+
for (const k of Object.keys(record || {})) {
|
|
1878
|
+
plain[k] = deps.unwrap(record[k]);
|
|
1879
|
+
}
|
|
1880
|
+
mesh.userData[WUIC_REPEATER_RECORD] = plain;
|
|
1881
|
+
}
|
|
1751
1882
|
deps.applyShadow(mesh);
|
|
1752
1883
|
group.add(mesh);
|
|
1753
1884
|
created.push(mesh);
|
|
@@ -1770,7 +1901,13 @@ function axisVec(axes, vals, dir) {
|
|
|
1770
1901
|
const v = new THREE.Vector3(0, 0, 0);
|
|
1771
1902
|
for (let i = 0; i < axes.length && i < vals.length; i++) {
|
|
1772
1903
|
const a = axes[i];
|
|
1773
|
-
|
|
1904
|
+
let s = dir?.z;
|
|
1905
|
+
if (a === 'x') {
|
|
1906
|
+
s = dir?.x;
|
|
1907
|
+
}
|
|
1908
|
+
else if (a === 'y') {
|
|
1909
|
+
s = dir?.y;
|
|
1910
|
+
}
|
|
1774
1911
|
const val = vals[i] * (s === -1 ? -1 : 1);
|
|
1775
1912
|
if (a === 'x') {
|
|
1776
1913
|
v.x = val;
|
|
@@ -1875,9 +2012,16 @@ function applyRepeaterTextureTo(mesh, value, opts, cache, loadTexture, onReady)
|
|
|
1875
2012
|
if (!matAny || !('map' in matAny)) {
|
|
1876
2013
|
return;
|
|
1877
2014
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2015
|
+
let kind;
|
|
2016
|
+
if (opts.sourceKind === 'text') {
|
|
2017
|
+
kind = 'text';
|
|
2018
|
+
}
|
|
2019
|
+
else if (opts.sourceKind === 'image') {
|
|
2020
|
+
kind = 'image';
|
|
2021
|
+
}
|
|
2022
|
+
else {
|
|
2023
|
+
kind = looksLikeImageRef(value) ? 'image' : 'text';
|
|
2024
|
+
}
|
|
1881
2025
|
const key = `${kind}|${value}|${opts.textBg}|${opts.textFg}|${opts.wrap}|${opts.repeatX}|${opts.repeatY}`;
|
|
1882
2026
|
let tex = cache.get(key);
|
|
1883
2027
|
if (!tex) {
|
|
@@ -1931,6 +2075,442 @@ function makeTextSpriteMesh(text) {
|
|
|
1931
2075
|
}
|
|
1932
2076
|
}
|
|
1933
2077
|
|
|
2078
|
+
/**
|
|
2079
|
+
* Helper per i callback di interazione del Mesh Repeater (click/dblclick/rightclick).
|
|
2080
|
+
*
|
|
2081
|
+
* - `runRepeaterCallback(js, ctx)`: esegue il corpo JS dell'app-builder con `ctx`
|
|
2082
|
+
* (record, event, mesh, scene, camera, THREE, wuic). Sandbox = `new Function`:
|
|
2083
|
+
* e' codice dell'admin che costruisce l'app (stesso modello di
|
|
2084
|
+
* validation/suggest callback), non input utente.
|
|
2085
|
+
* - `showItemPopup(opts, deps)`: mostra un popup "campi a sinistra + mesh a destra".
|
|
2086
|
+
* `opts.mode`: 1|'overlay' = overlay HTML con preview 3D live; 2|'panel3d' =
|
|
2087
|
+
* pannello 3D dentro la scena (billboard verso la camera). `opts.fields`: nomi
|
|
2088
|
+
* dei campi del record da mostrare (default: primi campi). `opts.width`/`opts.height`:
|
|
2089
|
+
* dimensione popup (number=px, string='50%'|'420px'; solo overlay). `opts.panelBg`:
|
|
2090
|
+
* colore sfondo pannello. `opts.swap`: inverte i lati (mesh sx, campi dx).
|
|
2091
|
+
* `opts.maximizable`: pulsante "ingrandisci" oltre alla chiusura (solo overlay).
|
|
2092
|
+
* `opts.verticalPosition` ('top'|'center'|'bottom') / `opts.horizontalPosition`
|
|
2093
|
+
* ('left'|'center'|'right'): ancoraggio del popup nel viewport (solo overlay).
|
|
2094
|
+
* `opts.autorotateMesh`: false = rotazione col mouse invece dell'auto-rotazione
|
|
2095
|
+
* dell'anteprima (solo overlay).
|
|
2096
|
+
*/
|
|
2097
|
+
// ── Stato singleton (un popup alla volta) ────────────────────────────────
|
|
2098
|
+
let overlayEl = null;
|
|
2099
|
+
let overlayRAF = 0;
|
|
2100
|
+
let overlayRenderer = null;
|
|
2101
|
+
let panel3d = null;
|
|
2102
|
+
let panelDeps = null;
|
|
2103
|
+
/** Esegue il callback JS con `ctx`. Errori loggati, mai propagati (non rompe il render). */
|
|
2104
|
+
function runRepeaterCallback(js, ctx) {
|
|
2105
|
+
const src = (js || '').trim();
|
|
2106
|
+
if (!src) {
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
try {
|
|
2110
|
+
// eslint-disable-next-line no-new-func
|
|
2111
|
+
const fn = new Function('ctx', 'record', 'wuic', 'THREE', src);
|
|
2112
|
+
fn(ctx, ctx.record, ctx.wuic, THREE);
|
|
2113
|
+
}
|
|
2114
|
+
catch (e) {
|
|
2115
|
+
console.error('[Scene3D] callback repeater fallito', e);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
/** Chiude qualunque popup aperto (overlay + panel3d). */
|
|
2119
|
+
function closeItemPopup() {
|
|
2120
|
+
if (overlayRAF) {
|
|
2121
|
+
cancelAnimationFrame(overlayRAF);
|
|
2122
|
+
overlayRAF = 0;
|
|
2123
|
+
}
|
|
2124
|
+
if (overlayRenderer) {
|
|
2125
|
+
try {
|
|
2126
|
+
overlayRenderer.dispose();
|
|
2127
|
+
}
|
|
2128
|
+
catch { /* noop */ }
|
|
2129
|
+
overlayRenderer = null;
|
|
2130
|
+
}
|
|
2131
|
+
if (overlayEl?.parentElement) {
|
|
2132
|
+
overlayEl.parentElement.removeChild(overlayEl);
|
|
2133
|
+
}
|
|
2134
|
+
overlayEl = null;
|
|
2135
|
+
if (panel3d?.parent) {
|
|
2136
|
+
panel3d.parent.remove(panel3d);
|
|
2137
|
+
}
|
|
2138
|
+
if (panel3d) {
|
|
2139
|
+
disposeObject(panel3d);
|
|
2140
|
+
}
|
|
2141
|
+
panel3d = null;
|
|
2142
|
+
if (panelDeps?.onFrame) {
|
|
2143
|
+
panelDeps.onFrame(null);
|
|
2144
|
+
}
|
|
2145
|
+
panelDeps = null;
|
|
2146
|
+
}
|
|
2147
|
+
/** Dispatcher per modalita'. */
|
|
2148
|
+
function showItemPopup(opts, deps) {
|
|
2149
|
+
closeItemPopup();
|
|
2150
|
+
const mode = (opts.mode === 2 || opts.mode === 'panel3d') ? 'panel3d' : 'overlay';
|
|
2151
|
+
if (mode === 'panel3d') {
|
|
2152
|
+
showPanel3d(opts, deps);
|
|
2153
|
+
}
|
|
2154
|
+
else {
|
|
2155
|
+
showOverlay(opts, deps);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
// ── util: righe campi (label:value) dal record ───────────────────────────
|
|
2159
|
+
function fieldRows(opts) {
|
|
2160
|
+
const rec = opts.record || {};
|
|
2161
|
+
const names = (opts.fields?.length) ? opts.fields : Object.keys(rec).slice(0, 8);
|
|
2162
|
+
return names.map((n) => ({ label: String(n), value: fmt(rec[n]) }));
|
|
2163
|
+
}
|
|
2164
|
+
function fmt(v) {
|
|
2165
|
+
if (v == null) {
|
|
2166
|
+
return '';
|
|
2167
|
+
}
|
|
2168
|
+
if (typeof v === 'object' && 'value' in v) {
|
|
2169
|
+
v = (v).value;
|
|
2170
|
+
}
|
|
2171
|
+
if (typeof v === 'number') {
|
|
2172
|
+
return String(v);
|
|
2173
|
+
}
|
|
2174
|
+
return String(v);
|
|
2175
|
+
}
|
|
2176
|
+
/** Dimensione CSS: number → px, string → as-is ('50%', '420px'). */
|
|
2177
|
+
function dim(v) {
|
|
2178
|
+
return typeof v === 'number' ? `${v}px` : String(v);
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Rotazione manuale di `target` trascinando il mouse su `host` (orbit-like, come
|
|
2182
|
+
* nel designer): drag orizzontale → yaw, verticale → pitch (clampato a ±90°).
|
|
2183
|
+
* I listener globali vivono solo durante il drag; il `pointerdown` resta su
|
|
2184
|
+
* `host`, che viene rimosso dal DOM alla chiusura del popup (nessun leak).
|
|
2185
|
+
*/
|
|
2186
|
+
function enableDragRotate(host, target) {
|
|
2187
|
+
host.style.cursor = 'grab';
|
|
2188
|
+
let dragging = false, px = 0, py = 0;
|
|
2189
|
+
const onMove = (e) => {
|
|
2190
|
+
if (!dragging) {
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
const dx = e.clientX - px, dy = e.clientY - py;
|
|
2194
|
+
px = e.clientX;
|
|
2195
|
+
py = e.clientY;
|
|
2196
|
+
target.rotation.y += dx * 0.01;
|
|
2197
|
+
target.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, target.rotation.x + dy * 0.01));
|
|
2198
|
+
};
|
|
2199
|
+
const onUp = () => {
|
|
2200
|
+
dragging = false;
|
|
2201
|
+
host.style.cursor = 'grab';
|
|
2202
|
+
globalThis.removeEventListener('pointermove', onMove);
|
|
2203
|
+
globalThis.removeEventListener('pointerup', onUp);
|
|
2204
|
+
};
|
|
2205
|
+
host.addEventListener('pointerdown', (e) => {
|
|
2206
|
+
dragging = true;
|
|
2207
|
+
px = e.clientX;
|
|
2208
|
+
py = e.clientY;
|
|
2209
|
+
host.style.cursor = 'grabbing';
|
|
2210
|
+
globalThis.addEventListener('pointermove', onMove);
|
|
2211
|
+
globalThis.addEventListener('pointerup', onUp);
|
|
2212
|
+
e.preventDefault();
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Zoom col mouse-wheel sull'anteprima: dolly della camera lungo il suo asse
|
|
2217
|
+
* verso l'origine (dove sta il mesh), con distanza clampata. `passive:false` +
|
|
2218
|
+
* preventDefault per non scrollare la pagina sotto.
|
|
2219
|
+
*/
|
|
2220
|
+
function enableWheelZoom(host, camera) {
|
|
2221
|
+
host.addEventListener('wheel', (e) => {
|
|
2222
|
+
e.preventDefault();
|
|
2223
|
+
const factor = e.deltaY > 0 ? 1.1 : 0.9;
|
|
2224
|
+
const dist = Math.max(1.2, Math.min(12, camera.position.length() * factor));
|
|
2225
|
+
camera.position.normalize().multiplyScalar(dist);
|
|
2226
|
+
camera.lookAt(0, 0, 0);
|
|
2227
|
+
}, { passive: false });
|
|
2228
|
+
}
|
|
2229
|
+
// ── util: clone del mesh normalizzato per il preview (centra + scala a ~1.6) ──
|
|
2230
|
+
function fitClone(mesh) {
|
|
2231
|
+
const clone = mesh.clone(true);
|
|
2232
|
+
clone.position.set(0, 0, 0);
|
|
2233
|
+
clone.rotation.set(0, 0, 0);
|
|
2234
|
+
clone.scale.set(1, 1, 1);
|
|
2235
|
+
const box = new THREE.Box3().setFromObject(clone);
|
|
2236
|
+
if (!box.isEmpty()) {
|
|
2237
|
+
const size = box.getSize(new THREE.Vector3());
|
|
2238
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
2239
|
+
const maxDim = Math.max(size.x, size.y, size.z) || 1;
|
|
2240
|
+
const wrap = new THREE.Group();
|
|
2241
|
+
clone.position.sub(center);
|
|
2242
|
+
wrap.add(clone);
|
|
2243
|
+
wrap.scale.setScalar(1.6 / maxDim);
|
|
2244
|
+
return wrap;
|
|
2245
|
+
}
|
|
2246
|
+
return clone;
|
|
2247
|
+
}
|
|
2248
|
+
// ── Modalita' 1: overlay HTML + preview 3D live ──────────────────────────
|
|
2249
|
+
function showOverlay(opts, deps) {
|
|
2250
|
+
const host = deps.canvas.parentElement || document.body;
|
|
2251
|
+
if (getComputedStyle(host).position === 'static') {
|
|
2252
|
+
host.style.position = 'relative';
|
|
2253
|
+
}
|
|
2254
|
+
const bg = opts.panelBg || 'rgba(20,24,33,0.94)';
|
|
2255
|
+
const el = document.createElement('div');
|
|
2256
|
+
el.className = 'wuic-scene3d-item-popup';
|
|
2257
|
+
const css = [
|
|
2258
|
+
'position:absolute', 'z-index:30',
|
|
2259
|
+
'display:flex', 'gap:14px', 'padding:14px 16px',
|
|
2260
|
+
`background:${bg}`, 'color:#e5e9f0',
|
|
2261
|
+
'border:1px solid rgba(255,255,255,0.12)', 'border-radius:10px',
|
|
2262
|
+
'box-shadow:0 8px 30px rgba(0,0,0,0.45)', 'backdrop-filter:blur(4px)',
|
|
2263
|
+
'font:13px/1.5 system-ui,Segoe UI,sans-serif', 'box-sizing:border-box'
|
|
2264
|
+
];
|
|
2265
|
+
// Dimensioni: number → px, string → CSS ('50%','420px'). Default: auto (max 520px).
|
|
2266
|
+
css.push(opts.width != null ? `width:${dim(opts.width)}` : 'max-width:520px');
|
|
2267
|
+
if (opts.height != null) {
|
|
2268
|
+
css.push(`height:${dim(opts.height)}`);
|
|
2269
|
+
}
|
|
2270
|
+
el.style.cssText = css.join(';');
|
|
2271
|
+
// Ancoraggio nel viewport: vertical/horizontalPosition → offset 16px + transform
|
|
2272
|
+
// di centratura sull'asse richiesto. `place()` e' riusato dal ripristino maximize.
|
|
2273
|
+
const vpos = opts.verticalPosition || 'top';
|
|
2274
|
+
const hpos = opts.horizontalPosition || 'right';
|
|
2275
|
+
const place = () => {
|
|
2276
|
+
el.style.top = el.style.right = el.style.bottom = el.style.left = '';
|
|
2277
|
+
const tx = [];
|
|
2278
|
+
if (hpos === 'left') {
|
|
2279
|
+
el.style.left = '16px';
|
|
2280
|
+
}
|
|
2281
|
+
else if (hpos === 'center') {
|
|
2282
|
+
el.style.left = '50%';
|
|
2283
|
+
tx.push('translateX(-50%)');
|
|
2284
|
+
}
|
|
2285
|
+
else {
|
|
2286
|
+
el.style.right = '16px';
|
|
2287
|
+
}
|
|
2288
|
+
if (vpos === 'bottom') {
|
|
2289
|
+
el.style.bottom = '16px';
|
|
2290
|
+
}
|
|
2291
|
+
else if (vpos === 'center') {
|
|
2292
|
+
el.style.top = '50%';
|
|
2293
|
+
tx.push('translateY(-50%)');
|
|
2294
|
+
}
|
|
2295
|
+
else {
|
|
2296
|
+
el.style.top = '16px';
|
|
2297
|
+
}
|
|
2298
|
+
el.style.transform = tx.join(' ');
|
|
2299
|
+
};
|
|
2300
|
+
place();
|
|
2301
|
+
// campi (colonna testo)
|
|
2302
|
+
const left = document.createElement('div');
|
|
2303
|
+
left.style.cssText = 'min-width:150px;max-width:280px;display:flex;flex-direction:column;gap:2px;overflow:auto';
|
|
2304
|
+
if (opts.title) {
|
|
2305
|
+
const t = document.createElement('div');
|
|
2306
|
+
t.textContent = opts.title;
|
|
2307
|
+
t.style.cssText = 'font-weight:700;font-size:14px;margin-bottom:6px;color:#9db4ff';
|
|
2308
|
+
left.appendChild(t);
|
|
2309
|
+
}
|
|
2310
|
+
for (const r of fieldRows(opts)) {
|
|
2311
|
+
const row = document.createElement('div');
|
|
2312
|
+
row.style.cssText = 'display:flex;justify-content:space-between;gap:12px;border-bottom:1px solid rgba(255,255,255,0.06);padding:2px 0';
|
|
2313
|
+
const lab = document.createElement('span');
|
|
2314
|
+
lab.textContent = r.label;
|
|
2315
|
+
lab.style.cssText = 'opacity:0.65';
|
|
2316
|
+
const val = document.createElement('span');
|
|
2317
|
+
val.textContent = r.value;
|
|
2318
|
+
val.style.cssText = 'font-weight:600;text-align:right;word-break:break-word';
|
|
2319
|
+
row.appendChild(lab);
|
|
2320
|
+
row.appendChild(val);
|
|
2321
|
+
left.appendChild(row);
|
|
2322
|
+
}
|
|
2323
|
+
// preview 3D live: riempie lo spazio (min 200px, cresce col popup/maximize).
|
|
2324
|
+
const right = document.createElement('div');
|
|
2325
|
+
right.style.cssText = 'position:relative;flex:1 1 auto;min-width:200px;min-height:200px;align-self:stretch;border-radius:8px;overflow:hidden;background:radial-gradient(circle at 50% 35%,#2a3140,#141821)';
|
|
2326
|
+
// controlli (maximize opzionale + chiusura), sempre in alto a destra del popup
|
|
2327
|
+
const controls = document.createElement('div');
|
|
2328
|
+
controls.style.cssText = 'position:absolute;top:6px;right:8px;z-index:2;display:flex;gap:2px';
|
|
2329
|
+
const btnCss = 'background:transparent;border:none;color:#e5e9f0;line-height:1;cursor:pointer;opacity:0.8;padding:0 4px';
|
|
2330
|
+
const close = document.createElement('button');
|
|
2331
|
+
close.textContent = '×';
|
|
2332
|
+
close.setAttribute('aria-label', 'Close');
|
|
2333
|
+
close.style.cssText = btnCss + ';font-size:20px';
|
|
2334
|
+
close.onclick = () => closeItemPopup();
|
|
2335
|
+
// ordine lati: default campi→mesh; swap → mesh→campi.
|
|
2336
|
+
if (opts.swap) {
|
|
2337
|
+
el.appendChild(right);
|
|
2338
|
+
el.appendChild(left);
|
|
2339
|
+
}
|
|
2340
|
+
else {
|
|
2341
|
+
el.appendChild(left);
|
|
2342
|
+
el.appendChild(right);
|
|
2343
|
+
}
|
|
2344
|
+
// mini renderer 3D (canvas al 100% del box: la size effettiva la da' resizePreview)
|
|
2345
|
+
const r = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
2346
|
+
r.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
|
|
2347
|
+
r.domElement.style.cssText = 'width:100%;height:100%;display:block';
|
|
2348
|
+
right.appendChild(r.domElement);
|
|
2349
|
+
overlayRenderer = r;
|
|
2350
|
+
const ms = new THREE.Scene();
|
|
2351
|
+
ms.add(new THREE.AmbientLight(0xffffff, 0.85));
|
|
2352
|
+
const dl = new THREE.DirectionalLight(0xffffff, 1.25);
|
|
2353
|
+
dl.position.set(2, 3, 2.5);
|
|
2354
|
+
ms.add(dl);
|
|
2355
|
+
const mc = new THREE.PerspectiveCamera(42, 1, 0.01, 100);
|
|
2356
|
+
mc.position.set(0, 0.25, 3.1);
|
|
2357
|
+
mc.lookAt(0, 0, 0);
|
|
2358
|
+
enableWheelZoom(right, mc); // zoom con la rotella (sempre attivo)
|
|
2359
|
+
const resizePreview = () => {
|
|
2360
|
+
const w = Math.max(1, right.clientWidth), h = Math.max(1, right.clientHeight);
|
|
2361
|
+
r.setSize(w, h, false);
|
|
2362
|
+
mc.aspect = w / h;
|
|
2363
|
+
mc.updateProjectionMatrix();
|
|
2364
|
+
};
|
|
2365
|
+
// maximize: il popup riempie l'host; il ripristino torna a size/posizione base.
|
|
2366
|
+
if (opts.maximizable) {
|
|
2367
|
+
let maximized = false;
|
|
2368
|
+
const maxBtn = document.createElement('button');
|
|
2369
|
+
maxBtn.textContent = '⤢';
|
|
2370
|
+
maxBtn.setAttribute('aria-label', 'Maximize');
|
|
2371
|
+
maxBtn.style.cssText = btnCss + ';font-size:16px';
|
|
2372
|
+
maxBtn.onclick = () => {
|
|
2373
|
+
maximized = !maximized;
|
|
2374
|
+
if (maximized) {
|
|
2375
|
+
el.style.transform = '';
|
|
2376
|
+
el.style.top = el.style.right = el.style.bottom = el.style.left = '8px';
|
|
2377
|
+
el.style.width = 'auto';
|
|
2378
|
+
el.style.height = 'auto';
|
|
2379
|
+
el.style.maxWidth = 'none';
|
|
2380
|
+
maxBtn.textContent = '⤡';
|
|
2381
|
+
}
|
|
2382
|
+
else {
|
|
2383
|
+
el.style.width = opts.width != null ? dim(opts.width) : '';
|
|
2384
|
+
el.style.height = opts.height != null ? dim(opts.height) : '';
|
|
2385
|
+
el.style.maxWidth = opts.width != null ? 'none' : '520px';
|
|
2386
|
+
place();
|
|
2387
|
+
maxBtn.textContent = '⤢';
|
|
2388
|
+
}
|
|
2389
|
+
resizePreview();
|
|
2390
|
+
};
|
|
2391
|
+
controls.appendChild(maxBtn);
|
|
2392
|
+
}
|
|
2393
|
+
controls.appendChild(close);
|
|
2394
|
+
el.appendChild(controls);
|
|
2395
|
+
host.appendChild(el);
|
|
2396
|
+
overlayEl = el;
|
|
2397
|
+
resizePreview(); // prima misura dopo il layout (clientWidth valorizzato)
|
|
2398
|
+
const spin = new THREE.Group();
|
|
2399
|
+
if (opts.mesh) {
|
|
2400
|
+
spin.add(fitClone(opts.mesh));
|
|
2401
|
+
}
|
|
2402
|
+
ms.add(spin);
|
|
2403
|
+
// Auto-rotazione (default) oppure rotazione manuale col mouse (drag) come nel designer.
|
|
2404
|
+
const autorotate = opts.autorotateMesh !== false;
|
|
2405
|
+
if (!autorotate) {
|
|
2406
|
+
enableDragRotate(right, spin);
|
|
2407
|
+
}
|
|
2408
|
+
const loop = () => {
|
|
2409
|
+
if (autorotate) {
|
|
2410
|
+
spin.rotation.y += 0.012;
|
|
2411
|
+
}
|
|
2412
|
+
r.render(ms, mc);
|
|
2413
|
+
overlayRAF = requestAnimationFrame(loop);
|
|
2414
|
+
};
|
|
2415
|
+
loop();
|
|
2416
|
+
}
|
|
2417
|
+
// ── Modalita' 2: pannello 3D nella scena (billboard verso la camera) ─────
|
|
2418
|
+
function showPanel3d(opts, deps) {
|
|
2419
|
+
const group = new THREE.Group();
|
|
2420
|
+
group.name = 'wuicItemPopupPanel';
|
|
2421
|
+
group.userData['wuicTransient'] = true;
|
|
2422
|
+
// pannello campi (canvas texture)
|
|
2423
|
+
const rows = fieldRows(opts);
|
|
2424
|
+
const cw = 512, ch = 512;
|
|
2425
|
+
const cvs = document.createElement('canvas');
|
|
2426
|
+
cvs.width = cw;
|
|
2427
|
+
cvs.height = ch;
|
|
2428
|
+
const g = cvs.getContext('2d');
|
|
2429
|
+
g.fillStyle = opts.panelBg || 'rgba(20,24,33,0.95)';
|
|
2430
|
+
roundRect(g, 0, 0, cw, ch, 24);
|
|
2431
|
+
g.fill();
|
|
2432
|
+
g.fillStyle = '#9db4ff';
|
|
2433
|
+
g.font = 'bold 34px system-ui,sans-serif';
|
|
2434
|
+
let y = 64;
|
|
2435
|
+
if (opts.title) {
|
|
2436
|
+
g.fillText(opts.title, 32, y);
|
|
2437
|
+
y += 20;
|
|
2438
|
+
}
|
|
2439
|
+
g.font = '26px system-ui,sans-serif';
|
|
2440
|
+
for (const rr of rows) {
|
|
2441
|
+
y += 48;
|
|
2442
|
+
g.fillStyle = 'rgba(229,233,240,0.62)';
|
|
2443
|
+
g.fillText(rr.label, 32, y);
|
|
2444
|
+
g.fillStyle = '#e5e9f0';
|
|
2445
|
+
g.font = 'bold 26px system-ui,sans-serif';
|
|
2446
|
+
g.fillText(rr.value.slice(0, 22), 32, y + 30);
|
|
2447
|
+
g.font = '26px system-ui,sans-serif';
|
|
2448
|
+
y += 30;
|
|
2449
|
+
if (y > ch - 60) {
|
|
2450
|
+
break;
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
const tex = new THREE.CanvasTexture(cvs);
|
|
2454
|
+
tex.colorSpace = THREE.SRGBColorSpace;
|
|
2455
|
+
const panel = new THREE.Mesh(new THREE.PlaneGeometry(2.2, 2.2), new THREE.MeshBasicMaterial({ map: tex, transparent: true, depthTest: false }));
|
|
2456
|
+
// swap → campi a destra, mesh a sinistra (default: campi sx, mesh dx).
|
|
2457
|
+
const panelX = opts.swap ? 1.4 : -1.4;
|
|
2458
|
+
const meshX = opts.swap ? -1.3 : 1.3;
|
|
2459
|
+
panel.position.set(panelX, 0, 0);
|
|
2460
|
+
panel.renderOrder = 999;
|
|
2461
|
+
group.add(panel);
|
|
2462
|
+
// mesh sul lato opposto ai campi
|
|
2463
|
+
if (opts.mesh) {
|
|
2464
|
+
const c = fitClone(opts.mesh);
|
|
2465
|
+
c.position.set(meshX, 0, 0);
|
|
2466
|
+
c.scale.multiplyScalar(1.4);
|
|
2467
|
+
group.add(c);
|
|
2468
|
+
}
|
|
2469
|
+
// posiziona davanti alla camera + billboard
|
|
2470
|
+
positionInFrontOfCamera(group, deps.camera, 6);
|
|
2471
|
+
deps.scene.add(group);
|
|
2472
|
+
panel3d = group;
|
|
2473
|
+
panelDeps = deps;
|
|
2474
|
+
if (deps.onFrame) {
|
|
2475
|
+
deps.onFrame(() => { group.quaternion.copy(deps.camera.quaternion); });
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
function positionInFrontOfCamera(obj, camera, dist) {
|
|
2479
|
+
const dir = new THREE.Vector3();
|
|
2480
|
+
camera.getWorldDirection(dir);
|
|
2481
|
+
const pos = new THREE.Vector3();
|
|
2482
|
+
camera.getWorldPosition(pos);
|
|
2483
|
+
obj.position.copy(pos).add(dir.multiplyScalar(dist));
|
|
2484
|
+
obj.quaternion.copy(camera.quaternion);
|
|
2485
|
+
}
|
|
2486
|
+
function roundRect(g, x, y, w, h, r) {
|
|
2487
|
+
g.beginPath();
|
|
2488
|
+
g.moveTo(x + r, y);
|
|
2489
|
+
g.arcTo(x + w, y, x + w, y + h, r);
|
|
2490
|
+
g.arcTo(x + w, y + h, x, y + h, r);
|
|
2491
|
+
g.arcTo(x, y + h, x, y, r);
|
|
2492
|
+
g.arcTo(x, y, x + w, y, r);
|
|
2493
|
+
g.closePath();
|
|
2494
|
+
}
|
|
2495
|
+
function disposeObject(obj) {
|
|
2496
|
+
obj.traverse((o) => {
|
|
2497
|
+
if (o.geometry) {
|
|
2498
|
+
try {
|
|
2499
|
+
o.geometry.dispose();
|
|
2500
|
+
}
|
|
2501
|
+
catch { /* noop */ }
|
|
2502
|
+
}
|
|
2503
|
+
const m = o.material;
|
|
2504
|
+
if (m) {
|
|
2505
|
+
(Array.isArray(m) ? m : [m]).forEach((mm) => { try {
|
|
2506
|
+
mm.map?.dispose?.();
|
|
2507
|
+
mm.dispose?.();
|
|
2508
|
+
}
|
|
2509
|
+
catch { /* noop */ } });
|
|
2510
|
+
}
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
|
|
1934
2514
|
/**
|
|
1935
2515
|
* Factory del renderer di scena — WebGL (default, percorso storico) o WebGPU
|
|
1936
2516
|
* (opt-in, iter WebGPU/node-materials).
|
|
@@ -2196,8 +2776,8 @@ function buildEffectNodeMaterial(effect, webgpu) {
|
|
|
2196
2776
|
material.opacityNode = float(opacity);
|
|
2197
2777
|
material.transparent = true;
|
|
2198
2778
|
}
|
|
2199
|
-
material.wuicShaderEffect = true;
|
|
2200
|
-
material.wuicShaderEffectNode = true;
|
|
2779
|
+
(material).wuicShaderEffect = true;
|
|
2780
|
+
(material).wuicShaderEffectNode = true;
|
|
2201
2781
|
return material;
|
|
2202
2782
|
}
|
|
2203
2783
|
/**
|
|
@@ -2223,5 +2803,5 @@ function swapNodeEffectMaterialsForSerialization(root) {
|
|
|
2223
2803
|
};
|
|
2224
2804
|
}
|
|
2225
2805
|
|
|
2226
|
-
export {
|
|
2227
|
-
//# sourceMappingURL=wuic-framework-lib-scene3d-shader-effect-
|
|
2806
|
+
export { buildUploadTextureResolver as A, applyRepeaterTextureTo as B, CarouselController as C, defaultRepeaterTextureApply as D, regenerateRepeater as E, getPhysicsDef as F, createPhysicsWorld as G, WUIC_PHYSICS_KEY as H, WUIC_OBJECT_TYPE_KEY as I, carouselArrowDir as J, WUIC_REPEATER_RECORD as K, runRepeaterCallback as L, closeItemPopup as M, showItemPopup as N, WUIC_BINDING_KEY as O, swapNodeEffectMaterialsForSerialization as P, serializeScene as Q, deserializeScene as R, SCENE3D_SHADER_EFFECT_DEFAULT as S, WUIC_TRANSIENT_FLAG as W, defaultRepeaterAxisDir as a, defaultCarouselConfig as b, defaultChartConfig as c, defaultMeshRepeaterConfig as d, defaultSurfaceConfig as e, defaultGaugeConfig as f, defaultGraphConfig as g, defaultMapConfig as h, isWebGpuSupported as i, defaultWarehouseConfig as j, defaultKanbanConfig as k, defaultCalendarConfig as l, defaultStackedConfig as m, createScene3dRenderer as n, WUIC_SHADER_EFFECT_KEY as o, makeTextSpriteMesh as p, WUIC_MESH_REPEATER_KEY as q, addRepeaterPlaceholder as r, resolveAssetFormat as s, parseShaderEffect as t, SCENE3D_SHADER_EFFECT_SCHEMA as u, buildEffectNodeMaterial as v, buildEffectShaderMaterial as w, getMeshRepeaterConfig as x, WUIC_REPEATER_CHILD as y, normalizeAssetToUnit as z };
|
|
2807
|
+
//# sourceMappingURL=wuic-framework-lib-scene3d-shader-effect-DjHyCAFK.mjs.map
|