@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 +41 -3
- package/demo/index.html +2 -2
- package/dist/liquidcars-atlas.js +197 -12
- package/index.html +2 -2
- package/package.json +1 -1
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-
|
|
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.
|
|
144
|
+
npm install ./liquidcars-atlas-0.1.17.tgz
|
|
107
145
|
```
|
|
108
146
|
|
|
109
147
|
## Estado
|
|
110
148
|
|
|
111
|
-
`0.1.
|
|
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.
|
|
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><liquidcars-atlas></code> · v0.1.
|
|
22
|
+
<p>Prueba de distribución del componente <code><liquidcars-atlas></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/dist/liquidcars-atlas.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
/*! LiquidCars Atlas 0.1.
|
|
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.
|
|
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
|
|
29961
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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)
|
|
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.
|
|
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><liquidcars-atlas></code> · v0.1.
|
|
22
|
+
<p>Prueba de distribución del componente <code><liquidcars-atlas></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>
|