@liquidcars/atlas 0.1.15 → 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 +33 -2
- package/dist/liquidcars-atlas.js +180 -11
- package/package.json +1 -1
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-
|
|
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:
|
|
@@ -108,4 +139,4 @@ npm install ./liquidcars-atlas-0.1.15.tgz
|
|
|
108
139
|
|
|
109
140
|
## Estado
|
|
110
141
|
|
|
111
|
-
`0.1.
|
|
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/dist/liquidcars-atlas.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
/*! LiquidCars Atlas 0.1.
|
|
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;
|
|
@@ -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);
|
|
@@ -29629,6 +29646,119 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29629
29646
|
set model(value) {
|
|
29630
29647
|
this.load(value);
|
|
29631
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
|
+
}
|
|
29632
29762
|
get palette() {
|
|
29633
29763
|
return this.modelData?.palette || { ...DEFAULT_PALETTE };
|
|
29634
29764
|
}
|
|
@@ -29802,6 +29932,8 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29802
29932
|
try {
|
|
29803
29933
|
const data = typeof source === "string" ? await fetch(source).then((response) => response.json()) : source;
|
|
29804
29934
|
this.modelData = normalizeModel(data);
|
|
29935
|
+
this.open.clear();
|
|
29936
|
+
this.selected = null;
|
|
29805
29937
|
if (this._pendingPalette) {
|
|
29806
29938
|
this.modelData.palette = { ...this.modelData.palette, ...this._pendingPalette };
|
|
29807
29939
|
this._pendingPalette = null;
|
|
@@ -29861,6 +29993,14 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29861
29993
|
this.scene.remove(node.group);
|
|
29862
29994
|
}
|
|
29863
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
|
+
}
|
|
29864
30004
|
this.relationGroup.clear();
|
|
29865
30005
|
}
|
|
29866
30006
|
_recolorNodes() {
|
|
@@ -29871,10 +30011,12 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29871
30011
|
if (node.edge?.material) node.edge.material.color.set(color);
|
|
29872
30012
|
}
|
|
29873
30013
|
}
|
|
29874
|
-
_build() {
|
|
30014
|
+
_build(options = {}) {
|
|
29875
30015
|
if (!this._ensureRenderer()) return;
|
|
30016
|
+
const selectedId = options.preserveSelection || null;
|
|
29876
30017
|
this._clearObjects();
|
|
29877
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)));
|
|
29878
30020
|
for (const entity of this.modelData.entities) {
|
|
29879
30021
|
const node = this.nodes.get(entity.id);
|
|
29880
30022
|
node.group = new Group();
|
|
@@ -29906,6 +30048,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29906
30048
|
node.group.add(node.label);
|
|
29907
30049
|
this.scene.add(node.group);
|
|
29908
30050
|
}
|
|
30051
|
+
this.selected = selectedId && this.nodes.has(selectedId) ? this.nodes.get(selectedId) : null;
|
|
29909
30052
|
this.relations = this.modelData.relations;
|
|
29910
30053
|
this._renderPanel();
|
|
29911
30054
|
this._rebuildRelations();
|
|
@@ -29945,7 +30088,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29945
30088
|
return this.relations.map((relation, index) => relation.from === this.selected.id || relation.to === this.selected.id ? index : -1).filter((index) => index >= 0);
|
|
29946
30089
|
}
|
|
29947
30090
|
_rebuildRelations() {
|
|
29948
|
-
this.
|
|
30091
|
+
this._clearRelationGroup();
|
|
29949
30092
|
const active = this._activeRelationIndexes();
|
|
29950
30093
|
const color = new Color(this.modelData?.palette.connection || DEFAULT_PALETTE.connection);
|
|
29951
30094
|
for (const index of active) {
|
|
@@ -29957,14 +30100,28 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29957
30100
|
const end = this._endpoint(to, from, false);
|
|
29958
30101
|
const delta = end.clone().sub(start);
|
|
29959
30102
|
if (delta.lengthSq() < 0.01) continue;
|
|
29960
|
-
const
|
|
29961
|
-
|
|
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);
|
|
29962
30118
|
for (let ringIndex = 0; ringIndex < 4; ringIndex++) {
|
|
29963
30119
|
const ring = new Mesh(new TorusGeometry(0.068, 0.018, 8, 20), new MeshBasicMaterial({ color, transparent: true, opacity: 0.9 }));
|
|
29964
30120
|
ring.userData.phase = ringIndex / 4;
|
|
30121
|
+
ring.userData.direction = visualMode === "bidirectional" && ringIndex % 2 ? -1 : 1;
|
|
29965
30122
|
ring.userData.start = start.clone();
|
|
29966
30123
|
ring.userData.end = end.clone();
|
|
29967
|
-
ring.quaternion.setFromUnitVectors(new Vector3(0, 0, 1), delta.normalize());
|
|
30124
|
+
ring.quaternion.setFromUnitVectors(new Vector3(0, 0, 1), delta.clone().normalize());
|
|
29968
30125
|
this.relationGroup.add(ring);
|
|
29969
30126
|
}
|
|
29970
30127
|
}
|
|
@@ -30012,7 +30169,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30012
30169
|
});
|
|
30013
30170
|
this._applyTheme();
|
|
30014
30171
|
}
|
|
30015
|
-
select(id) {
|
|
30172
|
+
select(id, options = {}) {
|
|
30016
30173
|
const node = this.nodes.get(id);
|
|
30017
30174
|
if (!node) return;
|
|
30018
30175
|
let parent = node.parent ? this.nodes.get(node.parent) : null;
|
|
@@ -30027,7 +30184,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30027
30184
|
this._renderPanel();
|
|
30028
30185
|
this._rebuildRelations();
|
|
30029
30186
|
this.invalidate();
|
|
30030
|
-
this.
|
|
30187
|
+
this._emit("lc-select", { id, entity: this._publicEntity(id), source: options.source || "api" });
|
|
30031
30188
|
}
|
|
30032
30189
|
setRelationMode(mode) {
|
|
30033
30190
|
this.relationMode = ["all", "selected", "none"].includes(mode) ? mode : "selected";
|
|
@@ -30045,6 +30202,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30045
30202
|
this.dispatchEvent(new CustomEvent("lc-relation-mode", { detail: { mode: this.relationMode } }));
|
|
30046
30203
|
}
|
|
30047
30204
|
reset() {
|
|
30205
|
+
const previousId = this.selected?.id || null;
|
|
30048
30206
|
this.selected = null;
|
|
30049
30207
|
this.open.clear();
|
|
30050
30208
|
this.relationMode = "selected";
|
|
@@ -30067,6 +30225,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30067
30225
|
this._renderPanel();
|
|
30068
30226
|
this._rebuildRelations();
|
|
30069
30227
|
this.invalidate();
|
|
30228
|
+
if (previousId) this._emit("lc-select", { id: null, entity: null, previousId, source: "reset" });
|
|
30070
30229
|
}
|
|
30071
30230
|
resize() {
|
|
30072
30231
|
if (!this.renderer) return;
|
|
@@ -30125,7 +30284,10 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30125
30284
|
top.visible = displayTransparentLabel && cameraAbove;
|
|
30126
30285
|
const scale = displayTransparentLabel ? 0.52 : 1;
|
|
30127
30286
|
front.scale.setScalar(scale);
|
|
30128
|
-
|
|
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;
|
|
30129
30291
|
const shellFrontTopMargin = 0.18;
|
|
30130
30292
|
front.position.y = displayTransparentLabel ? labelSize.y / 2 - shellFrontTopMargin - labelHeight * scale / 2 : labelSize.y / 2 - 0.09 - labelHeight / 2;
|
|
30131
30293
|
front.position.z = displayTransparentLabel ? labelSize.z / 2 + 0.025 : labelSize.z / 2 + 0.012;
|
|
@@ -30145,16 +30307,23 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30145
30307
|
back.visible = !cameraOnFront && !opened;
|
|
30146
30308
|
top.visible = false;
|
|
30147
30309
|
front.scale.setScalar(1);
|
|
30310
|
+
back.scale.setScalar(1);
|
|
30148
30311
|
front.position.x = 0;
|
|
30312
|
+
back.position.x = 0;
|
|
30149
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;
|
|
30150
30316
|
front.material.color.set(15923964);
|
|
30151
30317
|
front.material.opacity = 1;
|
|
30318
|
+
back.material.color.set(15923964);
|
|
30319
|
+
back.material.opacity = 1;
|
|
30152
30320
|
}
|
|
30153
30321
|
}
|
|
30154
30322
|
for (const child of this.relationGroup.children) {
|
|
30155
30323
|
if (!child.userData.start) continue;
|
|
30156
30324
|
const phase = child.userData.phase || 0;
|
|
30157
|
-
const
|
|
30325
|
+
const direction = child.userData.direction || 1;
|
|
30326
|
+
const factor = ((time * 22e-5 * direction + phase) % 1 + 1) % 1;
|
|
30158
30327
|
child.position.lerpVectors(child.userData.start, child.userData.end, factor);
|
|
30159
30328
|
}
|
|
30160
30329
|
this.renderer.render(this.scene, this.camera);
|
|
@@ -30208,7 +30377,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30208
30377
|
this.canvas.style.cursor = "grab";
|
|
30209
30378
|
if (drag && !drag.moved) {
|
|
30210
30379
|
const node = this._pick(event);
|
|
30211
|
-
if (node) this.select(node.id);
|
|
30380
|
+
if (node) this.select(node.id, { source: "pointer" });
|
|
30212
30381
|
}
|
|
30213
30382
|
this.invalidate();
|
|
30214
30383
|
});
|