@liquidcars/atlas 0.1.22 → 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 CHANGED
@@ -55,6 +55,8 @@ atlas.setRelationMode('all'); // 'all' | 'selected' | 'none'
55
55
  atlas.setToolbarVisible(true); // también se puede usar toolbar="on" | "off"
56
56
  atlas.zoomIn();
57
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
58
60
  atlas.fit();
59
61
  atlas.frontView();
60
62
  atlas.setShellLabels(true);
@@ -127,7 +129,7 @@ Geometrías incluidas en esta primera versión: `box`, `cylinder`, `sphere`, `co
127
129
 
128
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.
129
131
 
130
- 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.
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.
131
133
 
132
134
  La escena también se puede editar en caliente. `updateModel(patch)` reemplaza
133
135
  las secciones indicadas; `addEntity`, `updateEntity` y `removeEntity` gestionan
@@ -154,7 +156,9 @@ npm install ./liquidcars-atlas-0.1.22.tgz
154
156
 
155
157
  ## Estado
156
158
 
157
- `0.1.22` amplía la selección contextual a las relaciones. El usuario puede
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
158
162
  seleccionar un tubo directamente, el componente lo realza, muestra sus extremos
159
163
  y modo en el panel y emite el mismo evento `lc-select` con `kind: 'relation'`.
160
164
  La API pública añade `selectRelation()`, `selectedRelation` y `selection`, y la
@@ -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">Arrastra para girar \xB7 rueda para zoom \xB7 clic para seleccionar</div></section><aside class="panel" aria-live="polite"></aside></div>';
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[char]);
29377
29377
  }
@@ -29608,6 +29608,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
29608
29608
  this.focus = new Vector3();
29609
29609
  this.targetFocus = new Vector3();
29610
29610
  this.drag = null;
29611
+ this.pointers = /* @__PURE__ */ new Map();
29611
29612
  this.relationGroup = new Group();
29612
29613
  this.scene.add(this.relationGroup);
29613
29614
  this.relations = [];
@@ -29830,6 +29831,27 @@ var LiquidCarsAtlas = class extends HTMLElement {
29830
29831
  this.invalidate();
29831
29832
  return this;
29832
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
+ }
29833
29855
  fit() {
29834
29856
  const bounds = new Box3();
29835
29857
  for (const node of this.nodes.values()) {
@@ -30463,28 +30485,60 @@ var LiquidCarsAtlas = class extends HTMLElement {
30463
30485
  if (action === "relations-none") this.setRelationMode("none");
30464
30486
  if (action) this.invalidate();
30465
30487
  });
30488
+ this.canvas.addEventListener("contextmenu", (event) => event.preventDefault());
30466
30489
  this.canvas.addEventListener("pointerdown", (event) => {
30467
- if (event.button !== 0) return;
30468
- this.drag = { x: event.clientX, y: event.clientY, moved: false };
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
+ }
30469
30498
  this.canvas.setPointerCapture(event.pointerId);
30470
30499
  this.canvas.style.cursor = "grabbing";
30471
30500
  });
30472
30501
  this.canvas.addEventListener("pointermove", (event) => {
30473
- 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;
30474
30519
  const dx = event.clientX - this.drag.x;
30475
30520
  const dy = event.clientY - this.drag.y;
30476
30521
  if (Math.abs(dx) + Math.abs(dy) > 3) this.drag.moved = true;
30477
30522
  this.drag.x = event.clientX;
30478
30523
  this.drag.y = event.clientY;
30479
- this.yaw -= dx * 5e-3;
30480
- this.pitch = Math.max(-1.56, Math.min(1.56, this.pitch + dy * 5e-3));
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
+ }
30481
30529
  this.invalidate();
30482
30530
  });
30483
- this.canvas.addEventListener("pointerup", (event) => {
30531
+ const endPointer = (event, cancelled = false) => {
30484
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
+ }
30485
30539
  this.drag = null;
30486
30540
  this.canvas.style.cursor = "grab";
30487
- if (drag && !drag.moved) {
30541
+ if (!cancelled && drag && !drag.moved && drag.button === 0) {
30488
30542
  const picked = this._pick(event);
30489
30543
  if (picked?.kind === "entity") {
30490
30544
  const node = picked.node;
@@ -30498,7 +30552,9 @@ var LiquidCarsAtlas = class extends HTMLElement {
30498
30552
  }
30499
30553
  }
30500
30554
  this.invalidate();
30501
- });
30555
+ };
30556
+ this.canvas.addEventListener("pointerup", (event) => endPointer(event));
30557
+ this.canvas.addEventListener("pointercancel", (event) => endPointer(event, true));
30502
30558
  this.canvas.addEventListener("wheel", (event) => {
30503
30559
  event.preventDefault();
30504
30560
  this.targetZoom = Math.min(2.8, Math.max(0.55, this.targetZoom * Math.exp(-event.deltaY * 1e-3)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liquidcars/atlas",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Reusable 3D conceptual atlas Web Component for LiquidCars-style models",
5
5
  "type": "module",
6
6
  "main": "./dist/liquidcars-atlas.js",