@liquidcars/atlas 0.1.21 → 0.1.23
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 +27 -8
- package/dist/liquidcars-atlas.js +165 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,10 +50,13 @@ También se puede cargar el modelo desde JavaScript:
|
|
|
50
50
|
const atlas = document.querySelector('liquidcars-atlas');
|
|
51
51
|
atlas.model = model;
|
|
52
52
|
atlas.select('distribution-agreement');
|
|
53
|
+
atlas.selectRelation(0); // también acepta el id de una relación
|
|
53
54
|
atlas.setRelationMode('all'); // 'all' | 'selected' | 'none'
|
|
54
55
|
atlas.setToolbarVisible(true); // también se puede usar toolbar="on" | "off"
|
|
55
56
|
atlas.zoomIn();
|
|
56
57
|
atlas.zoomOut();
|
|
58
|
+
atlas.panBy(-1.5, 0); // desplazamiento horizontal/vertical en el plano de cámara
|
|
59
|
+
atlas.panTo(0, 0, 0); // centra la cámara en un punto del mundo
|
|
57
60
|
atlas.fit();
|
|
58
61
|
atlas.frontView();
|
|
59
62
|
atlas.setShellLabels(true);
|
|
@@ -76,7 +79,11 @@ atlas.removeRelation(0);
|
|
|
76
79
|
atlas.removeEntity('service');
|
|
77
80
|
|
|
78
81
|
atlas.addEventListener('lc-select', event => {
|
|
79
|
-
|
|
82
|
+
if (event.detail.kind === 'entity') {
|
|
83
|
+
console.log('Entidad seleccionada:', event.detail.id, event.detail.entity);
|
|
84
|
+
} else if (event.detail.kind === 'relation') {
|
|
85
|
+
console.log('Relación seleccionada:', event.detail.index, event.detail.relation);
|
|
86
|
+
}
|
|
80
87
|
});
|
|
81
88
|
atlas.addEventListener('lc-model-change', event => {
|
|
82
89
|
console.log('Modelo actualizado:', event.detail.kind);
|
|
@@ -90,9 +97,11 @@ por lo que una aplicación puede adaptar la apariencia sin modificar el bundle.
|
|
|
90
97
|
Las propiedades `atlas.theme` y `atlas.styles` son alias prácticos de esta configuración.
|
|
91
98
|
|
|
92
99
|
Eventos disponibles: `lc-ready`, `lc-error`, `lc-select`, `lc-model-change`,
|
|
93
|
-
`lc-relation-mode` y `lc-disconnect`. `lc-select`
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
`lc-relation-mode` y `lc-disconnect`. `lc-select` usa un detalle discriminado:
|
|
101
|
+
las entidades incluyen `{ kind: 'entity', id, entity, source }` y las relaciones
|
|
102
|
+
`{ kind: 'relation', id, index, relation, source }`. Los campos históricos `id`
|
|
103
|
+
y `entity` se conservan para las selecciones de entidad. Al volver a la vista
|
|
104
|
+
general, `kind`, `id`, `entity` y `relation` son `null`. Los eventos de selección y cambios de modelo son `bubbles` y
|
|
96
105
|
`composed`, por lo que funcionan también al integrar el componente en otras
|
|
97
106
|
capas de Web Components.
|
|
98
107
|
|
|
@@ -113,13 +122,14 @@ como `setClickBehavior()`).
|
|
|
113
122
|
El campo visual opcional `mode` admite `directed` (por defecto),
|
|
114
123
|
`bidirectional` y `broken`. En modo bidireccional los indicadores animados
|
|
115
124
|
circulan en ambos sentidos por el mismo tubo; en modo broken los dos tramos
|
|
116
|
-
|
|
125
|
+
se sustituyen por un único tubo rojo con tres anillos que vibran localmente,
|
|
126
|
+
distribuidos a lo largo del recorrido.
|
|
117
127
|
|
|
118
128
|
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.
|
|
119
129
|
|
|
120
130
|
La paleta puede cambiarse después de cargar el modelo mediante `atlas.setPalette({ nombre: color })` o asignando `atlas.palette = { ... }`. El cambio actualiza las piezas, aristas y relaciones sin reconstruir el modelo.
|
|
121
131
|
|
|
122
|
-
Las acciones de navegación y exploración
|
|
132
|
+
Las acciones de navegación y exploración también están disponibles para la página anfitriona: `zoomIn()`, `zoomOut()`, `panBy()`, `panTo()`, `fit()`, `frontView()`, `setShellLabels()`, `toggleShellLabels()`, `openAllContainers()` y `closeAllContainers()`. El usuario puede desplazar la vista con el botón derecho, con `Shift` + arrastre o mediante dos dedos en una pantalla táctil. Así se puede construir una barra de controles propia sin depender de la toolbar interna.
|
|
123
133
|
|
|
124
134
|
La escena también se puede editar en caliente. `updateModel(patch)` reemplaza
|
|
125
135
|
las secciones indicadas; `addEntity`, `updateEntity` y `removeEntity` gestionan
|
|
@@ -141,9 +151,18 @@ Una vez publicado, los consumidores podrán instalarlo con `npm install @liquidc
|
|
|
141
151
|
Para probar el paquete sin publicarlo, basta con instalar el tarball generado:
|
|
142
152
|
|
|
143
153
|
```bash
|
|
144
|
-
npm install ./liquidcars-atlas-0.1.
|
|
154
|
+
npm install ./liquidcars-atlas-0.1.22.tgz
|
|
145
155
|
```
|
|
146
156
|
|
|
147
157
|
## Estado
|
|
148
158
|
|
|
149
|
-
`0.1.
|
|
159
|
+
`0.1.23` añade desplazamiento lateral de cámara mediante ratón, teclado
|
|
160
|
+
modificador y gestos táctiles, además de la API pública `panBy()` y `panTo()`.
|
|
161
|
+
Se conserva la selección contextual de relaciones introducida en `0.1.22`: el usuario puede
|
|
162
|
+
seleccionar un tubo directamente, el componente lo realza, muestra sus extremos
|
|
163
|
+
y modo en el panel y emite el mismo evento `lc-select` con `kind: 'relation'`.
|
|
164
|
+
La API pública añade `selectRelation()`, `selectedRelation` y `selection`, y la
|
|
165
|
+
selección se conserva durante las mutaciones del modelo siempre que el elemento
|
|
166
|
+
siga existiendo. Se mantienen las correcciones de geometría, toolbar, etiquetas,
|
|
167
|
+
paleta externa, edición en caliente y los modos `directed`, `bidirectional` y
|
|
168
|
+
`broken` introducidos en las versiones anteriores.
|
package/dist/liquidcars-atlas.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! LiquidCars Atlas 0.1.
|
|
1
|
+
/*! LiquidCars Atlas 0.1.22 | MIT */
|
|
2
2
|
|
|
3
3
|
// src/three.core.js
|
|
4
4
|
/**
|
|
@@ -29371,7 +29371,7 @@ var CSS = [
|
|
|
29371
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}",
|
|
29372
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}}"
|
|
29373
29373
|
].join("");
|
|
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">
|
|
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">Gira: arrastra \xB7 Pan: \u21E7+arrastra / 2 dedos \xB7 Zoom: rueda o pellizco \xB7 Selecciona: clic</div></section><aside class="panel" aria-live="polite"></aside></div>';
|
|
29375
29375
|
function escapeHtml(value) {
|
|
29376
29376
|
return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]);
|
|
29377
29377
|
}
|
|
@@ -29595,6 +29595,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29595
29595
|
this.nodes = /* @__PURE__ */ new Map();
|
|
29596
29596
|
this.open = /* @__PURE__ */ new Set();
|
|
29597
29597
|
this.selected = null;
|
|
29598
|
+
this.selectedRelationIndex = null;
|
|
29598
29599
|
this.modelData = null;
|
|
29599
29600
|
this.theme = normalizeTheme();
|
|
29600
29601
|
this.relationMode = this.getAttribute("relation-mode") || "selected";
|
|
@@ -29607,6 +29608,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29607
29608
|
this.focus = new Vector3();
|
|
29608
29609
|
this.targetFocus = new Vector3();
|
|
29609
29610
|
this.drag = null;
|
|
29611
|
+
this.pointers = /* @__PURE__ */ new Map();
|
|
29610
29612
|
this.relationGroup = new Group();
|
|
29611
29613
|
this.scene.add(this.relationGroup);
|
|
29612
29614
|
this.relations = [];
|
|
@@ -29651,6 +29653,14 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29651
29653
|
get selectedId() {
|
|
29652
29654
|
return this.selected?.id || null;
|
|
29653
29655
|
}
|
|
29656
|
+
get selectedRelation() {
|
|
29657
|
+
return this._publicRelation(this.selectedRelationIndex);
|
|
29658
|
+
}
|
|
29659
|
+
get selection() {
|
|
29660
|
+
if (this.selected) return { kind: "entity", id: this.selected.id, entity: this._publicEntity(this.selected.id) };
|
|
29661
|
+
const relation = this.selectedRelation;
|
|
29662
|
+
return relation ? { kind: "relation", id: relation.id || null, index: relation.index, relation } : null;
|
|
29663
|
+
}
|
|
29654
29664
|
get clickBehavior() {
|
|
29655
29665
|
return this.selectionMode;
|
|
29656
29666
|
}
|
|
@@ -29673,10 +29683,17 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29673
29683
|
s: Array.isArray(entity.s) ? [...entity.s] : [1, 1, 1]
|
|
29674
29684
|
} : null;
|
|
29675
29685
|
}
|
|
29686
|
+
_publicRelation(reference) {
|
|
29687
|
+
const index = this._relationIndex(reference);
|
|
29688
|
+
const relation = this.modelData?.relations?.[index];
|
|
29689
|
+
return relation ? { ...relation, index } : null;
|
|
29690
|
+
}
|
|
29676
29691
|
_commitModelChange(kind, detail = {}, options = {}) {
|
|
29677
29692
|
const selectedId = options.preserveSelection === false ? null : this.selected?.id;
|
|
29693
|
+
const selectedRelation = options.preserveSelection === false ? null : this._publicRelation(this.selectedRelationIndex);
|
|
29694
|
+
const selectedRelationReference = selectedRelation?.id ?? selectedRelation?.index ?? null;
|
|
29678
29695
|
this.modelData = normalizeModel(this.modelData);
|
|
29679
|
-
this._build({ preserveSelection: selectedId });
|
|
29696
|
+
this._build({ preserveSelection: selectedId, preserveRelation: selectedRelationReference });
|
|
29680
29697
|
this._emit("lc-model-change", {
|
|
29681
29698
|
kind,
|
|
29682
29699
|
model: this.modelData,
|
|
@@ -29735,8 +29752,11 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29735
29752
|
}
|
|
29736
29753
|
}
|
|
29737
29754
|
if (!this.modelData.entities.some((item) => item.id === id)) throw new Error(`No existe una entidad con id "${id}".`);
|
|
29755
|
+
const selectedRelation = this.modelData.relations[this.selectedRelationIndex];
|
|
29738
29756
|
this.modelData.entities = this.modelData.entities.filter((item) => !removed.has(item.id));
|
|
29739
29757
|
this.modelData.relations = this.modelData.relations.filter((relation) => !removed.has(relation.from) && !removed.has(relation.to));
|
|
29758
|
+
const nextSelectedRelationIndex = selectedRelation ? this.modelData.relations.indexOf(selectedRelation) : -1;
|
|
29759
|
+
this.selectedRelationIndex = nextSelectedRelationIndex >= 0 ? nextSelectedRelationIndex : null;
|
|
29740
29760
|
for (const removedId of removed) this.open.delete(removedId);
|
|
29741
29761
|
if (this.selected && removed.has(this.selected.id)) this.selected = null;
|
|
29742
29762
|
return this._commitModelChange("entity-remove", { id, removed: [...removed] });
|
|
@@ -29770,6 +29790,8 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29770
29790
|
const index = this._relationIndex(reference);
|
|
29771
29791
|
if (index < 0 || index >= this.modelData.relations.length) throw new Error("No existe la relaci\xF3n indicada.");
|
|
29772
29792
|
const [relation] = this.modelData.relations.splice(index, 1);
|
|
29793
|
+
if (this.selectedRelationIndex === index) this.selectedRelationIndex = null;
|
|
29794
|
+
else if (this.selectedRelationIndex > index) this.selectedRelationIndex -= 1;
|
|
29773
29795
|
return this._commitModelChange("relation-remove", { index, relation: { ...relation } }, options);
|
|
29774
29796
|
}
|
|
29775
29797
|
get palette() {
|
|
@@ -29809,6 +29831,27 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29809
29831
|
this.invalidate();
|
|
29810
29832
|
return this;
|
|
29811
29833
|
}
|
|
29834
|
+
panBy(horizontal = 0, vertical = 0) {
|
|
29835
|
+
const direction = this.camera.getWorldDirection(new Vector3());
|
|
29836
|
+
const right = new Vector3().crossVectors(direction, this.camera.up).normalize();
|
|
29837
|
+
const up = new Vector3().crossVectors(right, direction).normalize();
|
|
29838
|
+
this.targetFocus.addScaledVector(right, Number(horizontal) || 0);
|
|
29839
|
+
this.targetFocus.addScaledVector(up, Number(vertical) || 0);
|
|
29840
|
+
this.invalidate();
|
|
29841
|
+
return this;
|
|
29842
|
+
}
|
|
29843
|
+
panTo(x = 0, y = 0, z = 0) {
|
|
29844
|
+
const value = Array.isArray(x) ? x : x && typeof x === "object" ? [x.x, x.y, x.z] : [x, y, z];
|
|
29845
|
+
this.targetFocus.set(Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0);
|
|
29846
|
+
this.invalidate();
|
|
29847
|
+
return this;
|
|
29848
|
+
}
|
|
29849
|
+
_panByPixels(dx, dy) {
|
|
29850
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
29851
|
+
const distance = Math.max(1, this.camera.position.distanceTo(this.focus));
|
|
29852
|
+
const worldPerPixel = 2 * distance * Math.tan(MathUtils.degToRad(this.camera.fov) / 2) / Math.max(1, rect.height);
|
|
29853
|
+
return this.panBy(-dx * worldPerPixel, dy * worldPerPixel);
|
|
29854
|
+
}
|
|
29812
29855
|
fit() {
|
|
29813
29856
|
const bounds = new Box3();
|
|
29814
29857
|
for (const node of this.nodes.values()) {
|
|
@@ -29858,6 +29901,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
29858
29901
|
closeAllContainers() {
|
|
29859
29902
|
this.open.clear();
|
|
29860
29903
|
this.selected = null;
|
|
29904
|
+
this.selectedRelationIndex = null;
|
|
29861
29905
|
this._renderPanel();
|
|
29862
29906
|
this._rebuildRelations();
|
|
29863
29907
|
this.invalidate();
|
|
@@ -30027,6 +30071,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30027
30071
|
_build(options = {}) {
|
|
30028
30072
|
if (!this._ensureRenderer()) return;
|
|
30029
30073
|
const selectedId = options.preserveSelection || null;
|
|
30074
|
+
const selectedRelationReference = options.preserveRelation ?? null;
|
|
30030
30075
|
this._clearObjects();
|
|
30031
30076
|
for (const entity of this.modelData.entities) this.nodes.set(entity.id, { ...entity });
|
|
30032
30077
|
this.open = new Set([...this.open].filter((id) => this.nodes.has(id)));
|
|
@@ -30061,8 +30106,10 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30061
30106
|
node.group.add(node.label);
|
|
30062
30107
|
this.scene.add(node.group);
|
|
30063
30108
|
}
|
|
30064
|
-
this.selected = selectedId && this.nodes.has(selectedId) ? this.nodes.get(selectedId) : null;
|
|
30065
30109
|
this.relations = this.modelData.relations;
|
|
30110
|
+
this.selected = selectedId && this.nodes.has(selectedId) ? this.nodes.get(selectedId) : null;
|
|
30111
|
+
const selectedRelationIndex = this._relationIndex(selectedRelationReference);
|
|
30112
|
+
this.selectedRelationIndex = !this.selected && selectedRelationIndex >= 0 && selectedRelationIndex < this.relations.length ? selectedRelationIndex : null;
|
|
30066
30113
|
this._renderPanel();
|
|
30067
30114
|
this._rebuildRelations();
|
|
30068
30115
|
this.ready = true;
|
|
@@ -30097,7 +30144,9 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30097
30144
|
}
|
|
30098
30145
|
_activeRelationIndexes() {
|
|
30099
30146
|
if (this.relationMode === "all") return this.relations.map((_, index) => index);
|
|
30100
|
-
if (this.relationMode !== "selected"
|
|
30147
|
+
if (this.relationMode !== "selected") return [];
|
|
30148
|
+
if (this.selectedRelationIndex !== null) return [this.selectedRelationIndex];
|
|
30149
|
+
if (!this.selected) return [];
|
|
30101
30150
|
return this.relations.map((relation, index) => relation.from === this.selected.id || relation.to === this.selected.id ? index : -1).filter((index) => index >= 0);
|
|
30102
30151
|
}
|
|
30103
30152
|
_rebuildRelations() {
|
|
@@ -30114,8 +30163,14 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30114
30163
|
const delta = end.clone().sub(start);
|
|
30115
30164
|
if (delta.lengthSq() < 0.01) continue;
|
|
30116
30165
|
const visualMode = relationVisualMode(relation);
|
|
30166
|
+
const isSelected = index === this.selectedRelationIndex;
|
|
30167
|
+
const pickTube = new Mesh(new TubeGeometry(new LineCurve3(start, end), 10, 0.16, 6, false), new MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false }));
|
|
30168
|
+
pickTube.userData.relationIndex = index;
|
|
30169
|
+
pickTube.userData.relationPick = true;
|
|
30170
|
+
this.relationGroup.add(pickTube);
|
|
30117
30171
|
const makeTube = (from2, to2, opacity = 0.82, tubeColor = color, tubeRadius = 0.045) => {
|
|
30118
|
-
const
|
|
30172
|
+
const displayColor = isSelected ? tubeColor.clone().lerp(new Color(16777215), 0.28) : tubeColor;
|
|
30173
|
+
const tube = new Mesh(new TubeGeometry(new LineCurve3(from2, to2), 10, isSelected ? tubeRadius * 1.45 : tubeRadius, 6, false), new MeshBasicMaterial({ color: displayColor, transparent: true, opacity: isSelected ? 1 : opacity }));
|
|
30119
30174
|
tube.userData.relationIndex = index;
|
|
30120
30175
|
this.relationGroup.add(tube);
|
|
30121
30176
|
};
|
|
@@ -30125,13 +30180,15 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30125
30180
|
// A broken communication remains a continuous red tube. Its rings
|
|
30126
30181
|
// oscillate locally around their positions instead of travelling
|
|
30127
30182
|
// along the connection, conveying a failed/retrying signal.
|
|
30128
|
-
const
|
|
30183
|
+
const displayBrokenColor = isSelected ? brokenColor.clone().lerp(new Color(16777215), 0.22) : brokenColor;
|
|
30184
|
+
const brokenTube = new Mesh(new TubeGeometry(new LineCurve3(start, end), 10, isSelected ? 0.08 : 0.055, 6, false), new MeshBasicMaterial({ color: displayBrokenColor, transparent: true, opacity: isSelected ? 1 : 0.88 }));
|
|
30129
30185
|
brokenTube.userData.relationIndex = index;
|
|
30130
30186
|
this.relationGroup.add(brokenTube);
|
|
30131
30187
|
const amplitude = Math.min(0.18, Math.max(0.07, delta.length() * 0.018));
|
|
30132
30188
|
for (let ringIndex = 0; ringIndex < 3; ringIndex++) {
|
|
30133
30189
|
const position = start.clone().lerp(end, (ringIndex + 1) / 4);
|
|
30134
|
-
const ring = new Mesh(new TorusGeometry(0.085, 0.024, 8, 20), new MeshBasicMaterial({ color:
|
|
30190
|
+
const ring = new Mesh(new TorusGeometry(isSelected ? 0.105 : 0.085, isSelected ? 0.03 : 0.024, 8, 20), new MeshBasicMaterial({ color: displayBrokenColor, transparent: true, opacity: 1 }));
|
|
30191
|
+
ring.userData.relationIndex = index;
|
|
30135
30192
|
ring.userData.phase = ringIndex / 3;
|
|
30136
30193
|
ring.userData.oscillate = true;
|
|
30137
30194
|
ring.userData.start = position.clone().addScaledVector(direction, -amplitude);
|
|
@@ -30143,7 +30200,9 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30143
30200
|
}
|
|
30144
30201
|
makeTube(start, end);
|
|
30145
30202
|
for (let ringIndex = 0; ringIndex < 4; ringIndex++) {
|
|
30146
|
-
const
|
|
30203
|
+
const displayColor = isSelected ? color.clone().lerp(new Color(16777215), 0.28) : color;
|
|
30204
|
+
const ring = new Mesh(new TorusGeometry(isSelected ? 0.088 : 0.068, isSelected ? 0.024 : 0.018, 8, 20), new MeshBasicMaterial({ color: displayColor, transparent: true, opacity: isSelected ? 1 : 0.9 }));
|
|
30205
|
+
ring.userData.relationIndex = index;
|
|
30147
30206
|
ring.userData.phase = ringIndex / 4;
|
|
30148
30207
|
ring.userData.direction = visualMode === "bidirectional" && ringIndex % 2 ? -1 : 1;
|
|
30149
30208
|
ring.userData.start = start.clone();
|
|
@@ -30156,6 +30215,16 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30156
30215
|
_renderPanel() {
|
|
30157
30216
|
if (!this.modelData) return;
|
|
30158
30217
|
const node = this.selected;
|
|
30218
|
+
const selectedRelation = this._publicRelation(this.selectedRelationIndex);
|
|
30219
|
+
if (selectedRelation) {
|
|
30220
|
+
const mode = relationVisualMode(selectedRelation);
|
|
30221
|
+
const title = selectedRelation.label || `${selectedRelation.from} \u2192 ${selectedRelation.to}`;
|
|
30222
|
+
this.panel.innerHTML = '<button class="back" data-action="overview">\u2190 Vista de conjunto</button><div class="eyebrow">RELACI\xD3N</div><h2>' + escapeHtml(title) + '</h2><div class="sub">' + escapeHtml(mode) + '</div><h3>ORIGEN</h3><button data-action="relation-endpoint" data-id="' + escapeHtml(selectedRelation.from) + '">' + escapeHtml(selectedRelation.from) + '</button><h3>DESTINO</h3><button data-action="relation-endpoint" data-id="' + escapeHtml(selectedRelation.to) + '">' + escapeHtml(selectedRelation.to) + '</button>';
|
|
30223
|
+
this.panel.querySelector('[data-action="overview"]')?.addEventListener("click", () => this.reset());
|
|
30224
|
+
this.panel.querySelectorAll('[data-action="relation-endpoint"]').forEach((button) => button.addEventListener("click", () => this.select(button.dataset.id)));
|
|
30225
|
+
this._applyTheme();
|
|
30226
|
+
return;
|
|
30227
|
+
}
|
|
30159
30228
|
if (!node) {
|
|
30160
30229
|
this.panel.innerHTML = '<div class="eyebrow">LIQUIDCARS ATLAS</div><h2>Explora el modelo.</h2><p>Selecciona un volumen para consultar su funci\xF3n y sus relaciones.</p><p class="note">El componente acepta un objeto JSON con palette, entities y relations.</p>';
|
|
30161
30230
|
this._applyTheme();
|
|
@@ -30205,13 +30274,38 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30205
30274
|
parent = parent.parent ? this.nodes.get(parent.parent) : null;
|
|
30206
30275
|
}
|
|
30207
30276
|
this.selected = node;
|
|
30277
|
+
this.selectedRelationIndex = null;
|
|
30208
30278
|
if (node.type === "shell") this.open.add(node.id);
|
|
30209
30279
|
this.targetFocus.copy(this._worldPosition(node));
|
|
30210
30280
|
this.targetZoom = node.parent ? 1.35 : 1.1;
|
|
30211
30281
|
this._renderPanel();
|
|
30212
30282
|
this._rebuildRelations();
|
|
30213
30283
|
this.invalidate();
|
|
30214
|
-
this._emit("lc-select", { id, entity: this._publicEntity(id), source: options.source || "api" });
|
|
30284
|
+
if (options.emit !== false) this._emit("lc-select", { kind: "entity", id, index: null, entity: this._publicEntity(id), relation: null, source: options.source || "api" });
|
|
30285
|
+
return this;
|
|
30286
|
+
}
|
|
30287
|
+
selectRelation(reference, options = {}) {
|
|
30288
|
+
const index = this._relationIndex(reference);
|
|
30289
|
+
const relation = this._publicRelation(index);
|
|
30290
|
+
if (!relation) return;
|
|
30291
|
+
for (const endpoint of [relation.from, relation.to]) {
|
|
30292
|
+
let node = this.nodes.get(endpoint);
|
|
30293
|
+
while (node?.parent) {
|
|
30294
|
+
this.open.add(node.parent);
|
|
30295
|
+
node = this.nodes.get(node.parent);
|
|
30296
|
+
}
|
|
30297
|
+
}
|
|
30298
|
+
this.selected = null;
|
|
30299
|
+
this.selectedRelationIndex = index;
|
|
30300
|
+
const from = this.nodes.get(relation.from);
|
|
30301
|
+
const to = this.nodes.get(relation.to);
|
|
30302
|
+
if (from && to) this.targetFocus.copy(this._worldPosition(from).add(this._worldPosition(to)).multiplyScalar(0.5));
|
|
30303
|
+
this.targetZoom = 1.22;
|
|
30304
|
+
this._renderPanel();
|
|
30305
|
+
this._rebuildRelations();
|
|
30306
|
+
this.invalidate();
|
|
30307
|
+
if (options.emit !== false) this._emit("lc-select", { kind: "relation", id: relation.id || null, index, entity: null, relation, source: options.source || "api" });
|
|
30308
|
+
return this;
|
|
30215
30309
|
}
|
|
30216
30310
|
setRelationMode(mode) {
|
|
30217
30311
|
this.relationMode = ["all", "selected", "none"].includes(mode) ? mode : "selected";
|
|
@@ -30230,7 +30324,9 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30230
30324
|
}
|
|
30231
30325
|
reset() {
|
|
30232
30326
|
const previousId = this.selected?.id || null;
|
|
30327
|
+
const previousRelation = this._publicRelation(this.selectedRelationIndex);
|
|
30233
30328
|
this.selected = null;
|
|
30329
|
+
this.selectedRelationIndex = null;
|
|
30234
30330
|
this.open.clear();
|
|
30235
30331
|
this.relationMode = "selected";
|
|
30236
30332
|
this.setAttribute("relation-mode", "selected");
|
|
@@ -30252,7 +30348,8 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30252
30348
|
this._renderPanel();
|
|
30253
30349
|
this._rebuildRelations();
|
|
30254
30350
|
this.invalidate();
|
|
30255
|
-
if (previousId) this._emit("lc-select", { id: null, entity: null, previousId, source: "reset" });
|
|
30351
|
+
if (previousId || previousRelation) this._emit("lc-select", { kind: null, id: null, index: null, entity: null, relation: null, previousId, previousRelation, source: "reset" });
|
|
30352
|
+
return this;
|
|
30256
30353
|
}
|
|
30257
30354
|
resize() {
|
|
30258
30355
|
if (!this.renderer) return;
|
|
@@ -30368,7 +30465,12 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30368
30465
|
if (!(node.type === "shell" && this.open.has(node.id))) meshes.push(node.mesh);
|
|
30369
30466
|
if (node.label?.visible) meshes.push(...node.label.children.filter((face) => face.visible));
|
|
30370
30467
|
}
|
|
30371
|
-
|
|
30468
|
+
const nodeHit = this.raycaster.intersectObjects(meshes, false)[0] || null;
|
|
30469
|
+
const relationTargets = this.relationGroup.children.filter((object) => object.userData.relationPick);
|
|
30470
|
+
const relationHit = this.raycaster.intersectObjects(relationTargets, false)[0] || null;
|
|
30471
|
+
if (nodeHit && (!relationHit || nodeHit.distance <= relationHit.distance + 0.12)) return { kind: "entity", node: nodeHit.object.userData.node };
|
|
30472
|
+
if (relationHit) return { kind: "relation", index: relationHit.object.userData.relationIndex };
|
|
30473
|
+
return nodeHit ? { kind: "entity", node: nodeHit.object.userData.node } : null;
|
|
30372
30474
|
}
|
|
30373
30475
|
_bindControls() {
|
|
30374
30476
|
this.shadowRoot.addEventListener("click", (event) => {
|
|
@@ -30383,36 +30485,76 @@ var LiquidCarsAtlas = class extends HTMLElement {
|
|
|
30383
30485
|
if (action === "relations-none") this.setRelationMode("none");
|
|
30384
30486
|
if (action) this.invalidate();
|
|
30385
30487
|
});
|
|
30488
|
+
this.canvas.addEventListener("contextmenu", (event) => event.preventDefault());
|
|
30386
30489
|
this.canvas.addEventListener("pointerdown", (event) => {
|
|
30387
|
-
if (event.
|
|
30388
|
-
this.
|
|
30490
|
+
if (event.pointerType !== "touch" && ![0, 1, 2].includes(event.button)) return;
|
|
30491
|
+
this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
30492
|
+
const mode = event.pointerType === "touch" ? "rotate" : event.shiftKey || event.button !== 0 ? "pan" : "rotate";
|
|
30493
|
+
this.drag = { pointerId: event.pointerId, pointerType: event.pointerType, button: event.button, mode, x: event.clientX, y: event.clientY, moved: false };
|
|
30494
|
+
if (event.pointerType === "touch" && this.pointers.size === 2) {
|
|
30495
|
+
const [a, b] = [...this.pointers.values()];
|
|
30496
|
+
this.drag = { mode: "touch-pan", x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, distance: Math.hypot(a.x - b.x, a.y - b.y), moved: true };
|
|
30497
|
+
}
|
|
30389
30498
|
this.canvas.setPointerCapture(event.pointerId);
|
|
30390
30499
|
this.canvas.style.cursor = "grabbing";
|
|
30391
30500
|
});
|
|
30392
30501
|
this.canvas.addEventListener("pointermove", (event) => {
|
|
30393
|
-
if (!this.drag) return;
|
|
30502
|
+
if (!this.drag || !this.pointers.has(event.pointerId)) return;
|
|
30503
|
+
this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
30504
|
+
if (event.pointerType === "touch" && this.pointers.size >= 2) {
|
|
30505
|
+
const [a, b] = [...this.pointers.values()];
|
|
30506
|
+
const x = (a.x + b.x) / 2;
|
|
30507
|
+
const y = (a.y + b.y) / 2;
|
|
30508
|
+
const distance = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
|
|
30509
|
+
if (this.drag.mode !== "touch-pan") this.drag = { mode: "touch-pan", x, y, distance, moved: true };
|
|
30510
|
+
else {
|
|
30511
|
+
this._panByPixels(x - this.drag.x, y - this.drag.y);
|
|
30512
|
+
this.targetZoom = Math.min(2.8, Math.max(0.55, this.targetZoom * distance / Math.max(1, this.drag.distance)));
|
|
30513
|
+
Object.assign(this.drag, { x, y, distance, moved: true });
|
|
30514
|
+
}
|
|
30515
|
+
this.invalidate();
|
|
30516
|
+
return;
|
|
30517
|
+
}
|
|
30518
|
+
if (this.drag.pointerId !== event.pointerId) return;
|
|
30394
30519
|
const dx = event.clientX - this.drag.x;
|
|
30395
30520
|
const dy = event.clientY - this.drag.y;
|
|
30396
30521
|
if (Math.abs(dx) + Math.abs(dy) > 3) this.drag.moved = true;
|
|
30397
30522
|
this.drag.x = event.clientX;
|
|
30398
30523
|
this.drag.y = event.clientY;
|
|
30399
|
-
this.
|
|
30400
|
-
|
|
30524
|
+
if (this.drag.mode === "pan" || event.shiftKey) this._panByPixels(dx, dy);
|
|
30525
|
+
else {
|
|
30526
|
+
this.yaw -= dx * 5e-3;
|
|
30527
|
+
this.pitch = Math.max(-1.56, Math.min(1.56, this.pitch + dy * 5e-3));
|
|
30528
|
+
}
|
|
30401
30529
|
this.invalidate();
|
|
30402
30530
|
});
|
|
30403
|
-
|
|
30531
|
+
const endPointer = (event, cancelled = false) => {
|
|
30404
30532
|
const drag = this.drag;
|
|
30533
|
+
this.pointers.delete(event.pointerId);
|
|
30534
|
+
if (this.pointers.size) {
|
|
30535
|
+
const [pointerId, point] = this.pointers.entries().next().value;
|
|
30536
|
+
this.drag = { pointerId, pointerType: "touch", button: 0, mode: "rotate", x: point.x, y: point.y, moved: true };
|
|
30537
|
+
return;
|
|
30538
|
+
}
|
|
30405
30539
|
this.drag = null;
|
|
30406
30540
|
this.canvas.style.cursor = "grab";
|
|
30407
|
-
if (drag && !drag.moved) {
|
|
30408
|
-
const
|
|
30409
|
-
if (
|
|
30410
|
-
|
|
30541
|
+
if (!cancelled && drag && !drag.moved && drag.button === 0) {
|
|
30542
|
+
const picked = this._pick(event);
|
|
30543
|
+
if (picked?.kind === "entity") {
|
|
30544
|
+
const node = picked.node;
|
|
30545
|
+
if (this.selectionMode === "event") this._emit("lc-select", { kind: "entity", id: node.id, index: null, entity: this._publicEntity(node.id), relation: null, source: "pointer" });
|
|
30411
30546
|
else this.select(node.id, { source: "pointer" });
|
|
30412
30547
|
}
|
|
30548
|
+
if (picked?.kind === "relation") {
|
|
30549
|
+
const relation = this._publicRelation(picked.index);
|
|
30550
|
+
if (this.selectionMode === "event") this._emit("lc-select", { kind: "relation", id: relation?.id || null, index: picked.index, entity: null, relation, source: "pointer" });
|
|
30551
|
+
else this.selectRelation(picked.index, { source: "pointer" });
|
|
30552
|
+
}
|
|
30413
30553
|
}
|
|
30414
30554
|
this.invalidate();
|
|
30415
|
-
}
|
|
30555
|
+
};
|
|
30556
|
+
this.canvas.addEventListener("pointerup", (event) => endPointer(event));
|
|
30557
|
+
this.canvas.addEventListener("pointercancel", (event) => endPointer(event, true));
|
|
30416
30558
|
this.canvas.addEventListener("wheel", (event) => {
|
|
30417
30559
|
event.preventDefault();
|
|
30418
30560
|
this.targetZoom = Math.min(2.8, Math.max(0.55, this.targetZoom * Math.exp(-event.deltaY * 1e-3)));
|