@liquidcars/atlas 0.1.15 → 0.1.17

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,22 @@ 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
+ atlas.setSelectionMode('event'); // el clic emite lc-select sin navegar internamente
69
+
70
+ // El componente se puede modificar sin volver a crearlo.
71
+ atlas.addEntity({ id: 'service', name: 'Service', geometry: 'cone', p: [4, 0, 0], s: [2, 2, 2] });
72
+ atlas.updateEntity('service', { text: 'Servicio actualizado', c: '#ed8f98' });
73
+ atlas.addRelation({ from: 'service', to: 'offers', label: 'publica', mode: 'bidirectional' });
74
+ atlas.updateRelation(0, { mode: 'broken' });
75
+ atlas.removeRelation(0);
76
+ atlas.removeEntity('service');
77
+
78
+ atlas.addEventListener('lc-select', event => {
79
+ console.log('Entidad seleccionada:', event.detail.id, event.detail.entity);
80
+ });
81
+ atlas.addEventListener('lc-model-change', event => {
82
+ console.log('Modelo actualizado:', event.detail.kind);
83
+ });
68
84
  ```
69
85
 
70
86
  El mismo bloque `theme` puede formar parte del JSON del modelo. Los estilos
@@ -73,7 +89,18 @@ principales también están expuestos como variables CSS (`--lc-bg`,
73
89
  por lo que una aplicación puede adaptar la apariencia sin modificar el bundle.
74
90
  Las propiedades `atlas.theme` y `atlas.styles` son alias prácticos de esta configuración.
75
91
 
76
- Eventos disponibles: `lc-ready`, `lc-error`, `lc-select`, `lc-relation-mode` y `lc-disconnect`.
92
+ Eventos disponibles: `lc-ready`, `lc-error`, `lc-select`, `lc-model-change`,
93
+ `lc-relation-mode` y `lc-disconnect`. `lc-select` incluye `{ id, entity,
94
+ source }` en `event.detail`; cuando se vuelve a la vista general emite `id:
95
+ null`. Los eventos de selección y cambios de modelo son `bubbles` y
96
+ `composed`, por lo que funcionan también al integrar el componente en otras
97
+ capas de Web Components.
98
+
99
+ El comportamiento del clic se puede elegir con `selection-mode="internal"`
100
+ (por defecto) o `selection-mode="event"`. En el segundo caso el componente
101
+ emite `lc-select` con `source: "pointer"`, pero deja la decisión a la página
102
+ anfitriona. La API equivalente es `setSelectionMode()` (también disponible
103
+ como `setClickBehavior()`).
77
104
 
78
105
  ## Contrato de datos
79
106
 
@@ -83,6 +110,10 @@ Eventos disponibles: `lc-ready`, `lc-error`, `lc-select`, `lc-relation-mode` y `
83
110
  - `p` es la posición local respecto a `parent`; una entidad sin `parent` usa coordenadas de escena.
84
111
  - `s` son dimensiones absolutas locales `[x, y, z]`.
85
112
  - `relations` acepta objetos `{from, to, label}` o la forma corta `[from, to, label]`.
113
+ El campo visual opcional `mode` admite `directed` (por defecto),
114
+ `bidirectional` y `broken`. En modo bidireccional los indicadores animados
115
+ circulan en ambos sentidos por el mismo tubo; en modo broken el tubo aparece
116
+ interrumpido en su punto medio.
86
117
 
87
118
  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
119
 
@@ -90,6 +121,13 @@ La paleta puede cambiarse después de cargar el modelo mediante `atlas.setPalett
90
121
 
91
122
  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
123
 
124
+ La escena también se puede editar en caliente. `updateModel(patch)` reemplaza
125
+ las secciones indicadas; `addEntity`, `updateEntity` y `removeEntity` gestionan
126
+ entidades, mientras `addRelation`, `updateRelation` y `removeRelation`
127
+ gestionan relaciones. Cada operación reconstruye la geometría necesaria,
128
+ conserva la cámara y la selección cuando sigue siendo válida, y emite
129
+ `lc-model-change` con el tipo de operación y el modelo resultante.
130
+
93
131
  ## Distribución
94
132
 
95
133
  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 +141,9 @@ Una vez publicado, los consumidores podrán instalarlo con `npm install @liquidc
103
141
  Para probar el paquete sin publicarlo, basta con instalar el tarball generado:
104
142
 
105
143
  ```bash
106
- npm install ./liquidcars-atlas-0.1.15.tgz
144
+ npm install ./liquidcars-atlas-0.1.17.tgz
107
145
  ```
108
146
 
109
147
  ## Estado
110
148
 
111
- `0.1.15` 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.
149
+ `0.1.17` 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.15"></script>
17
+ <script type="module" src="../dist/liquidcars-atlas.js?v=0.1.17"></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.15</p>
22
+ <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</code> · v0.1.17</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.15 | MIT */
1
+ /*! LiquidCars Atlas 0.1.17 | 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;
@@ -19224,6 +19229,11 @@ if (typeof window !== "undefined") {
19224
19229
  }
19225
19230
 
19226
19231
  // src/three.module.js
19232
+ /**
19233
+ * @license
19234
+ * Copyright 2010-2025 Three.js Authors
19235
+ * SPDX-License-Identifier: MIT
19236
+ */
19227
19237
  function WebGLAnimation() {
19228
19238
  let context = null;
19229
19239
  let isAnimating = false;
@@ -29371,6 +29381,13 @@ function normalizeModel(input) {
29371
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 }) : [];
29372
29382
  return { ...source, palette: { ...DEFAULT_PALETTE, ...source.palette || {} }, theme: normalizeTheme(source.theme || source.styles), entities, relations };
29373
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
+ }
29374
29391
  function roundedBox(radius, segments, size = [1, 1, 1]) {
29375
29392
  const r = Math.min(Math.max(0, Number(radius) || 0), size[0] / 2, size[1] / 2, size[2] / 2);
29376
29393
  if (!r) return new BoxGeometry(1, 1, 1);
@@ -29561,7 +29578,7 @@ function makeLabel(text, color, dimensions, type = "") {
29561
29578
  }
29562
29579
  var LiquidCarsAtlas = class extends HTMLElement {
29563
29580
  static get observedAttributes() {
29564
- return ["height", "relation-mode", "shell-labels", "toolbar"];
29581
+ return ["height", "relation-mode", "shell-labels", "toolbar", "selection-mode"];
29565
29582
  }
29566
29583
  constructor() {
29567
29584
  super();
@@ -29581,6 +29598,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
29581
29598
  this.modelData = null;
29582
29599
  this.theme = normalizeTheme();
29583
29600
  this.relationMode = this.getAttribute("relation-mode") || "selected";
29601
+ this.selectionMode = this.getAttribute("selection-mode") || "internal";
29584
29602
  this.showShellLabels = this.getAttribute("shell-labels") !== "false";
29585
29603
  this.yaw = 0.37;
29586
29604
  this.pitch = 0.22;
@@ -29622,6 +29640,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
29622
29640
  this._toolbarVisible = value !== "off";
29623
29641
  if (this.ready) this._applyTheme();
29624
29642
  }
29643
+ if (name === "selection-mode") this.selectionMode = ["internal", "event"].includes(value) ? value : "internal";
29625
29644
  }
29626
29645
  get model() {
29627
29646
  return this.modelData;
@@ -29629,6 +29648,130 @@ var LiquidCarsAtlas = class extends HTMLElement {
29629
29648
  set model(value) {
29630
29649
  this.load(value);
29631
29650
  }
29651
+ get selectedId() {
29652
+ return this.selected?.id || null;
29653
+ }
29654
+ get clickBehavior() {
29655
+ return this.selectionMode;
29656
+ }
29657
+ setClickBehavior(value) {
29658
+ return this.setSelectionMode(value);
29659
+ }
29660
+ setSelectionMode(mode = "internal") {
29661
+ this.selectionMode = mode === "event" || mode === "external" ? "event" : "internal";
29662
+ this.setAttribute("selection-mode", this.selectionMode);
29663
+ return this;
29664
+ }
29665
+ _emit(type, detail = {}) {
29666
+ this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
29667
+ }
29668
+ _publicEntity(id) {
29669
+ const entity = this.modelData?.entities?.find((item) => item.id === id);
29670
+ return entity ? {
29671
+ ...entity,
29672
+ p: Array.isArray(entity.p) ? [...entity.p] : [0, 0, 0],
29673
+ s: Array.isArray(entity.s) ? [...entity.s] : [1, 1, 1]
29674
+ } : null;
29675
+ }
29676
+ _commitModelChange(kind, detail = {}, options = {}) {
29677
+ const selectedId = options.preserveSelection === false ? null : this.selected?.id;
29678
+ this.modelData = normalizeModel(this.modelData);
29679
+ this._build({ preserveSelection: selectedId });
29680
+ this._emit("lc-model-change", {
29681
+ kind,
29682
+ model: this.modelData,
29683
+ ...detail
29684
+ });
29685
+ return this;
29686
+ }
29687
+ /** Replace selected parts of the model and rebuild the scene in place. */
29688
+ updateModel(patch = {}, options = {}) {
29689
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) throw new TypeError("El parche del modelo debe ser un objeto.");
29690
+ const current = this.modelData || normalizeModel({});
29691
+ const next = {
29692
+ ...current,
29693
+ ...patch,
29694
+ palette: patch.palette ? { ...current.palette, ...patch.palette } : current.palette,
29695
+ theme: patch.theme || patch.styles ? patch.theme || patch.styles : current.theme,
29696
+ entities: Array.isArray(patch.entities) ? patch.entities : current.entities,
29697
+ relations: Array.isArray(patch.relations) ? patch.relations : current.relations
29698
+ };
29699
+ this.modelData = normalizeModel(next);
29700
+ this.theme = this.modelData.theme;
29701
+ return this._commitModelChange("model-update", { patch }, options);
29702
+ }
29703
+ addEntity(entity, options = {}) {
29704
+ if (!entity || typeof entity !== "object" || !entity.id) throw new TypeError("La entidad debe incluir un id.");
29705
+ if (!this.modelData) this.modelData = normalizeModel({});
29706
+ if (this.modelData.entities.some((item) => item.id === entity.id)) throw new Error(`Ya existe una entidad con id "${entity.id}".`);
29707
+ if (entity.parent && !this.modelData.entities.some((item) => item.id === entity.parent)) throw new Error(`No existe el contenedor padre "${entity.parent}".`);
29708
+ this.modelData.entities = [...this.modelData.entities, { ...entity }];
29709
+ return this._commitModelChange("entity-add", { entity: this._publicEntity(entity.id) }, options);
29710
+ }
29711
+ updateEntity(id, patch = {}, options = {}) {
29712
+ if (!this.modelData) throw new Error("No hay ning\xFAn modelo cargado.");
29713
+ const index = this.modelData.entities.findIndex((item) => item.id === id);
29714
+ if (index < 0) throw new Error(`No existe una entidad con id "${id}".`);
29715
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) throw new TypeError("Los cambios de una entidad deben ser un objeto.");
29716
+ if (patch.id && patch.id !== id) throw new Error("El id de una entidad no se puede modificar.");
29717
+ 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}".`);
29718
+ this.modelData.entities = this.modelData.entities.map((item, itemIndex) => itemIndex === index ? { ...item, ...patch, id } : item);
29719
+ return this._commitModelChange("entity-update", { id, patch, entity: this._publicEntity(id) }, options);
29720
+ }
29721
+ removeEntity(id, options = {}) {
29722
+ if (!this.modelData) throw new Error("No hay ning\xFAn modelo cargado.");
29723
+ const cascade = options.cascade !== false;
29724
+ const removed = /* @__PURE__ */ new Set([id]);
29725
+ if (cascade) {
29726
+ let changed = true;
29727
+ while (changed) {
29728
+ changed = false;
29729
+ for (const entity of this.modelData.entities) {
29730
+ if (entity.parent && removed.has(entity.parent) && !removed.has(entity.id)) {
29731
+ removed.add(entity.id);
29732
+ changed = true;
29733
+ }
29734
+ }
29735
+ }
29736
+ }
29737
+ if (!this.modelData.entities.some((item) => item.id === id)) throw new Error(`No existe una entidad con id "${id}".`);
29738
+ this.modelData.entities = this.modelData.entities.filter((item) => !removed.has(item.id));
29739
+ this.modelData.relations = this.modelData.relations.filter((relation) => !removed.has(relation.from) && !removed.has(relation.to));
29740
+ for (const removedId of removed) this.open.delete(removedId);
29741
+ if (this.selected && removed.has(this.selected.id)) this.selected = null;
29742
+ return this._commitModelChange("entity-remove", { id, removed: [...removed] });
29743
+ }
29744
+ _relationIndex(reference) {
29745
+ if (Number.isInteger(reference)) return reference;
29746
+ if (typeof reference === "string") return this.modelData?.relations?.findIndex((relation) => relation.id === reference) ?? -1;
29747
+ return -1;
29748
+ }
29749
+ addRelation(relation, options = {}) {
29750
+ if (!relation || typeof relation !== "object" || !relation.from || !relation.to) throw new TypeError("La relaci\xF3n debe incluir from y to.");
29751
+ if (!this.modelData) throw new Error("No hay ning\xFAn modelo cargado.");
29752
+ const ids = new Set(this.modelData.entities.map((entity) => entity.id));
29753
+ if (!ids.has(relation.from) || !ids.has(relation.to)) throw new Error("Los extremos de la relaci\xF3n deben existir en el modelo.");
29754
+ this.modelData.relations = [...this.modelData.relations, { ...relation }];
29755
+ return this._commitModelChange("relation-add", { relation: { ...relation } }, options);
29756
+ }
29757
+ updateRelation(reference, patch = {}, options = {}) {
29758
+ if (!this.modelData) throw new Error("No hay ning\xFAn modelo cargado.");
29759
+ const index = this._relationIndex(reference);
29760
+ if (index < 0 || index >= this.modelData.relations.length) throw new Error("No existe la relaci\xF3n indicada.");
29761
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) throw new TypeError("Los cambios de una relaci\xF3n deben ser un objeto.");
29762
+ const next = { ...this.modelData.relations[index], ...patch };
29763
+ const ids = new Set(this.modelData.entities.map((entity) => entity.id));
29764
+ if (!ids.has(next.from) || !ids.has(next.to)) throw new Error("Los extremos de la relaci\xF3n deben existir en el modelo.");
29765
+ this.modelData.relations = this.modelData.relations.map((item, itemIndex) => itemIndex === index ? next : item);
29766
+ return this._commitModelChange("relation-update", { index, patch, relation: { ...next } }, options);
29767
+ }
29768
+ removeRelation(reference, options = {}) {
29769
+ if (!this.modelData) throw new Error("No hay ning\xFAn modelo cargado.");
29770
+ const index = this._relationIndex(reference);
29771
+ if (index < 0 || index >= this.modelData.relations.length) throw new Error("No existe la relaci\xF3n indicada.");
29772
+ const [relation] = this.modelData.relations.splice(index, 1);
29773
+ return this._commitModelChange("relation-remove", { index, relation: { ...relation } }, options);
29774
+ }
29632
29775
  get palette() {
29633
29776
  return this.modelData?.palette || { ...DEFAULT_PALETTE };
29634
29777
  }
@@ -29802,6 +29945,8 @@ var LiquidCarsAtlas = class extends HTMLElement {
29802
29945
  try {
29803
29946
  const data = typeof source === "string" ? await fetch(source).then((response) => response.json()) : source;
29804
29947
  this.modelData = normalizeModel(data);
29948
+ this.open.clear();
29949
+ this.selected = null;
29805
29950
  if (this._pendingPalette) {
29806
29951
  this.modelData.palette = { ...this.modelData.palette, ...this._pendingPalette };
29807
29952
  this._pendingPalette = null;
@@ -29861,6 +30006,14 @@ var LiquidCarsAtlas = class extends HTMLElement {
29861
30006
  this.scene.remove(node.group);
29862
30007
  }
29863
30008
  this.nodes.clear();
30009
+ this._clearRelationGroup();
30010
+ }
30011
+ _clearRelationGroup() {
30012
+ for (const object of this.relationGroup.children) {
30013
+ object.geometry?.dispose();
30014
+ if (Array.isArray(object.material)) object.material.forEach((material) => material.dispose());
30015
+ else object.material?.dispose();
30016
+ }
29864
30017
  this.relationGroup.clear();
29865
30018
  }
29866
30019
  _recolorNodes() {
@@ -29871,10 +30024,12 @@ var LiquidCarsAtlas = class extends HTMLElement {
29871
30024
  if (node.edge?.material) node.edge.material.color.set(color);
29872
30025
  }
29873
30026
  }
29874
- _build() {
30027
+ _build(options = {}) {
29875
30028
  if (!this._ensureRenderer()) return;
30029
+ const selectedId = options.preserveSelection || null;
29876
30030
  this._clearObjects();
29877
30031
  for (const entity of this.modelData.entities) this.nodes.set(entity.id, { ...entity });
30032
+ this.open = new Set([...this.open].filter((id) => this.nodes.has(id)));
29878
30033
  for (const entity of this.modelData.entities) {
29879
30034
  const node = this.nodes.get(entity.id);
29880
30035
  node.group = new Group();
@@ -29906,6 +30061,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
29906
30061
  node.group.add(node.label);
29907
30062
  this.scene.add(node.group);
29908
30063
  }
30064
+ this.selected = selectedId && this.nodes.has(selectedId) ? this.nodes.get(selectedId) : null;
29909
30065
  this.relations = this.modelData.relations;
29910
30066
  this._renderPanel();
29911
30067
  this._rebuildRelations();
@@ -29945,7 +30101,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
29945
30101
  return this.relations.map((relation, index) => relation.from === this.selected.id || relation.to === this.selected.id ? index : -1).filter((index) => index >= 0);
29946
30102
  }
29947
30103
  _rebuildRelations() {
29948
- this.relationGroup.clear();
30104
+ this._clearRelationGroup();
29949
30105
  const active = this._activeRelationIndexes();
29950
30106
  const color = new Color(this.modelData?.palette.connection || DEFAULT_PALETTE.connection);
29951
30107
  for (const index of active) {
@@ -29957,14 +30113,28 @@ var LiquidCarsAtlas = class extends HTMLElement {
29957
30113
  const end = this._endpoint(to, from, false);
29958
30114
  const delta = end.clone().sub(start);
29959
30115
  if (delta.lengthSq() < 0.01) continue;
29960
- const tube = new Mesh(new TubeGeometry(new LineCurve3(start, end), 10, 0.045, 6, false), new MeshBasicMaterial({ color, transparent: true, opacity: 0.82 }));
29961
- this.relationGroup.add(tube);
30116
+ const visualMode = relationVisualMode(relation);
30117
+ const makeTube = (from2, to2, opacity = 0.82) => {
30118
+ const tube = new Mesh(new TubeGeometry(new LineCurve3(from2, to2), 10, 0.045, 6, false), new MeshBasicMaterial({ color, transparent: true, opacity }));
30119
+ tube.userData.relationIndex = index;
30120
+ this.relationGroup.add(tube);
30121
+ };
30122
+ if (visualMode === "broken") {
30123
+ const direction = delta.clone().normalize();
30124
+ const midpoint = start.clone().lerp(end, 0.5);
30125
+ const gap = Math.min(delta.length() * 0.16, 0.72);
30126
+ makeTube(start, midpoint.clone().sub(direction.clone().multiplyScalar(gap / 2)), 0.55);
30127
+ makeTube(midpoint.clone().add(direction.clone().multiplyScalar(gap / 2)), end, 0.55);
30128
+ continue;
30129
+ }
30130
+ makeTube(start, end);
29962
30131
  for (let ringIndex = 0; ringIndex < 4; ringIndex++) {
29963
30132
  const ring = new Mesh(new TorusGeometry(0.068, 0.018, 8, 20), new MeshBasicMaterial({ color, transparent: true, opacity: 0.9 }));
29964
30133
  ring.userData.phase = ringIndex / 4;
30134
+ ring.userData.direction = visualMode === "bidirectional" && ringIndex % 2 ? -1 : 1;
29965
30135
  ring.userData.start = start.clone();
29966
30136
  ring.userData.end = end.clone();
29967
- ring.quaternion.setFromUnitVectors(new Vector3(0, 0, 1), delta.normalize());
30137
+ ring.quaternion.setFromUnitVectors(new Vector3(0, 0, 1), delta.clone().normalize());
29968
30138
  this.relationGroup.add(ring);
29969
30139
  }
29970
30140
  }
@@ -30012,7 +30182,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
30012
30182
  });
30013
30183
  this._applyTheme();
30014
30184
  }
30015
- select(id) {
30185
+ select(id, options = {}) {
30016
30186
  const node = this.nodes.get(id);
30017
30187
  if (!node) return;
30018
30188
  let parent = node.parent ? this.nodes.get(node.parent) : null;
@@ -30027,7 +30197,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
30027
30197
  this._renderPanel();
30028
30198
  this._rebuildRelations();
30029
30199
  this.invalidate();
30030
- this.dispatchEvent(new CustomEvent("lc-select", { detail: { id, entity: node } }));
30200
+ this._emit("lc-select", { id, entity: this._publicEntity(id), source: options.source || "api" });
30031
30201
  }
30032
30202
  setRelationMode(mode) {
30033
30203
  this.relationMode = ["all", "selected", "none"].includes(mode) ? mode : "selected";
@@ -30045,6 +30215,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
30045
30215
  this.dispatchEvent(new CustomEvent("lc-relation-mode", { detail: { mode: this.relationMode } }));
30046
30216
  }
30047
30217
  reset() {
30218
+ const previousId = this.selected?.id || null;
30048
30219
  this.selected = null;
30049
30220
  this.open.clear();
30050
30221
  this.relationMode = "selected";
@@ -30067,6 +30238,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
30067
30238
  this._renderPanel();
30068
30239
  this._rebuildRelations();
30069
30240
  this.invalidate();
30241
+ if (previousId) this._emit("lc-select", { id: null, entity: null, previousId, source: "reset" });
30070
30242
  }
30071
30243
  resize() {
30072
30244
  if (!this.renderer) return;
@@ -30125,7 +30297,10 @@ var LiquidCarsAtlas = class extends HTMLElement {
30125
30297
  top.visible = displayTransparentLabel && cameraAbove;
30126
30298
  const scale = displayTransparentLabel ? 0.52 : 1;
30127
30299
  front.scale.setScalar(scale);
30128
- front.position.x = displayTransparentLabel ? -labelSize.x / 2 + 0.18 + labelWidth * scale / 2 - textOffsetX * scale : 0;
30300
+ back.scale.setScalar(scale);
30301
+ const openLabelX = displayTransparentLabel ? -labelSize.x / 2 + 0.18 + labelWidth * scale / 2 - textOffsetX * scale : 0;
30302
+ front.position.x = openLabelX;
30303
+ back.position.x = displayTransparentLabel ? -openLabelX : 0;
30129
30304
  const shellFrontTopMargin = 0.18;
30130
30305
  front.position.y = displayTransparentLabel ? labelSize.y / 2 - shellFrontTopMargin - labelHeight * scale / 2 : labelSize.y / 2 - 0.09 - labelHeight / 2;
30131
30306
  front.position.z = displayTransparentLabel ? labelSize.z / 2 + 0.025 : labelSize.z / 2 + 0.012;
@@ -30145,16 +30320,23 @@ var LiquidCarsAtlas = class extends HTMLElement {
30145
30320
  back.visible = !cameraOnFront && !opened;
30146
30321
  top.visible = false;
30147
30322
  front.scale.setScalar(1);
30323
+ back.scale.setScalar(1);
30148
30324
  front.position.x = 0;
30325
+ back.position.x = 0;
30149
30326
  front.position.y = labelSize.y / 2 - 0.09 - labelHeight / 2;
30327
+ back.position.y = front.position.y;
30328
+ back.position.z = -labelSize.z / 2 - 0.012;
30150
30329
  front.material.color.set(15923964);
30151
30330
  front.material.opacity = 1;
30331
+ back.material.color.set(15923964);
30332
+ back.material.opacity = 1;
30152
30333
  }
30153
30334
  }
30154
30335
  for (const child of this.relationGroup.children) {
30155
30336
  if (!child.userData.start) continue;
30156
30337
  const phase = child.userData.phase || 0;
30157
- const factor = (time * 22e-5 + phase) % 1;
30338
+ const direction = child.userData.direction || 1;
30339
+ const factor = ((time * 22e-5 * direction + phase) % 1 + 1) % 1;
30158
30340
  child.position.lerpVectors(child.userData.start, child.userData.end, factor);
30159
30341
  }
30160
30342
  this.renderer.render(this.scene, this.camera);
@@ -30208,7 +30390,10 @@ var LiquidCarsAtlas = class extends HTMLElement {
30208
30390
  this.canvas.style.cursor = "grab";
30209
30391
  if (drag && !drag.moved) {
30210
30392
  const node = this._pick(event);
30211
- if (node) this.select(node.id);
30393
+ if (node) {
30394
+ if (this.selectionMode === "event") this._emit("lc-select", { id: node.id, entity: this._publicEntity(node.id), source: "pointer" });
30395
+ else this.select(node.id, { source: "pointer" });
30396
+ }
30212
30397
  }
30213
30398
  this.invalidate();
30214
30399
  });
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.15"></script>
17
+ <script type="module" src="./dist/liquidcars-atlas.js?v=0.1.17"></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.15</p>
22
+ <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</code> · v0.1.17</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.15",
3
+ "version": "0.1.17",
4
4
  "description": "Reusable 3D conceptual atlas Web Component for LiquidCars-style models",
5
5
  "type": "module",
6
6
  "main": "./dist/liquidcars-atlas.js",