@liquidcars/atlas 0.1.14 → 0.1.16

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/README.md CHANGED
@@ -65,6 +65,21 @@ atlas.setTheme({
65
65
  ui: { breadcrumbs: true, panel: true, toolbar: true, hint: false }
66
66
  });
67
67
  atlas.setPalette({ channels: '#58b6ec', offers: '#48c8b0', connection: '#f4edba' });
68
+
69
+ // El componente se puede modificar sin volver a crearlo.
70
+ atlas.addEntity({ id: 'service', name: 'Service', geometry: 'cone', p: [4, 0, 0], s: [2, 2, 2] });
71
+ atlas.updateEntity('service', { text: 'Servicio actualizado', c: '#ed8f98' });
72
+ atlas.addRelation({ from: 'service', to: 'offers', label: 'publica', mode: 'bidirectional' });
73
+ atlas.updateRelation(0, { mode: 'broken' });
74
+ atlas.removeRelation(0);
75
+ atlas.removeEntity('service');
76
+
77
+ atlas.addEventListener('lc-select', event => {
78
+ console.log('Entidad seleccionada:', event.detail.id, event.detail.entity);
79
+ });
80
+ atlas.addEventListener('lc-model-change', event => {
81
+ console.log('Modelo actualizado:', event.detail.kind);
82
+ });
68
83
  ```
69
84
 
70
85
  El mismo bloque `theme` puede formar parte del JSON del modelo. Los estilos
@@ -73,7 +88,12 @@ principales también están expuestos como variables CSS (`--lc-bg`,
73
88
  por lo que una aplicación puede adaptar la apariencia sin modificar el bundle.
74
89
  Las propiedades `atlas.theme` y `atlas.styles` son alias prácticos de esta configuración.
75
90
 
76
- Eventos disponibles: `lc-ready`, `lc-error`, `lc-select`, `lc-relation-mode` y `lc-disconnect`.
91
+ Eventos disponibles: `lc-ready`, `lc-error`, `lc-select`, `lc-model-change`,
92
+ `lc-relation-mode` y `lc-disconnect`. `lc-select` incluye `{ id, entity,
93
+ source }` en `event.detail`; cuando se vuelve a la vista general emite `id:
94
+ null`. Los eventos de selección y cambios de modelo son `bubbles` y
95
+ `composed`, por lo que funcionan también al integrar el componente en otras
96
+ capas de Web Components.
77
97
 
78
98
  ## Contrato de datos
79
99
 
@@ -83,6 +103,10 @@ Eventos disponibles: `lc-ready`, `lc-error`, `lc-select`, `lc-relation-mode` y `
83
103
  - `p` es la posición local respecto a `parent`; una entidad sin `parent` usa coordenadas de escena.
84
104
  - `s` son dimensiones absolutas locales `[x, y, z]`.
85
105
  - `relations` acepta objetos `{from, to, label}` o la forma corta `[from, to, label]`.
106
+ El campo visual opcional `mode` admite `directed` (por defecto),
107
+ `bidirectional` y `broken`. En modo bidireccional los indicadores animados
108
+ circulan en ambos sentidos por el mismo tubo; en modo broken el tubo aparece
109
+ interrumpido en su punto medio.
86
110
 
87
111
  Geometrías incluidas en esta primera versión: `box`, `cylinder`, `sphere`, `cone`, `frustum-cone`, `frustum-pyramid`, `frustum-triangle`, `prism`, `pyramid-square`, `pyramid-triangle`, `octahedron`, `dodecahedron` e `icosahedron`. Las variantes `frustum-*` son sólidos truncados con dos caras finitas paralelas; las dimensiones `s` se aplican sobre su caja final normalizada.
88
112
 
@@ -90,6 +114,13 @@ La paleta puede cambiarse después de cargar el modelo mediante `atlas.setPalett
90
114
 
91
115
  Las acciones de navegación y exploración de la toolbar también están disponibles para la página anfitriona: `zoomIn()`, `zoomOut()`, `fit()`, `frontView()`, `setShellLabels()`, `toggleShellLabels()`, `openAllContainers()` y `closeAllContainers()`. Así se puede construir una barra de controles propia sin depender de la toolbar interna.
92
116
 
117
+ La escena también se puede editar en caliente. `updateModel(patch)` reemplaza
118
+ las secciones indicadas; `addEntity`, `updateEntity` y `removeEntity` gestionan
119
+ entidades, mientras `addRelation`, `updateRelation` y `removeRelation`
120
+ gestionan relaciones. Cada operación reconstruye la geometría necesaria,
121
+ conserva la cámara y la selección cuando sigue siendo válida, y emite
122
+ `lc-model-change` con el tipo de operación y el modelo resultante.
123
+
93
124
  ## Distribución
94
125
 
95
126
  El archivo `dist/liquidcars-atlas.js` es un módulo ESM autocontenido: Three.js se incluye dentro del bundle, por lo que el consumidor no necesita instalar otra dependencia para probarlo. El bundle no contiene ningún modelo de negocio ni datos de LiquidCars Atlas; los modelos se suministran aparte mediante `model`, `src` o un `<script type="application/json">`. Para una distribución privada se puede servir ese archivo desde un CDN propio o desde el mismo sitio. Para publicar en npm, después de revisar el API estable:
@@ -103,9 +134,9 @@ Una vez publicado, los consumidores podrán instalarlo con `npm install @liquidc
103
134
  Para probar el paquete sin publicarlo, basta con instalar el tarball generado:
104
135
 
105
136
  ```bash
106
- npm install ./liquidcars-atlas-0.1.14.tgz
137
+ npm install ./liquidcars-atlas-0.1.15.tgz
107
138
  ```
108
139
 
109
140
  ## Estado
110
141
 
111
- `0.1.14` mantiene la corrección del desplazamiento interno del texto y ancla las etiquetas a las dimensiones reales de las geometrías redondeadas. Además fija un margen superior absoluto para las etiquetas secundarias de contenedores abiertos, situado dentro del frontal del contenedor y sin depender de sus dimensiones. También corrige los sólidos truncados, normaliza sus dimensiones finales, refuerza la visibilidad de la toolbar con el atributo `toolbar="on"`/`"off"` y `setToolbarVisible()`, desacopla la paleta por defecto de nombres de dominio, añade actualización programática de la paleta con `setPalette` y expone las acciones de navegación para controles externos.
142
+ `0.1.16` mantiene la corrección del desplazamiento interno del texto y ancla las etiquetas a las dimensiones reales de las geometrías redondeadas. Además fija un margen superior absoluto para las etiquetas secundarias de contenedores abiertos, situado dentro del frontal del contenedor y sin depender de sus dimensiones. También corrige los sólidos truncados, normaliza sus dimensiones finales, refuerza la visibilidad de la toolbar con el atributo `toolbar="on"`/`"off"` y `setToolbarVisible()`, desacopla la paleta por defecto de nombres de dominio, añade actualización programática de la paleta con `setPalette`, expone las acciones de navegación para controles externos y mantiene siempre visibles las ayudas de interacción dentro del viewport. Esta versión añade edición en caliente de entidades y relaciones, eventos compuestos de selección y los modos de relación `directed`, `bidirectional` y `broken`.
package/demo/index.html CHANGED
@@ -14,12 +14,12 @@
14
14
  liquidcars-atlas { display: block; width: 100%; max-width: 1480px; margin: 0 auto; }
15
15
  code { color: #85e5d3; }
16
16
  </style>
17
- <script type="module" src="../dist/liquidcars-atlas.js?v=0.1.14"></script>
17
+ <script type="module" src="../dist/liquidcars-atlas.js?v=0.1.15"></script>
18
18
  </head>
19
19
  <body>
20
20
  <header>
21
21
  <h1>LiquidCars Atlas</h1>
22
- <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</code> · v0.1.14</p>
22
+ <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</code> · v0.1.15</p>
23
23
  </header>
24
24
  <main>
25
25
  <liquidcars-atlas id="atlas" height="720" relation-mode="selected" shell-labels="on"></liquidcars-atlas>
@@ -1,6 +1,11 @@
1
- /*! LiquidCars Atlas 0.1.14 | MIT */
1
+ /*! LiquidCars Atlas 0.1.16 | MIT */
2
2
 
3
3
  // src/three.core.js
4
+ /**
5
+ * @license
6
+ * Copyright 2010-2025 Three.js Authors
7
+ * SPDX-License-Identifier: MIT
8
+ */
4
9
  var REVISION = "180";
5
10
  var CullFaceNone = 0;
6
11
  var CullFaceBack = 1;
@@ -221,6 +226,7 @@ var EventDispatcher = class {
221
226
  }
222
227
  };
223
228
  var _lut = ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2a", "2b", "2c", "2d", "2e", "2f", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", "3c", "3d", "3e", "3f", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5a", "5b", "5c", "5d", "5e", "5f", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", "6e", "6f", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7a", "7b", "7c", "7d", "7e", "7f", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", "8c", "8d", "8e", "8f", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af", "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf", "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf", "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df", "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef", "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff"];
229
+ var _seed = 1234567;
224
230
  var DEG2RAD = Math.PI / 180;
225
231
  var RAD2DEG = 180 / Math.PI;
226
232
  function generateUUID() {
@@ -237,9 +243,102 @@ function clamp(value, min, max) {
237
243
  function euclideanModulo(n, m) {
238
244
  return (n % m + m) % m;
239
245
  }
246
+ function mapLinear(x, a1, a2, b1, b2) {
247
+ return b1 + (x - a1) * (b2 - b1) / (a2 - a1);
248
+ }
249
+ function inverseLerp(x, y, value) {
250
+ if (x !== y) {
251
+ return (value - x) / (y - x);
252
+ } else {
253
+ return 0;
254
+ }
255
+ }
240
256
  function lerp(x, y, t) {
241
257
  return (1 - t) * x + t * y;
242
258
  }
259
+ function damp(x, y, lambda, dt) {
260
+ return lerp(x, y, 1 - Math.exp(-lambda * dt));
261
+ }
262
+ function pingpong(x, length = 1) {
263
+ return length - Math.abs(euclideanModulo(x, length * 2) - length);
264
+ }
265
+ function smoothstep(x, min, max) {
266
+ if (x <= min) return 0;
267
+ if (x >= max) return 1;
268
+ x = (x - min) / (max - min);
269
+ return x * x * (3 - 2 * x);
270
+ }
271
+ function smootherstep(x, min, max) {
272
+ if (x <= min) return 0;
273
+ if (x >= max) return 1;
274
+ x = (x - min) / (max - min);
275
+ return x * x * x * (x * (x * 6 - 15) + 10);
276
+ }
277
+ function randInt(low, high) {
278
+ return low + Math.floor(Math.random() * (high - low + 1));
279
+ }
280
+ function randFloat(low, high) {
281
+ return low + Math.random() * (high - low);
282
+ }
283
+ function randFloatSpread(range) {
284
+ return range * (0.5 - Math.random());
285
+ }
286
+ function seededRandom(s) {
287
+ if (s !== void 0) _seed = s;
288
+ let t = _seed += 1831565813;
289
+ t = Math.imul(t ^ t >>> 15, t | 1);
290
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
291
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
292
+ }
293
+ function degToRad(degrees) {
294
+ return degrees * DEG2RAD;
295
+ }
296
+ function radToDeg(radians) {
297
+ return radians * RAD2DEG;
298
+ }
299
+ function isPowerOfTwo(value) {
300
+ return (value & value - 1) === 0 && value !== 0;
301
+ }
302
+ function ceilPowerOfTwo(value) {
303
+ return Math.pow(2, Math.ceil(Math.log(value) / Math.LN2));
304
+ }
305
+ function floorPowerOfTwo(value) {
306
+ return Math.pow(2, Math.floor(Math.log(value) / Math.LN2));
307
+ }
308
+ function setQuaternionFromProperEuler(q, a, b, c, order) {
309
+ const cos = Math.cos;
310
+ const sin = Math.sin;
311
+ const c2 = cos(b / 2);
312
+ const s2 = sin(b / 2);
313
+ const c13 = cos((a + c) / 2);
314
+ const s13 = sin((a + c) / 2);
315
+ const c1_3 = cos((a - c) / 2);
316
+ const s1_3 = sin((a - c) / 2);
317
+ const c3_1 = cos((c - a) / 2);
318
+ const s3_1 = sin((c - a) / 2);
319
+ switch (order) {
320
+ case "XYX":
321
+ q.set(c2 * s13, s2 * c1_3, s2 * s1_3, c2 * c13);
322
+ break;
323
+ case "YZY":
324
+ q.set(s2 * s1_3, c2 * s13, s2 * c1_3, c2 * c13);
325
+ break;
326
+ case "ZXZ":
327
+ q.set(s2 * c1_3, s2 * s1_3, c2 * s13, c2 * c13);
328
+ break;
329
+ case "XZX":
330
+ q.set(c2 * s13, s2 * s3_1, s2 * c3_1, c2 * c13);
331
+ break;
332
+ case "YXY":
333
+ q.set(s2 * c3_1, c2 * s13, s2 * s3_1, c2 * c13);
334
+ break;
335
+ case "ZYZ":
336
+ q.set(s2 * s3_1, s2 * c3_1, c2 * s13, c2 * c13);
337
+ break;
338
+ default:
339
+ console.warn("THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: " + order);
340
+ }
341
+ }
243
342
  function denormalize(value, array) {
244
343
  switch (array.constructor) {
245
344
  case Float32Array:
@@ -280,6 +379,250 @@ function normalize(value, array) {
280
379
  throw new Error("Invalid component type.");
281
380
  }
282
381
  }
382
+ var MathUtils = {
383
+ DEG2RAD,
384
+ RAD2DEG,
385
+ /**
386
+ * Generate a [UUID]{@link https://en.wikipedia.org/wiki/Universally_unique_identifier}
387
+ * (universally unique identifier).
388
+ *
389
+ * @static
390
+ * @method
391
+ * @return {string} The UUID.
392
+ */
393
+ generateUUID,
394
+ /**
395
+ * Clamps the given value between min and max.
396
+ *
397
+ * @static
398
+ * @method
399
+ * @param {number} value - The value to clamp.
400
+ * @param {number} min - The min value.
401
+ * @param {number} max - The max value.
402
+ * @return {number} The clamped value.
403
+ */
404
+ clamp,
405
+ /**
406
+ * Computes the Euclidean modulo of the given parameters that
407
+ * is `( ( n % m ) + m ) % m`.
408
+ *
409
+ * @static
410
+ * @method
411
+ * @param {number} n - The first parameter.
412
+ * @param {number} m - The second parameter.
413
+ * @return {number} The Euclidean modulo.
414
+ */
415
+ euclideanModulo,
416
+ /**
417
+ * Performs a linear mapping from range `<a1, a2>` to range `<b1, b2>`
418
+ * for the given value.
419
+ *
420
+ * @static
421
+ * @method
422
+ * @param {number} x - The value to be mapped.
423
+ * @param {number} a1 - Minimum value for range A.
424
+ * @param {number} a2 - Maximum value for range A.
425
+ * @param {number} b1 - Minimum value for range B.
426
+ * @param {number} b2 - Maximum value for range B.
427
+ * @return {number} The mapped value.
428
+ */
429
+ mapLinear,
430
+ /**
431
+ * Returns the percentage in the closed interval `[0, 1]` of the given value
432
+ * between the start and end point.
433
+ *
434
+ * @static
435
+ * @method
436
+ * @param {number} x - The start point
437
+ * @param {number} y - The end point.
438
+ * @param {number} value - A value between start and end.
439
+ * @return {number} The interpolation factor.
440
+ */
441
+ inverseLerp,
442
+ /**
443
+ * Returns a value linearly interpolated from two known points based on the given interval -
444
+ * `t = 0` will return `x` and `t = 1` will return `y`.
445
+ *
446
+ * @static
447
+ * @method
448
+ * @param {number} x - The start point
449
+ * @param {number} y - The end point.
450
+ * @param {number} t - The interpolation factor in the closed interval `[0, 1]`.
451
+ * @return {number} The interpolated value.
452
+ */
453
+ lerp,
454
+ /**
455
+ * Smoothly interpolate a number from `x` to `y` in a spring-like manner using a delta
456
+ * time to maintain frame rate independent movement. For details, see
457
+ * [Frame rate independent damping using lerp]{@link http://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/}.
458
+ *
459
+ * @static
460
+ * @method
461
+ * @param {number} x - The current point.
462
+ * @param {number} y - The target point.
463
+ * @param {number} lambda - A higher lambda value will make the movement more sudden,
464
+ * and a lower value will make the movement more gradual.
465
+ * @param {number} dt - Delta time in seconds.
466
+ * @return {number} The interpolated value.
467
+ */
468
+ damp,
469
+ /**
470
+ * Returns a value that alternates between `0` and the given `length` parameter.
471
+ *
472
+ * @static
473
+ * @method
474
+ * @param {number} x - The value to pingpong.
475
+ * @param {number} [length=1] - The positive value the function will pingpong to.
476
+ * @return {number} The alternated value.
477
+ */
478
+ pingpong,
479
+ /**
480
+ * Returns a value in the range `[0,1]` that represents the percentage that `x` has
481
+ * moved between `min` and `max`, but smoothed or slowed down the closer `x` is to
482
+ * the `min` and `max`.
483
+ *
484
+ * See [Smoothstep]{@link http://en.wikipedia.org/wiki/Smoothstep} for more details.
485
+ *
486
+ * @static
487
+ * @method
488
+ * @param {number} x - The value to evaluate based on its position between min and max.
489
+ * @param {number} min - The min value. Any x value below min will be `0`.
490
+ * @param {number} max - The max value. Any x value above max will be `1`.
491
+ * @return {number} The alternated value.
492
+ */
493
+ smoothstep,
494
+ /**
495
+ * A [variation on smoothstep]{@link https://en.wikipedia.org/wiki/Smoothstep#Variations}
496
+ * that has zero 1st and 2nd order derivatives at x=0 and x=1.
497
+ *
498
+ * @static
499
+ * @method
500
+ * @param {number} x - The value to evaluate based on its position between min and max.
501
+ * @param {number} min - The min value. Any x value below min will be `0`.
502
+ * @param {number} max - The max value. Any x value above max will be `1`.
503
+ * @return {number} The alternated value.
504
+ */
505
+ smootherstep,
506
+ /**
507
+ * Returns a random integer from `<low, high>` interval.
508
+ *
509
+ * @static
510
+ * @method
511
+ * @param {number} low - The lower value boundary.
512
+ * @param {number} high - The upper value boundary
513
+ * @return {number} A random integer.
514
+ */
515
+ randInt,
516
+ /**
517
+ * Returns a random float from `<low, high>` interval.
518
+ *
519
+ * @static
520
+ * @method
521
+ * @param {number} low - The lower value boundary.
522
+ * @param {number} high - The upper value boundary
523
+ * @return {number} A random float.
524
+ */
525
+ randFloat,
526
+ /**
527
+ * Returns a random integer from `<-range/2, range/2>` interval.
528
+ *
529
+ * @static
530
+ * @method
531
+ * @param {number} range - Defines the value range.
532
+ * @return {number} A random float.
533
+ */
534
+ randFloatSpread,
535
+ /**
536
+ * Returns a deterministic pseudo-random float in the interval `[0, 1]`.
537
+ *
538
+ * @static
539
+ * @method
540
+ * @param {number} [s] - The integer seed.
541
+ * @return {number} A random float.
542
+ */
543
+ seededRandom,
544
+ /**
545
+ * Converts degrees to radians.
546
+ *
547
+ * @static
548
+ * @method
549
+ * @param {number} degrees - A value in degrees.
550
+ * @return {number} The converted value in radians.
551
+ */
552
+ degToRad,
553
+ /**
554
+ * Converts radians to degrees.
555
+ *
556
+ * @static
557
+ * @method
558
+ * @param {number} radians - A value in radians.
559
+ * @return {number} The converted value in degrees.
560
+ */
561
+ radToDeg,
562
+ /**
563
+ * Returns `true` if the given number is a power of two.
564
+ *
565
+ * @static
566
+ * @method
567
+ * @param {number} value - The value to check.
568
+ * @return {boolean} Whether the given number is a power of two or not.
569
+ */
570
+ isPowerOfTwo,
571
+ /**
572
+ * Returns the smallest power of two that is greater than or equal to the given number.
573
+ *
574
+ * @static
575
+ * @method
576
+ * @param {number} value - The value to find a POT for.
577
+ * @return {number} The smallest power of two that is greater than or equal to the given number.
578
+ */
579
+ ceilPowerOfTwo,
580
+ /**
581
+ * Returns the largest power of two that is less than or equal to the given number.
582
+ *
583
+ * @static
584
+ * @method
585
+ * @param {number} value - The value to find a POT for.
586
+ * @return {number} The largest power of two that is less than or equal to the given number.
587
+ */
588
+ floorPowerOfTwo,
589
+ /**
590
+ * Sets the given quaternion from the [Intrinsic Proper Euler Angles]{@link https://en.wikipedia.org/wiki/Euler_angles}
591
+ * defined by the given angles and order.
592
+ *
593
+ * Rotations are applied to the axes in the order specified by order:
594
+ * rotation by angle `a` is applied first, then by angle `b`, then by angle `c`.
595
+ *
596
+ * @static
597
+ * @method
598
+ * @param {Quaternion} q - The quaternion to set.
599
+ * @param {number} a - The rotation applied to the first axis, in radians.
600
+ * @param {number} b - The rotation applied to the second axis, in radians.
601
+ * @param {number} c - The rotation applied to the third axis, in radians.
602
+ * @param {('XYX'|'XZX'|'YXY'|'YZY'|'ZXZ'|'ZYZ')} order - A string specifying the axes order.
603
+ */
604
+ setQuaternionFromProperEuler,
605
+ /**
606
+ * Normalizes the given value according to the given typed array.
607
+ *
608
+ * @static
609
+ * @method
610
+ * @param {number} value - The float value in the range `[0,1]` to normalize.
611
+ * @param {TypedArray} array - The typed array that defines the data type of the value.
612
+ * @return {number} The normalize value.
613
+ */
614
+ normalize,
615
+ /**
616
+ * Denormalizes the given value according to the given typed array.
617
+ *
618
+ * @static
619
+ * @method
620
+ * @param {number} value - The value to denormalize.
621
+ * @param {TypedArray} array - The typed array that defines the data type of the value.
622
+ * @return {number} The denormalize (float) value in the range `[0,1]`.
623
+ */
624
+ denormalize
625
+ };
283
626
  var Vector2 = class _Vector2 {
284
627
  /**
285
628
  * Constructs a new 2D vector.
@@ -18886,6 +19229,11 @@ if (typeof window !== "undefined") {
18886
19229
  }
18887
19230
 
18888
19231
  // src/three.module.js
19232
+ /**
19233
+ * @license
19234
+ * Copyright 2010-2025 Three.js Authors
19235
+ * SPDX-License-Identifier: MIT
19236
+ */
18889
19237
  function WebGLAnimation() {
18890
19238
  let context = null;
18891
19239
  let isAnimating = false;
@@ -29021,7 +29369,7 @@ function normalizeTheme(value) {
29021
29369
  }
29022
29370
  var CSS = [
29023
29371
  ":host{display:block;position:relative;height:var(--lc-height,680px);min-height:420px;color:var(--lc-panel-title,#e9f0f5);font:14px Inter,system-ui,-apple-system,sans-serif;background:var(--lc-bg,#0a111a);border:1px solid #294252;border-radius:12px;overflow:hidden;contain:content}",
29024
- "*{box-sizing:border-box}.atlas{display:grid;grid-template-columns:minmax(0,1fr) 300px;height:100%}.atlas[data-panel=hidden]{grid-template-columns:1fr}.viewport{position:relative;min-width:0;background:radial-gradient(ellipse at 50% 45%,var(--lc-bg-glow,#1c2b39),var(--lc-bg-fade,#0a111a) 82%)}canvas{display:block;width:100%;height:100%;cursor:grab;touch-action:none}.toolbar{position:absolute;z-index:10;bottom:16px;left:50%;transform:translateX(-50%);display:flex;gap:3px;padding:6px;background:var(--lc-toolbar-bg,#15232fe8);border:1px solid var(--lc-toolbar-border,#3a5062);border-radius:8px;white-space:nowrap}.toolbar[data-hidden=true],.toolbar[hidden],.panel[data-hidden=true],.panel[hidden],.crumbs[data-hidden=true],.crumbs[hidden],.hint[data-hidden=true],.hint[hidden]{display:none!important}.toolbar button{height:30px;padding:0 9px;border:0;border-radius:5px;background:none;color:var(--lc-toolbar-text,#dbe7ec);cursor:pointer}.toolbar button:hover,.toolbar button.active{background:var(--lc-toolbar-active,#1f3d4b);color:var(--lc-toolbar-active-text,#c5f2e9)}.toolbar i{display:block;width:1px;margin:2px 3px;background:#38515f}.hint{position:absolute;bottom:5px;left:0;right:0;text-align:center;font-size:11px;color:var(--lc-hint,#8da6b8);pointer-events:none}.panel{overflow:auto;padding:24px 20px;background:var(--lc-panel-bg,linear-gradient(140deg,#182633,#0e1925 65%));border-left:1px solid var(--lc-panel-border,#2a3a48)}.panel h2{font-size:26px;line-height:1.12;font-weight:470;margin:12px 0;color:var(--lc-panel-title,#e9f0f5)}.panel h3{font-size:11px;letter-spacing:1.4px;color:#91aabd;margin:24px 0 10px}.panel p{font-size:14px;line-height:1.55;color:var(--lc-panel-text,#b5c6d2)}.panel .eyebrow{font-size:11px;letter-spacing:1.7px;color:#6ad7c0}.panel .sub{font-size:13px;color:#8fa8bd;margin-bottom:16px}.panel button{display:block;width:100%;padding:10px 0;text-align:left;background:none;border:0;border-bottom:1px solid var(--lc-panel-border,#2a3a48);color:#d9e7eb;cursor:pointer}.panel button:hover{color:#6ad7c0}.panel .relation{padding:10px 0;border-bottom:1px solid var(--lc-panel-border,#2a3a48)}.panel .relation strong{display:block;font-weight:550}.panel .relation small{display:block;color:#86a6b9;font-size:12px;margin-top:4px}.panel .back{color:#9db7c8;font-size:13px}.panel .crumbs{display:flex;flex-wrap:wrap;gap:5px;align-items:center;margin:0 0 12px}.panel .crumbs button{display:inline;width:auto;padding:3px 7px;border:0;border-radius:4px;background:var(--lc-crumb-bg,#142733);color:var(--lc-crumb-text,#91b6c7);font-size:12px}.panel .crumbs button.current{background:var(--lc-crumb-active,#1c4a4d);color:var(--lc-crumb-active-text,#c5f2e9)}.panel .note{border-top:1px solid var(--lc-panel-border,#2a3a48);padding-top:16px;margin-top:22px;font-size:12px;color:#7692a6}.error{position:absolute;inset:20%;padding:18px;background:#172b3b;border:1px solid #7896a9;line-height:1.5;z-index:4}@media(max-width:900px){.toolbar{gap:0}.toolbar button{padding:0 6px;font-size:12px}}@media(max-width:760px){:host{height:680px}.atlas{grid-template-columns:1fr;grid-template-rows:minmax(420px,1fr) auto}.panel{max-height:250px;border-left:0;border-top:1px solid var(--lc-panel-border,#2a3a48);padding:18px}.toolbar{bottom:10px}.hint{display:none}}"
29372
+ "*{box-sizing:border-box}.atlas{display:grid;grid-template-columns:minmax(0,1fr) 300px;height:100%}.atlas[data-panel=hidden]{grid-template-columns:1fr}.viewport{position:relative;min-width:0;overflow:hidden;background:radial-gradient(ellipse at 50% 45%,var(--lc-bg-glow,#1c2b39),var(--lc-bg-fade,#0a111a) 82%)}canvas{display:block;width:100%;height:100%;cursor:grab;touch-action:none}.toolbar{position:absolute;z-index:10;bottom:28px;left:50%;transform:translateX(-50%);display:flex;gap:3px;padding:6px;background:var(--lc-toolbar-bg,#15232fe8);border:1px solid var(--lc-toolbar-border,#3a5062);border-radius:8px;white-space:nowrap}.toolbar[data-hidden=true],.toolbar[hidden],.panel[data-hidden=true],.panel[hidden],.crumbs[data-hidden=true],.crumbs[hidden],.hint[data-hidden=true],.hint[hidden]{display:none!important}.toolbar button{height:30px;padding:0 9px;border:0;border-radius:5px;background:none;color:var(--lc-toolbar-text,#dbe7ec);cursor:pointer}.toolbar button:hover,.toolbar button.active{background:var(--lc-toolbar-active,#1f3d4b);color:var(--lc-toolbar-active-text,#c5f2e9)}.toolbar i{display:block;width:1px;margin:2px 3px;background:#38515f}.hint{position:absolute;z-index:9;top:8px;bottom:auto;left:0;right:0;text-align:center;padding:2px 8px;font-size:11px;line-height:16px;color:var(--lc-hint,#8da6b8);pointer-events:none}.panel{overflow:auto;padding:24px 20px;background:var(--lc-panel-bg,linear-gradient(140deg,#182633,#0e1925 65%));border-left:1px solid var(--lc-panel-border,#2a3a48)}.panel h2{font-size:26px;line-height:1.12;font-weight:470;margin:12px 0;color:var(--lc-panel-title,#e9f0f5)}.panel h3{font-size:11px;letter-spacing:1.4px;color:#91aabd;margin:24px 0 10px}.panel p{font-size:14px;line-height:1.55;color:var(--lc-panel-text,#b5c6d2)}.panel .eyebrow{font-size:11px;letter-spacing:1.7px;color:#6ad7c0}.panel .sub{font-size:13px;color:#8fa8bd;margin-bottom:16px}.panel button{display:block;width:100%;padding:10px 0;text-align:left;background:none;border:0;border-bottom:1px solid var(--lc-panel-border,#2a3a48);color:#d9e7eb;cursor:pointer}.panel button:hover{color:#6ad7c0}.panel .relation{padding:10px 0;border-bottom:1px solid var(--lc-panel-border,#2a3a48)}.panel .relation strong{display:block;font-weight:550}.panel .relation small{display:block;color:#86a6b9;font-size:12px;margin-top:4px}.panel .back{color:#9db7c8;font-size:13px}.panel .crumbs{display:flex;flex-wrap:wrap;gap:5px;align-items:center;margin:0 0 12px}.panel .crumbs button{display:inline;width:auto;padding:3px 7px;border:0;border-radius:4px;background:var(--lc-crumb-bg,#142733);color:var(--lc-crumb-text,#91b6c7);font-size:12px}.panel .crumbs button.current{background:var(--lc-crumb-active,#1c4a4d);color:var(--lc-crumb-active-text,#c5f2e9)}.panel .note{border-top:1px solid var(--lc-panel-border,#2a3a48);padding-top:16px;margin-top:22px;font-size:12px;color:#7692a6}.error{position:absolute;inset:20%;padding:18px;background:#172b3b;border:1px solid #7896a9;line-height:1.5;z-index:4}@media(max-width:900px){.toolbar{gap:0}.toolbar button{padding:0 6px;font-size:12px}}@media(max-width:760px){:host{height:680px}.atlas{grid-template-columns:1fr;grid-template-rows:minmax(420px,1fr) auto}.panel{max-height:250px;border-left:0;border-top:1px solid var(--lc-panel-border,#2a3a48);padding:18px}.toolbar{bottom:24px}.hint{top:8px;bottom:auto;display:block}}"
29025
29373
  ].join("");
29026
29374
  var TEMPLATE = '<div class="atlas"><section class="viewport"><canvas tabindex="0" aria-label="Modelo 3D interactivo"></canvas><div class="toolbar"><button data-action="minus" aria-label="Alejar">\u2212</button><button data-action="plus" aria-label="Acercar">+</button><i></i><button data-action="fit">\u2316 Encuadrar</button><button data-action="front">\u25B1 Frontal</button><button data-action="labels" class="active" aria-pressed="true">\u25A7 Etiquetas</button><i></i><button data-action="relations-all" aria-pressed="false">Todas</button><button data-action="relations-selected" class="active" aria-pressed="true">Selecci\xF3n</button><button data-action="relations-none" aria-pressed="false">Ocultar</button></div><div class="hint">Arrastra para girar \xB7 rueda para zoom \xB7 clic para seleccionar</div></section><aside class="panel" aria-live="polite"></aside></div>';
29027
29375
  function escapeHtml(value) {
@@ -29033,6 +29381,13 @@ function normalizeModel(input) {
29033
29381
  const relations = Array.isArray(source.relations) ? source.relations.map((relation, index) => Array.isArray(relation) ? { from: relation[0], to: relation[1], label: relation[2] || "", index } : { ...relation, from: relation.from, to: relation.to, label: relation.label || "", index }) : [];
29034
29382
  return { ...source, palette: { ...DEFAULT_PALETTE, ...source.palette || {} }, theme: normalizeTheme(source.theme || source.styles), entities, relations };
29035
29383
  }
29384
+ function relationVisualMode(relation) {
29385
+ if (!relation || typeof relation !== "object") return "directed";
29386
+ const raw = String(relation.mode || relation.visual || relation.render || relation.type || relation.direction || "").toLowerCase();
29387
+ if (relation.bidirectional === true || ["bidirectional", "both", "two-way", "two_way", "duplex"].includes(raw)) return "bidirectional";
29388
+ if (relation.broken === true || ["broken", "interrupted", "\u65AD"].includes(raw)) return "broken";
29389
+ return "directed";
29390
+ }
29036
29391
  function roundedBox(radius, segments, size = [1, 1, 1]) {
29037
29392
  const r = Math.min(Math.max(0, Number(radius) || 0), size[0] / 2, size[1] / 2, size[2] / 2);
29038
29393
  if (!r) return new BoxGeometry(1, 1, 1);
@@ -29291,6 +29646,119 @@ var LiquidCarsAtlas = class extends HTMLElement {
29291
29646
  set model(value) {
29292
29647
  this.load(value);
29293
29648
  }
29649
+ get selectedId() {
29650
+ return this.selected?.id || null;
29651
+ }
29652
+ _emit(type, detail = {}) {
29653
+ this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
29654
+ }
29655
+ _publicEntity(id) {
29656
+ const entity = this.modelData?.entities?.find((item) => item.id === id);
29657
+ return entity ? {
29658
+ ...entity,
29659
+ p: Array.isArray(entity.p) ? [...entity.p] : [0, 0, 0],
29660
+ s: Array.isArray(entity.s) ? [...entity.s] : [1, 1, 1]
29661
+ } : null;
29662
+ }
29663
+ _commitModelChange(kind, detail = {}, options = {}) {
29664
+ const selectedId = options.preserveSelection === false ? null : this.selected?.id;
29665
+ this.modelData = normalizeModel(this.modelData);
29666
+ this._build({ preserveSelection: selectedId });
29667
+ this._emit("lc-model-change", {
29668
+ kind,
29669
+ model: this.modelData,
29670
+ ...detail
29671
+ });
29672
+ return this;
29673
+ }
29674
+ /** Replace selected parts of the model and rebuild the scene in place. */
29675
+ updateModel(patch = {}, options = {}) {
29676
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) throw new TypeError("El parche del modelo debe ser un objeto.");
29677
+ const current = this.modelData || normalizeModel({});
29678
+ const next = {
29679
+ ...current,
29680
+ ...patch,
29681
+ palette: patch.palette ? { ...current.palette, ...patch.palette } : current.palette,
29682
+ theme: patch.theme || patch.styles ? patch.theme || patch.styles : current.theme,
29683
+ entities: Array.isArray(patch.entities) ? patch.entities : current.entities,
29684
+ relations: Array.isArray(patch.relations) ? patch.relations : current.relations
29685
+ };
29686
+ this.modelData = normalizeModel(next);
29687
+ this.theme = this.modelData.theme;
29688
+ return this._commitModelChange("model-update", { patch }, options);
29689
+ }
29690
+ addEntity(entity, options = {}) {
29691
+ if (!entity || typeof entity !== "object" || !entity.id) throw new TypeError("La entidad debe incluir un id.");
29692
+ if (!this.modelData) this.modelData = normalizeModel({});
29693
+ if (this.modelData.entities.some((item) => item.id === entity.id)) throw new Error(`Ya existe una entidad con id "${entity.id}".`);
29694
+ if (entity.parent && !this.modelData.entities.some((item) => item.id === entity.parent)) throw new Error(`No existe el contenedor padre "${entity.parent}".`);
29695
+ this.modelData.entities = [...this.modelData.entities, { ...entity }];
29696
+ return this._commitModelChange("entity-add", { entity: this._publicEntity(entity.id) }, options);
29697
+ }
29698
+ updateEntity(id, patch = {}, options = {}) {
29699
+ if (!this.modelData) throw new Error("No hay ning\xFAn modelo cargado.");
29700
+ const index = this.modelData.entities.findIndex((item) => item.id === id);
29701
+ if (index < 0) throw new Error(`No existe una entidad con id "${id}".`);
29702
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) throw new TypeError("Los cambios de una entidad deben ser un objeto.");
29703
+ if (patch.id && patch.id !== id) throw new Error("El id de una entidad no se puede modificar.");
29704
+ if (patch.parent && patch.parent !== id && !this.modelData.entities.some((item) => item.id === patch.parent)) throw new Error(`No existe el contenedor padre "${patch.parent}".`);
29705
+ this.modelData.entities = this.modelData.entities.map((item, itemIndex) => itemIndex === index ? { ...item, ...patch, id } : item);
29706
+ return this._commitModelChange("entity-update", { id, patch, entity: this._publicEntity(id) }, options);
29707
+ }
29708
+ removeEntity(id, options = {}) {
29709
+ if (!this.modelData) throw new Error("No hay ning\xFAn modelo cargado.");
29710
+ const cascade = options.cascade !== false;
29711
+ const removed = /* @__PURE__ */ new Set([id]);
29712
+ if (cascade) {
29713
+ let changed = true;
29714
+ while (changed) {
29715
+ changed = false;
29716
+ for (const entity of this.modelData.entities) {
29717
+ if (entity.parent && removed.has(entity.parent) && !removed.has(entity.id)) {
29718
+ removed.add(entity.id);
29719
+ changed = true;
29720
+ }
29721
+ }
29722
+ }
29723
+ }
29724
+ if (!this.modelData.entities.some((item) => item.id === id)) throw new Error(`No existe una entidad con id "${id}".`);
29725
+ this.modelData.entities = this.modelData.entities.filter((item) => !removed.has(item.id));
29726
+ this.modelData.relations = this.modelData.relations.filter((relation) => !removed.has(relation.from) && !removed.has(relation.to));
29727
+ for (const removedId of removed) this.open.delete(removedId);
29728
+ if (this.selected && removed.has(this.selected.id)) this.selected = null;
29729
+ return this._commitModelChange("entity-remove", { id, removed: [...removed] });
29730
+ }
29731
+ _relationIndex(reference) {
29732
+ if (Number.isInteger(reference)) return reference;
29733
+ if (typeof reference === "string") return this.modelData?.relations?.findIndex((relation) => relation.id === reference) ?? -1;
29734
+ return -1;
29735
+ }
29736
+ addRelation(relation, options = {}) {
29737
+ if (!relation || typeof relation !== "object" || !relation.from || !relation.to) throw new TypeError("La relaci\xF3n debe incluir from y to.");
29738
+ if (!this.modelData) throw new Error("No hay ning\xFAn modelo cargado.");
29739
+ const ids = new Set(this.modelData.entities.map((entity) => entity.id));
29740
+ if (!ids.has(relation.from) || !ids.has(relation.to)) throw new Error("Los extremos de la relaci\xF3n deben existir en el modelo.");
29741
+ this.modelData.relations = [...this.modelData.relations, { ...relation }];
29742
+ return this._commitModelChange("relation-add", { relation: { ...relation } }, options);
29743
+ }
29744
+ updateRelation(reference, patch = {}, options = {}) {
29745
+ if (!this.modelData) throw new Error("No hay ning\xFAn modelo cargado.");
29746
+ const index = this._relationIndex(reference);
29747
+ if (index < 0 || index >= this.modelData.relations.length) throw new Error("No existe la relaci\xF3n indicada.");
29748
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) throw new TypeError("Los cambios de una relaci\xF3n deben ser un objeto.");
29749
+ const next = { ...this.modelData.relations[index], ...patch };
29750
+ const ids = new Set(this.modelData.entities.map((entity) => entity.id));
29751
+ if (!ids.has(next.from) || !ids.has(next.to)) throw new Error("Los extremos de la relaci\xF3n deben existir en el modelo.");
29752
+ this.modelData.relations = this.modelData.relations.map((item, itemIndex) => itemIndex === index ? next : item);
29753
+ return this._commitModelChange("relation-update", { index, patch, relation: { ...next } }, options);
29754
+ }
29755
+ removeRelation(reference, options = {}) {
29756
+ if (!this.modelData) throw new Error("No hay ning\xFAn modelo cargado.");
29757
+ const index = this._relationIndex(reference);
29758
+ if (index < 0 || index >= this.modelData.relations.length) throw new Error("No existe la relaci\xF3n indicada.");
29759
+ const [relation] = this.modelData.relations.splice(index, 1);
29760
+ return this._commitModelChange("relation-remove", { index, relation: { ...relation } }, options);
29761
+ }
29294
29762
  get palette() {
29295
29763
  return this.modelData?.palette || { ...DEFAULT_PALETTE };
29296
29764
  }
@@ -29329,8 +29797,21 @@ var LiquidCarsAtlas = class extends HTMLElement {
29329
29797
  return this;
29330
29798
  }
29331
29799
  fit() {
29332
- this.targetZoom = 1;
29333
- this.targetFocus.set(0, 0, 0);
29800
+ const bounds = new Box3();
29801
+ for (const node of this.nodes.values()) {
29802
+ const center2 = this._worldPosition(node);
29803
+ const half = new Vector3(node.s[0] / 2, node.s[1] / 2, node.s[2] / 2);
29804
+ bounds.expandByPoint(center2.clone().sub(half));
29805
+ bounds.expandByPoint(center2.clone().add(half));
29806
+ }
29807
+ const center = bounds.isEmpty() ? new Vector3() : bounds.getCenter(new Vector3());
29808
+ const size = bounds.isEmpty() ? new Vector3(1, 1, 1) : bounds.getSize(new Vector3());
29809
+ const radius = Math.max(1, size.length() / 2);
29810
+ const fov2 = MathUtils.degToRad(this.camera.fov);
29811
+ const requiredDistance = radius / Math.sin(fov2 / 2) * 1.12;
29812
+ const baseDistance = Math.max(29, 39.5 / Math.max(this.camera.aspect, 0.5));
29813
+ this.targetZoom = Math.max(0.55, Math.min(2.8, baseDistance / requiredDistance));
29814
+ this.targetFocus.copy(center);
29334
29815
  this.invalidate();
29335
29816
  return this;
29336
29817
  }
@@ -29451,6 +29932,8 @@ var LiquidCarsAtlas = class extends HTMLElement {
29451
29932
  try {
29452
29933
  const data = typeof source === "string" ? await fetch(source).then((response) => response.json()) : source;
29453
29934
  this.modelData = normalizeModel(data);
29935
+ this.open.clear();
29936
+ this.selected = null;
29454
29937
  if (this._pendingPalette) {
29455
29938
  this.modelData.palette = { ...this.modelData.palette, ...this._pendingPalette };
29456
29939
  this._pendingPalette = null;
@@ -29510,6 +29993,14 @@ var LiquidCarsAtlas = class extends HTMLElement {
29510
29993
  this.scene.remove(node.group);
29511
29994
  }
29512
29995
  this.nodes.clear();
29996
+ this._clearRelationGroup();
29997
+ }
29998
+ _clearRelationGroup() {
29999
+ for (const object of this.relationGroup.children) {
30000
+ object.geometry?.dispose();
30001
+ if (Array.isArray(object.material)) object.material.forEach((material) => material.dispose());
30002
+ else object.material?.dispose();
30003
+ }
29513
30004
  this.relationGroup.clear();
29514
30005
  }
29515
30006
  _recolorNodes() {
@@ -29520,10 +30011,12 @@ var LiquidCarsAtlas = class extends HTMLElement {
29520
30011
  if (node.edge?.material) node.edge.material.color.set(color);
29521
30012
  }
29522
30013
  }
29523
- _build() {
30014
+ _build(options = {}) {
29524
30015
  if (!this._ensureRenderer()) return;
30016
+ const selectedId = options.preserveSelection || null;
29525
30017
  this._clearObjects();
29526
30018
  for (const entity of this.modelData.entities) this.nodes.set(entity.id, { ...entity });
30019
+ this.open = new Set([...this.open].filter((id) => this.nodes.has(id)));
29527
30020
  for (const entity of this.modelData.entities) {
29528
30021
  const node = this.nodes.get(entity.id);
29529
30022
  node.group = new Group();
@@ -29555,6 +30048,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
29555
30048
  node.group.add(node.label);
29556
30049
  this.scene.add(node.group);
29557
30050
  }
30051
+ this.selected = selectedId && this.nodes.has(selectedId) ? this.nodes.get(selectedId) : null;
29558
30052
  this.relations = this.modelData.relations;
29559
30053
  this._renderPanel();
29560
30054
  this._rebuildRelations();
@@ -29594,7 +30088,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
29594
30088
  return this.relations.map((relation, index) => relation.from === this.selected.id || relation.to === this.selected.id ? index : -1).filter((index) => index >= 0);
29595
30089
  }
29596
30090
  _rebuildRelations() {
29597
- this.relationGroup.clear();
30091
+ this._clearRelationGroup();
29598
30092
  const active = this._activeRelationIndexes();
29599
30093
  const color = new Color(this.modelData?.palette.connection || DEFAULT_PALETTE.connection);
29600
30094
  for (const index of active) {
@@ -29606,14 +30100,28 @@ var LiquidCarsAtlas = class extends HTMLElement {
29606
30100
  const end = this._endpoint(to, from, false);
29607
30101
  const delta = end.clone().sub(start);
29608
30102
  if (delta.lengthSq() < 0.01) continue;
29609
- const tube = new Mesh(new TubeGeometry(new LineCurve3(start, end), 10, 0.045, 6, false), new MeshBasicMaterial({ color, transparent: true, opacity: 0.82 }));
29610
- this.relationGroup.add(tube);
30103
+ const visualMode = relationVisualMode(relation);
30104
+ const makeTube = (from2, to2, opacity = 0.82) => {
30105
+ const tube = new Mesh(new TubeGeometry(new LineCurve3(from2, to2), 10, 0.045, 6, false), new MeshBasicMaterial({ color, transparent: true, opacity }));
30106
+ tube.userData.relationIndex = index;
30107
+ this.relationGroup.add(tube);
30108
+ };
30109
+ if (visualMode === "broken") {
30110
+ const direction = delta.clone().normalize();
30111
+ const midpoint = start.clone().lerp(end, 0.5);
30112
+ const gap = Math.min(delta.length() * 0.16, 0.72);
30113
+ makeTube(start, midpoint.clone().sub(direction.clone().multiplyScalar(gap / 2)), 0.55);
30114
+ makeTube(midpoint.clone().add(direction.clone().multiplyScalar(gap / 2)), end, 0.55);
30115
+ continue;
30116
+ }
30117
+ makeTube(start, end);
29611
30118
  for (let ringIndex = 0; ringIndex < 4; ringIndex++) {
29612
30119
  const ring = new Mesh(new TorusGeometry(0.068, 0.018, 8, 20), new MeshBasicMaterial({ color, transparent: true, opacity: 0.9 }));
29613
30120
  ring.userData.phase = ringIndex / 4;
30121
+ ring.userData.direction = visualMode === "bidirectional" && ringIndex % 2 ? -1 : 1;
29614
30122
  ring.userData.start = start.clone();
29615
30123
  ring.userData.end = end.clone();
29616
- ring.quaternion.setFromUnitVectors(new Vector3(0, 0, 1), delta.normalize());
30124
+ ring.quaternion.setFromUnitVectors(new Vector3(0, 0, 1), delta.clone().normalize());
29617
30125
  this.relationGroup.add(ring);
29618
30126
  }
29619
30127
  }
@@ -29661,7 +30169,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
29661
30169
  });
29662
30170
  this._applyTheme();
29663
30171
  }
29664
- select(id) {
30172
+ select(id, options = {}) {
29665
30173
  const node = this.nodes.get(id);
29666
30174
  if (!node) return;
29667
30175
  let parent = node.parent ? this.nodes.get(node.parent) : null;
@@ -29676,7 +30184,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
29676
30184
  this._renderPanel();
29677
30185
  this._rebuildRelations();
29678
30186
  this.invalidate();
29679
- this.dispatchEvent(new CustomEvent("lc-select", { detail: { id, entity: node } }));
30187
+ this._emit("lc-select", { id, entity: this._publicEntity(id), source: options.source || "api" });
29680
30188
  }
29681
30189
  setRelationMode(mode) {
29682
30190
  this.relationMode = ["all", "selected", "none"].includes(mode) ? mode : "selected";
@@ -29694,6 +30202,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
29694
30202
  this.dispatchEvent(new CustomEvent("lc-relation-mode", { detail: { mode: this.relationMode } }));
29695
30203
  }
29696
30204
  reset() {
30205
+ const previousId = this.selected?.id || null;
29697
30206
  this.selected = null;
29698
30207
  this.open.clear();
29699
30208
  this.relationMode = "selected";
@@ -29712,9 +30221,11 @@ var LiquidCarsAtlas = class extends HTMLElement {
29712
30221
  this.targetZoom = 1;
29713
30222
  this.focus.set(0, 0, 0);
29714
30223
  this.targetFocus.set(0, 0, 0);
30224
+ this.fit();
29715
30225
  this._renderPanel();
29716
30226
  this._rebuildRelations();
29717
30227
  this.invalidate();
30228
+ if (previousId) this._emit("lc-select", { id: null, entity: null, previousId, source: "reset" });
29718
30229
  }
29719
30230
  resize() {
29720
30231
  if (!this.renderer) return;
@@ -29773,7 +30284,10 @@ var LiquidCarsAtlas = class extends HTMLElement {
29773
30284
  top.visible = displayTransparentLabel && cameraAbove;
29774
30285
  const scale = displayTransparentLabel ? 0.52 : 1;
29775
30286
  front.scale.setScalar(scale);
29776
- front.position.x = displayTransparentLabel ? -labelSize.x / 2 + 0.18 + labelWidth * scale / 2 - textOffsetX * scale : 0;
30287
+ back.scale.setScalar(scale);
30288
+ const openLabelX = displayTransparentLabel ? -labelSize.x / 2 + 0.18 + labelWidth * scale / 2 - textOffsetX * scale : 0;
30289
+ front.position.x = openLabelX;
30290
+ back.position.x = displayTransparentLabel ? -openLabelX : 0;
29777
30291
  const shellFrontTopMargin = 0.18;
29778
30292
  front.position.y = displayTransparentLabel ? labelSize.y / 2 - shellFrontTopMargin - labelHeight * scale / 2 : labelSize.y / 2 - 0.09 - labelHeight / 2;
29779
30293
  front.position.z = displayTransparentLabel ? labelSize.z / 2 + 0.025 : labelSize.z / 2 + 0.012;
@@ -29793,16 +30307,23 @@ var LiquidCarsAtlas = class extends HTMLElement {
29793
30307
  back.visible = !cameraOnFront && !opened;
29794
30308
  top.visible = false;
29795
30309
  front.scale.setScalar(1);
30310
+ back.scale.setScalar(1);
29796
30311
  front.position.x = 0;
30312
+ back.position.x = 0;
29797
30313
  front.position.y = labelSize.y / 2 - 0.09 - labelHeight / 2;
30314
+ back.position.y = front.position.y;
30315
+ back.position.z = -labelSize.z / 2 - 0.012;
29798
30316
  front.material.color.set(15923964);
29799
30317
  front.material.opacity = 1;
30318
+ back.material.color.set(15923964);
30319
+ back.material.opacity = 1;
29800
30320
  }
29801
30321
  }
29802
30322
  for (const child of this.relationGroup.children) {
29803
30323
  if (!child.userData.start) continue;
29804
30324
  const phase = child.userData.phase || 0;
29805
- const factor = (time * 22e-5 + phase) % 1;
30325
+ const direction = child.userData.direction || 1;
30326
+ const factor = ((time * 22e-5 * direction + phase) % 1 + 1) % 1;
29806
30327
  child.position.lerpVectors(child.userData.start, child.userData.end, factor);
29807
30328
  }
29808
30329
  this.renderer.render(this.scene, this.camera);
@@ -29856,7 +30377,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
29856
30377
  this.canvas.style.cursor = "grab";
29857
30378
  if (drag && !drag.moved) {
29858
30379
  const node = this._pick(event);
29859
- if (node) this.select(node.id);
30380
+ if (node) this.select(node.id, { source: "pointer" });
29860
30381
  }
29861
30382
  this.invalidate();
29862
30383
  });
package/index.html CHANGED
@@ -14,12 +14,12 @@
14
14
  liquidcars-atlas { display: block; width: 100%; max-width: 1480px; margin: 0 auto; }
15
15
  code { color: #85e5d3; }
16
16
  </style>
17
- <script type="module" src="./dist/liquidcars-atlas.js?v=0.1.14"></script>
17
+ <script type="module" src="./dist/liquidcars-atlas.js?v=0.1.15"></script>
18
18
  </head>
19
19
  <body>
20
20
  <header>
21
21
  <h1>LiquidCars Atlas</h1>
22
- <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</code> · v0.1.14</p>
22
+ <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</code> · v0.1.15</p>
23
23
  </header>
24
24
  <main>
25
25
  <liquidcars-atlas id="atlas" height="720" relation-mode="selected" shell-labels="on"></liquidcars-atlas>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liquidcars/atlas",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Reusable 3D conceptual atlas Web Component for LiquidCars-style models",
5
5
  "type": "module",
6
6
  "main": "./dist/liquidcars-atlas.js",