@liquidcars/atlas 0.1.23 → 0.1.25

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
@@ -96,7 +96,7 @@ principales también están expuestos como variables CSS (`--lc-bg`,
96
96
  por lo que una aplicación puede adaptar la apariencia sin modificar el bundle.
97
97
  Las propiedades `atlas.theme` y `atlas.styles` son alias prácticos de esta configuración.
98
98
 
99
- Eventos disponibles: `lc-ready`, `lc-error`, `lc-select`, `lc-model-change`,
99
+ Eventos disponibles: `lc-ready`, `lc-error`, `lc-warning`, `lc-select`, `lc-model-change`,
100
100
  `lc-relation-mode` y `lc-disconnect`. `lc-select` usa un detalle discriminado:
101
101
  las entidades incluyen `{ kind: 'entity', id, entity, source }` y las relaciones
102
102
  `{ kind: 'relation', id, index, relation, source }`. Los campos históricos `id`
@@ -127,6 +127,68 @@ como `setClickBehavior()`).
127
127
 
128
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.
129
129
 
130
+ ## Geometry packs
131
+
132
+ Las geometrías pueden distribuirse en paquetes independientes. El componente
133
+ expone `registerGeometryPack()`, `unregisterGeometryPack()` y
134
+ `getGeometryCatalog()`; los mismos métodos están disponibles como estáticos en
135
+ `LiquidCarsAtlas`. Los identificadores usan un namespace (`infra.server`,
136
+ `architecture.application`, etc.) y las primitivas integradas también se
137
+ publican como `geo.box`, `geo.sphere`, etc. Los nombres históricos sin
138
+ namespace siguen siendo compatibles.
139
+
140
+ ```js
141
+ import { registerGeometryPack } from "@liquidcars/atlas";
142
+
143
+ registerGeometryPack({
144
+ namespace: "infra",
145
+ name: "Infrastructure",
146
+ version: "0.1.0",
147
+ palette: { chassis: "#587386", accent: "#72dbc9" },
148
+ geometries: [{
149
+ id: "server",
150
+ name: "Server",
151
+ canonicalSize: [2.7, 1.8, 2.2],
152
+ parameters: {
153
+ units: { type: "number", min: 1, max: 8, default: 3 },
154
+ icon: { type: "icon", default: "⚙" },
155
+ image: { type: "image", default: "" }
156
+ },
157
+ anchors: { input: [-1, 0, 0], output: [1, 0, 0], network: [0, 0, 1] },
158
+ create({ parameters }) {
159
+ return { parts: [
160
+ { shape: "box", size: [1, 1, 1], color: "chassis" },
161
+ { shape: "plane", size: [.42, .42, 1], position: [0, 0, .51],
162
+ color: "accent", icon: parameters.icon, image: parameters.image,
163
+ edges: false, doubleSided: true }
164
+ ] };
165
+ }
166
+ }]
167
+ });
168
+ ```
169
+
170
+ El modelo configura cada instancia sin conocer la implementación del paquete:
171
+
172
+ ```json
173
+ {
174
+ "geometry": "infra.server",
175
+ "geometryOptions": { "units": 4, "icon": "DB" },
176
+ "geometryPalette": { "accent": "#ffd166" }
177
+ }
178
+ ```
179
+
180
+ Cada definición puede declarar parámetros tipados, una paleta local,
181
+ proporciones canónicas y anchors. Las piezas declarativas admiten primitivas,
182
+ posición, rotación, escala, materiales, iconos de texto o emoji e imágenes por
183
+ URL/data URI. El renderer mantiene Three.js encapsulado para evitar que los
184
+ packs dependan de su versión interna. Las relaciones pueden seleccionar anchors
185
+ con `fromAnchor` y `toAnchor`.
186
+
187
+ Si un namespace no está registrado, Atlas conserva el modelo renderizable con
188
+ una caja roja de fallback y emite `lc-warning` con el código
189
+ `GEOMETRY_NOT_REGISTERED`. Si la factoría del pack falla se usa el código
190
+ `GEOMETRY_BUILD_FAILED`.
191
+
130
192
  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.
131
193
 
132
194
  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.
@@ -151,12 +213,17 @@ Una vez publicado, los consumidores podrán instalarlo con `npm install @liquidc
151
213
  Para probar el paquete sin publicarlo, basta con instalar el tarball generado:
152
214
 
153
215
  ```bash
154
- npm install ./liquidcars-atlas-0.1.22.tgz
216
+ npm install ./liquidcars-atlas-0.1.25.tgz
155
217
  ```
156
218
 
157
219
  ## Estado
158
220
 
159
- `0.1.23` añade desplazamiento lateral de cámara mediante ratón, teclado
221
+ `0.1.25` mantiene el centro como anclaje implícito de toda relación y utiliza
222
+ los puertos de geometrías externas únicamente cuando el modelo los solicita.
223
+ También evita recorrer los materiales de geometrías compuestas en cada frame.
224
+ `0.1.24` incorporó geometry packs independientes, namespaces, parámetros
225
+ personalizados, paletas locales, anchors e imágenes/iconos declarativos. La
226
+ versión `0.1.23` añadió desplazamiento lateral de cámara mediante ratón, teclado
160
227
  modificador y gestos táctiles, además de la API pública `panBy()` y `panTo()`.
161
228
  Se conserva la selección contextual de relaciones introducida en `0.1.22`: el usuario puede
162
229
  seleccionar un tubo directamente, el componente lo realza, muestra sus extremos
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.21"></script>
17
+ <script type="module" src="../dist/liquidcars-atlas.js?v=0.1.25"></script>
18
18
  </head>
19
19
  <body>
20
20
  <header>
21
21
  <h1>LiquidCars Atlas</h1>
22
- <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</code> · v0.1.21</p>
22
+ <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</code> · v0.1.25</p>
23
23
  </header>
24
24
  <main>
25
25
  <liquidcars-atlas id="atlas" height="720" relation-mode="selected" shell-labels="on"></liquidcars-atlas>
@@ -1,4 +1,4 @@
1
- /*! LiquidCars Atlas 0.1.22 | MIT */
1
+ /*! LiquidCars Atlas 0.1.25 | MIT */
2
2
 
3
3
  // src/three.core.js
4
4
  /**
@@ -29344,6 +29344,86 @@ var DEFAULT_PALETTE = {
29344
29344
  component: "#8ae4d0",
29345
29345
  connection: "#f4edba"
29346
29346
  };
29347
+ var GEOMETRY_DEFINITIONS = /* @__PURE__ */ new Map();
29348
+ var GEOMETRY_ALIASES = /* @__PURE__ */ new Map();
29349
+ function publicGeometryDefinition(definition) {
29350
+ const { create, ...metadata } = definition;
29351
+ return {
29352
+ ...metadata,
29353
+ canonicalSize: [...definition.canonicalSize],
29354
+ parameters: { ...definition.parameters },
29355
+ palette: { ...definition.palette },
29356
+ anchors: { ...definition.anchors }
29357
+ };
29358
+ }
29359
+ function registerGeometryPack(pack) {
29360
+ if (!pack || typeof pack !== "object") throw new TypeError("A geometry pack must be an object.");
29361
+ const namespace = String(pack.namespace || "").trim();
29362
+ if (!/^[a-z][a-z0-9-]*$/i.test(namespace)) throw new TypeError("A geometry pack requires a valid namespace.");
29363
+ if (!Array.isArray(pack.geometries) || !pack.geometries.length) throw new TypeError("A geometry pack requires at least one geometry.");
29364
+ const registered = [];
29365
+ for (const source of pack.geometries) {
29366
+ if (!source || typeof source !== "object" || !source.id) throw new TypeError(`Geometry pack '${namespace}' contains an invalid geometry.`);
29367
+ const localId = String(source.id).includes(".") ? String(source.id).split(".").pop() : String(source.id);
29368
+ const id = String(source.id).includes(".") ? String(source.id) : `${namespace}.${localId}`;
29369
+ const definition = {
29370
+ ...source,
29371
+ id,
29372
+ localId,
29373
+ namespace,
29374
+ pack: pack.name || namespace,
29375
+ packVersion: pack.version || "0.0.0",
29376
+ category: source.category || pack.category || pack.name || namespace,
29377
+ canonicalSize: Array.isArray(source.canonicalSize) && source.canonicalSize.length === 3 ? source.canonicalSize.map(Number) : [2.5, 1.5, 2.5],
29378
+ parameters: source.parameters && typeof source.parameters === "object" ? source.parameters : {},
29379
+ palette: { ...pack.palette || {}, ...source.palette || {} },
29380
+ anchors: { ...pack.anchors || {}, ...source.anchors || {} }
29381
+ };
29382
+ GEOMETRY_DEFINITIONS.set(id, definition);
29383
+ GEOMETRY_ALIASES.set(id, id);
29384
+ for (const alias of source.aliases || []) GEOMETRY_ALIASES.set(String(alias), id);
29385
+ registered.push(id);
29386
+ }
29387
+ if (typeof globalThis.dispatchEvent === "function" && typeof CustomEvent === "function") {
29388
+ globalThis.dispatchEvent(new CustomEvent("lc-geometry-registry-change", { detail: { namespace, geometries: registered } }));
29389
+ }
29390
+ return { namespace, geometries: registered };
29391
+ }
29392
+ function unregisterGeometryPack(namespace) {
29393
+ const removed = [];
29394
+ for (const [id, definition] of GEOMETRY_DEFINITIONS) {
29395
+ if (definition.namespace !== namespace) continue;
29396
+ GEOMETRY_DEFINITIONS.delete(id);
29397
+ removed.push(id);
29398
+ }
29399
+ for (const [alias, id] of GEOMETRY_ALIASES) if (removed.includes(id)) GEOMETRY_ALIASES.delete(alias);
29400
+ return removed;
29401
+ }
29402
+ function getGeometryCatalog() {
29403
+ return [...GEOMETRY_DEFINITIONS.values()].map(publicGeometryDefinition);
29404
+ }
29405
+ function geometryDefinition(kind) {
29406
+ const id = GEOMETRY_ALIASES.get(String(kind || "box")) || String(kind || "box");
29407
+ return GEOMETRY_DEFINITIONS.get(id) || null;
29408
+ }
29409
+ var CORE_GEOMETRIES = [
29410
+ "box", "cylinder", "sphere", "cone", "frustum-cone", "frustum-pyramid", "frustum-triangle",
29411
+ "prism", "pyramid-square", "pyramid-triangle", "octahedron", "dodecahedron", "icosahedron"
29412
+ ];
29413
+ registerGeometryPack({
29414
+ namespace: "geo",
29415
+ name: "Geometric primitives",
29416
+ version: "1",
29417
+ category: "Geometric primitives",
29418
+ geometries: CORE_GEOMETRIES.map((id) => ({
29419
+ id,
29420
+ primitive: id,
29421
+ aliases: [id],
29422
+ name: id.split("-").map((word) => word[0].toUpperCase() + word.slice(1)).join(" "),
29423
+ canonicalSize: id === "sphere" || ["octahedron", "dodecahedron", "icosahedron"].includes(id) ? [2.2, 2.2, 2.2] : [2.5, 1.5, 2.5],
29424
+ parameters: id === "box" ? { radius: { type: "number", label: "Radius", min: 0, max: 0.72, step: 0.01, default: 0 } } : {}
29425
+ }))
29426
+ });
29347
29427
  var DEFAULT_THEME = {
29348
29428
  background: { color: "#0a111a", glow: "#1c2b39", fade: "#0a111a" },
29349
29429
  panel: { background: "linear-gradient(140deg,#182633,#0e1925 65%)", border: "#2a3a48", text: "#b5c6d2", title: "#e9f0f5" },
@@ -29500,6 +29580,112 @@ function normalizeGeometry(geometry) {
29500
29580
  geometry.computeBoundingBox();
29501
29581
  return geometry;
29502
29582
  }
29583
+ function resolveGeometryParameters(entity, definition) {
29584
+ const supplied = entity.geometryOptions || entity.geometry_options || entity.parameters || {};
29585
+ const resolved = {};
29586
+ for (const [name, schema] of Object.entries(definition?.parameters || {})) {
29587
+ if (schema && Object.hasOwn(schema, "default")) resolved[name] = schema.default;
29588
+ }
29589
+ return { ...resolved, ...supplied && typeof supplied === "object" ? supplied : {} };
29590
+ }
29591
+ function resolveGeometryColor(value, palette, fallback) {
29592
+ if (typeof value !== "string" || !value) return fallback;
29593
+ return palette[value] || value;
29594
+ }
29595
+ function iconTexture(value, foreground = "#ffffff", background = "transparent") {
29596
+ const bitmap = document.createElement("canvas");
29597
+ bitmap.width = 256;
29598
+ bitmap.height = 256;
29599
+ const context = bitmap.getContext("2d");
29600
+ context.clearRect(0, 0, 256, 256);
29601
+ if (background !== "transparent") {
29602
+ context.fillStyle = background;
29603
+ context.fillRect(0, 0, 256, 256);
29604
+ }
29605
+ context.fillStyle = foreground;
29606
+ context.font = "600 150px system-ui, sans-serif";
29607
+ context.textAlign = "center";
29608
+ context.textBaseline = "middle";
29609
+ context.fillText(String(value || ""), 128, 137, 220);
29610
+ const texture = new CanvasTexture(bitmap);
29611
+ texture.colorSpace = SRGBColorSpace;
29612
+ return texture;
29613
+ }
29614
+ function partGeometry(part) {
29615
+ if (part.shape === "plane") return new PlaneGeometry(1, 1);
29616
+ return makeGeometry(part.shape || part.geometry || "box", part.radius, part.segments, part.size);
29617
+ }
29618
+ function createGeometryContent(definition, entity, modelPalette, invalidate) {
29619
+ const parameters = resolveGeometryParameters(entity, definition);
29620
+ const localPalette = {
29621
+ ...modelPalette,
29622
+ ...definition.palette || {},
29623
+ ...entity.geometryPalette || entity.geometry_palette || {}
29624
+ };
29625
+ let descriptor = typeof definition.create === "function" ? definition.create({ parameters: { ...parameters }, colors: { ...localPalette }, entity: { ...entity } }) : { parts: definition.parts || [] };
29626
+ if (Array.isArray(descriptor)) descriptor = { parts: descriptor };
29627
+ const parts = Array.isArray(descriptor?.parts) ? descriptor.parts : [];
29628
+ if (!parts.length) throw new Error(`Geometry '${definition.id}' did not produce any parts.`);
29629
+ const root = new Group();
29630
+ const colorBindings = [];
29631
+ const meshes = [];
29632
+ for (const part of parts) {
29633
+ const geometry = partGeometry(part);
29634
+ normalizeGeometry(geometry);
29635
+ const colorValue = part.color || "component";
29636
+ const color = resolveGeometryColor(colorValue, localPalette, modelPalette.component || DEFAULT_PALETTE.component);
29637
+ const material = new MeshStandardMaterial({
29638
+ color,
29639
+ roughness: Number.isFinite(Number(part.roughness)) ? Number(part.roughness) : 0.72,
29640
+ metalness: Number.isFinite(Number(part.metalness)) ? Number(part.metalness) : 0.06,
29641
+ transparent: part.transparent === true || Number(part.opacity) < 1,
29642
+ opacity: Number.isFinite(Number(part.opacity)) ? Number(part.opacity) : 1,
29643
+ side: part.doubleSided ? DoubleSide : FrontSide
29644
+ });
29645
+ const icon = part.icon;
29646
+ const image = part.image;
29647
+ if (typeof image === "string" && image) {
29648
+ const bitmap = new Image();
29649
+ bitmap.crossOrigin = "anonymous";
29650
+ material.map = new Texture(bitmap);
29651
+ bitmap.addEventListener("load", () => {
29652
+ material.map.needsUpdate = true;
29653
+ invalidate?.();
29654
+ });
29655
+ bitmap.addEventListener("error", () => invalidate?.());
29656
+ bitmap.src = image;
29657
+ material.map.colorSpace = SRGBColorSpace;
29658
+ material.needsUpdate = true;
29659
+ } else if (icon != null && icon !== "") {
29660
+ material.map = iconTexture(icon, resolveGeometryColor(part.iconColor || "#ffffff", localPalette, "#ffffff"), resolveGeometryColor(part.iconBackground || "transparent", localPalette, "transparent"));
29661
+ material.transparent = true;
29662
+ material.needsUpdate = true;
29663
+ }
29664
+ const mesh = new Mesh(geometry, material);
29665
+ mesh.position.fromArray(Array.isArray(part.position) ? part.position : [0, 0, 0]);
29666
+ mesh.scale.fromArray(Array.isArray(part.size) ? part.size : [1, 1, 1]);
29667
+ if (Array.isArray(part.rotation)) mesh.rotation.set(...part.rotation.map(Number));
29668
+ root.add(mesh);
29669
+ meshes.push(mesh);
29670
+ colorBindings.push({ material, value: colorValue, palette: localPalette });
29671
+ if (part.edges !== false && part.shape !== "plane") {
29672
+ const edgeMaterial = new LineBasicMaterial({ color, transparent: true, opacity: Number.isFinite(Number(part.edgeOpacity)) ? Number(part.edgeOpacity) : 0.55 });
29673
+ const edge = new LineSegments(new EdgesGeometry(geometry, 22), edgeMaterial);
29674
+ edge.position.copy(mesh.position);
29675
+ edge.rotation.copy(mesh.rotation);
29676
+ edge.scale.copy(mesh.scale);
29677
+ root.add(edge);
29678
+ colorBindings.push({ material: edgeMaterial, value: part.edgeColor || colorValue, palette: localPalette });
29679
+ }
29680
+ }
29681
+ root.updateMatrixWorld(true);
29682
+ const bounds = new Box3().setFromObject(root);
29683
+ const contentSize = bounds.getSize(new Vector3());
29684
+ const center = bounds.getCenter(new Vector3());
29685
+ root.children.forEach((child) => child.position.sub(center));
29686
+ root.scale.set(1 / Math.max(contentSize.x, 1e-4), 1 / Math.max(contentSize.y, 1e-4), 1 / Math.max(contentSize.z, 1e-4));
29687
+ return { root, meshes, colorBindings, parameters, descriptor };
29688
+ }
29503
29689
  function makeLabel(text, color, dimensions, type = "") {
29504
29690
  const bitmap = document.createElement("canvas");
29505
29691
  const context = bitmap.getContext("2d");
@@ -29580,6 +29766,15 @@ var LiquidCarsAtlas = class extends HTMLElement {
29580
29766
  static get observedAttributes() {
29581
29767
  return ["height", "relation-mode", "shell-labels", "toolbar", "selection-mode"];
29582
29768
  }
29769
+ static registerGeometryPack(pack) {
29770
+ return registerGeometryPack(pack);
29771
+ }
29772
+ static unregisterGeometryPack(namespace) {
29773
+ return unregisterGeometryPack(namespace);
29774
+ }
29775
+ static getGeometryCatalog() {
29776
+ return getGeometryCatalog();
29777
+ }
29583
29778
  constructor() {
29584
29779
  super();
29585
29780
  this.attachShadow({ mode: "open" });
@@ -29616,15 +29811,20 @@ var LiquidCarsAtlas = class extends HTMLElement {
29616
29811
  this.ready = false;
29617
29812
  this._toolbarVisible = this.getAttribute("toolbar") !== "off";
29618
29813
  this.toolbar = this.shadowRoot.querySelector(".toolbar");
29814
+ this._geometryRegistryListener = () => {
29815
+ if (this.ready && this.modelData) this._build({ preserveSelection: this.selected?.id, preserveRelation: this.selectedRelationIndex });
29816
+ };
29619
29817
  this._bindControls();
29620
29818
  this.resizeObserver = new ResizeObserver(() => this.resize());
29621
29819
  this.resizeObserver.observe(this.canvas);
29622
29820
  }
29623
29821
  connectedCallback() {
29822
+ globalThis.addEventListener?.("lc-geometry-registry-change", this._geometryRegistryListener);
29624
29823
  if (this.hasAttribute("height")) this.style.setProperty("--lc-height", this.getAttribute("height"));
29625
29824
  this._loadInitial();
29626
29825
  }
29627
29826
  disconnectedCallback() {
29827
+ globalThis.removeEventListener?.("lc-geometry-registry-change", this._geometryRegistryListener);
29628
29828
  this.resizeObserver.disconnect();
29629
29829
  if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
29630
29830
  if (this.renderer) this.renderer.dispose();
@@ -30042,10 +30242,16 @@ var LiquidCarsAtlas = class extends HTMLElement {
30042
30242
  _clearObjects() {
30043
30243
  for (const node of this.nodes.values()) {
30044
30244
  node.group?.traverse((object) => {
30045
- if (!object.isMesh && !object.isSprite) return;
30245
+ if (!object.isMesh && !object.isSprite && !object.isLineSegments) return;
30046
30246
  object.geometry?.dispose();
30047
- if (Array.isArray(object.material)) object.material.forEach((material) => material.dispose());
30048
- else object.material?.dispose();
30247
+ if (Array.isArray(object.material)) object.material.forEach((material) => {
30248
+ material.map?.dispose();
30249
+ material.dispose();
30250
+ });
30251
+ else {
30252
+ object.material?.map?.dispose();
30253
+ object.material?.dispose();
30254
+ }
30049
30255
  });
30050
30256
  this.scene.remove(node.group);
30051
30257
  }
@@ -30066,6 +30272,10 @@ var LiquidCarsAtlas = class extends HTMLElement {
30066
30272
  const color = this.modelData.palette[node.c] || node.c || this.modelData.palette.component;
30067
30273
  if (node.material) node.material.color.set(color);
30068
30274
  if (node.edge?.material) node.edge.material.color.set(color);
30275
+ for (const binding of node.colorBindings || []) {
30276
+ const palette = { ...this.modelData.palette, ...node.geometryPalette || {}, ...node.geometryPaletteOverrides || {} };
30277
+ binding.material.color.set(resolveGeometryColor(binding.value, palette, color));
30278
+ }
30069
30279
  }
30070
30280
  }
30071
30281
  _build(options = {}) {
@@ -30080,24 +30290,61 @@ var LiquidCarsAtlas = class extends HTMLElement {
30080
30290
  node.group = new Group();
30081
30291
  node.group.position.copy(this._worldPosition(node));
30082
30292
  const geometryKind = entity.geometry || "box";
30083
- const baked = geometryKind === "box" && Number(entity.radius) > 0;
30084
- const geometry = makeGeometry(geometryKind, entity.radius, entity.segments, entity.s);
30085
- if (!baked) normalizeGeometry(geometry);
30086
- const color = this.modelData.palette[entity.c] || entity.c || this.modelData.palette.component;
30087
- node.material = new MeshStandardMaterial({ color, roughness: 0.72, metalness: 0.06, transparent: true, opacity: entity.type === "shell" ? 0.96 : 1 });
30088
- node.mesh = new Mesh(geometry, node.material);
30089
- node.mesh.scale.fromArray(baked ? [1, 1, 1] : entity.s);
30090
- if (baked) {
30091
- geometry.computeBoundingBox();
30092
- node.labelSize = geometry.boundingBox.getSize(new Vector3());
30093
- } else {
30293
+ const definition = geometryDefinition(geometryKind);
30294
+ const missingGeometry = !definition && geometryKind.includes(".");
30295
+ const color = missingGeometry ? "#ed8f98" : this.modelData.palette[entity.c] || entity.c || this.modelData.palette.component;
30296
+ if (missingGeometry) this.dispatchEvent(new CustomEvent("lc-warning", { detail: { code: "GEOMETRY_NOT_REGISTERED", geometry: geometryKind, entityId: entity.id } }));
30297
+ const primitiveKind = definition?.primitive || geometryKind;
30298
+ const isCustom = definition && !definition.primitive && entity.type !== "shell";
30299
+ node.geometryDefinition = definition || null;
30300
+ node.geometryMissing = missingGeometry;
30301
+ node.geometryPalette = definition?.palette || {};
30302
+ node.geometryPaletteOverrides = entity.geometryPalette || entity.geometry_palette || {};
30303
+ node.ports = { ...definition?.anchors || {}, ...entity.ports || {} };
30304
+ if (isCustom) {
30305
+ try {
30306
+ const content = createGeometryContent(definition, entity, this.modelData.palette, () => this.invalidate());
30307
+ content.root.scale.multiply(new Vector3().fromArray(entity.s));
30308
+ node.appearanceMaterials = [];
30309
+ content.root.traverse((object) => {
30310
+ if (!object.isMesh) return;
30311
+ object.userData.node = node;
30312
+ const materials = Array.isArray(object.material) ? object.material : [object.material];
30313
+ node.appearanceMaterials.push(...materials.filter(Boolean));
30314
+ });
30315
+ node.mesh = content.root;
30316
+ node.colorBindings = content.colorBindings;
30317
+ node.geometryOptions = content.parameters;
30318
+ node.group.add(content.root);
30319
+ } catch (error) {
30320
+ this.dispatchEvent(new CustomEvent("lc-warning", { detail: { code: "GEOMETRY_BUILD_FAILED", geometry: geometryKind, entityId: entity.id, error } }));
30321
+ const fallback = normalizeGeometry(makeGeometry("box"));
30322
+ node.material = new MeshStandardMaterial({ color: "#ed8f98", roughness: 0.72, metalness: 0.06 });
30323
+ node.mesh = new Mesh(fallback, node.material);
30324
+ node.mesh.scale.fromArray(entity.s);
30325
+ node.mesh.userData.node = node;
30326
+ node.group.add(node.mesh);
30327
+ }
30094
30328
  node.labelSize = new Vector3().fromArray(entity.s);
30329
+ } else {
30330
+ const baked = primitiveKind === "box" && Number(entity.radius) > 0;
30331
+ const geometry = makeGeometry(primitiveKind, entity.radius, entity.segments, entity.s);
30332
+ if (!baked) normalizeGeometry(geometry);
30333
+ node.material = new MeshStandardMaterial({ color, roughness: 0.72, metalness: 0.06, transparent: true, opacity: entity.type === "shell" ? 0.96 : 1 });
30334
+ node.mesh = new Mesh(geometry, node.material);
30335
+ node.mesh.scale.fromArray(baked ? [1, 1, 1] : entity.s);
30336
+ if (baked) {
30337
+ geometry.computeBoundingBox();
30338
+ node.labelSize = geometry.boundingBox.getSize(new Vector3());
30339
+ } else {
30340
+ node.labelSize = new Vector3().fromArray(entity.s);
30341
+ }
30342
+ node.mesh.userData.node = node;
30343
+ node.group.add(node.mesh);
30344
+ node.edge = new LineSegments(new EdgesGeometry(geometry, 22), new LineBasicMaterial({ color, transparent: true, opacity: 0.7 }));
30345
+ node.edge.scale.fromArray(baked ? [1, 1, 1] : entity.s);
30346
+ node.group.add(node.edge);
30095
30347
  }
30096
- node.mesh.userData.node = node;
30097
- node.group.add(node.mesh);
30098
- node.edge = new LineSegments(new EdgesGeometry(geometry, 22), new LineBasicMaterial({ color, transparent: true, opacity: 0.7 }));
30099
- node.edge.scale.fromArray(baked ? [1, 1, 1] : entity.s);
30100
- node.group.add(node.edge);
30101
30348
  node.label = makeLabel(entity.name, color, node.labelSize, entity.type);
30102
30349
  node.label.userData.node = node;
30103
30350
  node.label.traverse((labelFace) => {
@@ -30123,18 +30370,22 @@ var LiquidCarsAtlas = class extends HTMLElement {
30123
30370
  }
30124
30371
  return true;
30125
30372
  }
30126
- _endpoint(node, other, isSource = false) {
30373
+ _endpoint(node, other, isSource = false, relation = null) {
30127
30374
  const center = this._worldPosition(node);
30128
30375
  const mode = node.anchor || "center";
30129
- if (mode === "center") return center;
30130
- if (mode === "port") {
30376
+ const requestedAnchor = relation?.[isSource ? "fromAnchor" : "toAnchor"] || relation?.[isSource ? "from_anchor" : "to_anchor"];
30377
+ // Geometry packs may advertise available ports, but their mere presence
30378
+ // must not alter the model. The centre remains the default endpoint;
30379
+ // ports are used only when the entity or relation explicitly requests one.
30380
+ if (mode === "port" || requestedAnchor) {
30131
30381
  const ports = node.ports || {};
30132
- const port = ports[isSource ? "output" : "input"] || ports.default;
30382
+ const port = ports[requestedAnchor] || ports[isSource ? "output" : "input"] || ports.default;
30133
30383
  if (Array.isArray(port) && port.length >= 3) {
30134
30384
  const half2 = new Vector3(node.s[0] / 2, node.s[1] / 2, node.s[2] / 2);
30135
30385
  return center.add(new Vector3(port[0] * half2.x, port[1] * half2.y, port[2] * half2.z));
30136
30386
  }
30137
30387
  }
30388
+ if (mode === "center") return center;
30138
30389
  const direction = this._worldPosition(other).sub(center);
30139
30390
  if (direction.lengthSq() < 1e-8) return center;
30140
30391
  const half = new Vector3(node.s[0] / 2, node.s[1] / 2, node.s[2] / 2);
@@ -30158,8 +30409,8 @@ var LiquidCarsAtlas = class extends HTMLElement {
30158
30409
  const from = this.nodes.get(relation.from);
30159
30410
  const to = this.nodes.get(relation.to);
30160
30411
  if (!from || !to) continue;
30161
- const start = this._endpoint(from, to, true);
30162
- const end = this._endpoint(to, from, false);
30412
+ const start = this._endpoint(from, to, true, relation);
30413
+ const end = this._endpoint(to, from, false, relation);
30163
30414
  const delta = end.clone().sub(start);
30164
30415
  if (delta.lengthSq() < 0.01) continue;
30165
30416
  const visualMode = relationVisualMode(relation);
@@ -30380,13 +30631,21 @@ var LiquidCarsAtlas = class extends HTMLElement {
30380
30631
  const visible = this._isVisible(node);
30381
30632
  node.group.visible = visible;
30382
30633
  const alpha = node.type === "shell" ? this.open.has(node.id) ? 0.12 : 1 : 1;
30383
- node.material.opacity = alpha;
30384
30634
  const transparent = alpha < 0.999;
30385
- node.material.depthWrite = !transparent;
30386
30635
  node.mesh.renderOrder = transparent ? 10 - (node.parent ? 1 : 0) : 0;
30387
- node.material.emissive?.set(node === this.selected ? node.material.color : 0);
30388
- node.material.emissiveIntensity = node === this.selected ? 0.15 : 0;
30389
- node.edge.material.opacity = node === this.selected ? 1 : 0.65;
30636
+ if (node.material) {
30637
+ node.material.opacity = alpha;
30638
+ node.material.depthWrite = !transparent;
30639
+ node.material.emissive?.set(node === this.selected ? node.material.color : 0);
30640
+ node.material.emissiveIntensity = node === this.selected ? 0.15 : 0;
30641
+ } else if (node._appearanceSelected !== (node === this.selected)) {
30642
+ for (const material of node.appearanceMaterials || []) {
30643
+ material.emissive?.set(node === this.selected ? material.color : 0);
30644
+ material.emissiveIntensity = node === this.selected ? 0.15 : 0;
30645
+ }
30646
+ node._appearanceSelected = node === this.selected;
30647
+ }
30648
+ if (node.edge?.material) node.edge.material.opacity = node === this.selected ? 1 : 0.65;
30390
30649
  const opened = node.type === "shell" && this.open.has(node.id);
30391
30650
  const transparentLabel = node.type === "shell" && (opened || alpha < 0.65);
30392
30651
  const displayTransparentLabel = transparentLabel && this.showShellLabels;
@@ -30465,7 +30724,7 @@ var LiquidCarsAtlas = class extends HTMLElement {
30465
30724
  if (!(node.type === "shell" && this.open.has(node.id))) meshes.push(node.mesh);
30466
30725
  if (node.label?.visible) meshes.push(...node.label.children.filter((face) => face.visible));
30467
30726
  }
30468
- const nodeHit = this.raycaster.intersectObjects(meshes, false)[0] || null;
30727
+ const nodeHit = this.raycaster.intersectObjects(meshes, true)[0] || null;
30469
30728
  const relationTargets = this.relationGroup.children.filter((object) => object.userData.relationPick);
30470
30729
  const relationHit = this.raycaster.intersectObjects(relationTargets, false)[0] || null;
30471
30730
  if (nodeHit && (!relationHit || nodeHit.distance <= relationHit.distance + 0.12)) return { kind: "entity", node: nodeHit.object.userData.node };
@@ -30571,5 +30830,8 @@ var LiquidCarsAtlas = class extends HTMLElement {
30571
30830
  if (!customElements.get("liquidcars-atlas")) customElements.define("liquidcars-atlas", LiquidCarsAtlas);
30572
30831
  export {
30573
30832
  LiquidCarsAtlas,
30833
+ getGeometryCatalog,
30834
+ registerGeometryPack,
30835
+ unregisterGeometryPack,
30574
30836
  normalizeModel
30575
30837
  };
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.21"></script>
17
+ <script type="module" src="./dist/liquidcars-atlas.js?v=0.1.25"></script>
18
18
  </head>
19
19
  <body>
20
20
  <header>
21
21
  <h1>LiquidCars Atlas</h1>
22
- <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</code> · v0.1.21</p>
22
+ <p>Prueba de distribución del componente <code>&lt;liquidcars-atlas&gt;</code> · v0.1.25</p>
23
23
  </header>
24
24
  <main>
25
25
  <liquidcars-atlas id="atlas" height="720" relation-mode="selected" shell-labels="on"></liquidcars-atlas>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liquidcars/atlas",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Reusable 3D conceptual atlas Web Component for LiquidCars-style models",
5
5
  "type": "module",
6
6
  "main": "./dist/liquidcars-atlas.js",