@liquidcars/atlas 0.1.37 → 0.1.39

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liquidcars/atlas",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "Reusable 3D conceptual atlas Web Component for LiquidCars-style models",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,4 +1,10 @@
1
1
  import * as THREE from "./three.module.js";
2
+ import atlasPackage from "../package.json" with { type: "json" };
3
+
4
+ // This value is bundled from the package manifest, so it always identifies
5
+ // the Atlas module that is actually executing rather than the host
6
+ // application's declared dependency.
7
+ export const ATLAS_VERSION = atlasPackage.version;
2
8
 
3
9
  const DEFAULT_PALETTE = {
4
10
  component: "#8ae4d0",
@@ -10,50 +16,156 @@ const RELATION_RENDER_ORDER = 15;
10
16
 
11
17
  const GEOMETRY_DEFINITIONS = new Map();
12
18
  const GEOMETRY_ALIASES = new Map();
19
+ const GEOMETRY_PACKS = new Map();
20
+
21
+ function cloneGeometryMetadata(value) {
22
+ if (Array.isArray(value)) return value.map(cloneGeometryMetadata);
23
+ if (!value || typeof value !== "object") return value;
24
+ return Object.fromEntries(
25
+ Object.entries(value)
26
+ .filter(([, item]) => typeof item !== "function" && item !== undefined)
27
+ .map(([key, item]) => [key, cloneGeometryMetadata(item)])
28
+ );
29
+ }
30
+
31
+ function normalizeGeometryAuthors(value) {
32
+ const authors = value == null ? [] : Array.isArray(value) ? value : [value];
33
+ return authors
34
+ .map(author => typeof author === "string" ? { name: author } : cloneGeometryMetadata(author))
35
+ .filter(author => author && String(author.name || "").trim())
36
+ .map(author => ({ ...author, name: String(author.name).trim() }));
37
+ }
38
+
39
+ function normalizeGeometryStringList(value) {
40
+ const values = value == null ? [] : Array.isArray(value) ? value : [value];
41
+ return [...new Set(values.map(item => String(item || "").trim()).filter(Boolean))];
42
+ }
43
+
44
+ function normalizeGeometryLicense(value) {
45
+ if (!value) return null;
46
+ return typeof value === "string" ? { spdx: value } : cloneGeometryMetadata(value);
47
+ }
48
+
49
+ function normalizeGeometrySource(source, index) {
50
+ const value = cloneGeometryMetadata(source || {});
51
+ const id = String(value.id || `source-${index + 1}`).trim();
52
+ if (!id) throw new TypeError("Geometry pack sources require an id.");
53
+ return {
54
+ ...value,
55
+ id,
56
+ authors: normalizeGeometryAuthors(value.authors ?? value.author),
57
+ license: normalizeGeometryLicense(value.license)
58
+ };
59
+ }
60
+
61
+ function normalizeGeometryResource(resource, index, sourceIds) {
62
+ const value = cloneGeometryMetadata(resource || {});
63
+ const id = String(value.id || "").trim();
64
+ if (!id) throw new TypeError(`Geometry pack resource at index ${index} requires an id.`);
65
+ const source = value.source == null ? null : String(value.source).trim();
66
+ if (source && !sourceIds.has(source)) {
67
+ throw new TypeError(`Geometry pack resource '${id}' references unknown source '${source}'.`);
68
+ }
69
+ return {
70
+ ...value,
71
+ id,
72
+ kind: String(value.kind || value.type || "asset"),
73
+ source,
74
+ authors: normalizeGeometryAuthors(value.authors ?? value.author),
75
+ license: normalizeGeometryLicense(value.license)
76
+ };
77
+ }
78
+
79
+ function normalizeGeometryPackMetadata(pack, namespace) {
80
+ const sources = (Array.isArray(pack.sources) ? pack.sources : []).map(normalizeGeometrySource);
81
+ const sourceIds = new Set(sources.map(source => source.id));
82
+ const resources = (Array.isArray(pack.resources) ? pack.resources : [])
83
+ .map((resource, index) => normalizeGeometryResource(resource, index, sourceIds));
84
+ if (new Set(resources.map(resource => resource.id)).size !== resources.length) {
85
+ throw new TypeError(`Geometry pack '${namespace}' contains duplicate resource ids.`);
86
+ }
87
+ return {
88
+ namespace,
89
+ name: String(pack.name || namespace),
90
+ version: String(pack.version || "0.0.0"),
91
+ description: String(pack.description || ""),
92
+ category: String(pack.category || pack.name || namespace),
93
+ homepage: pack.homepage ? String(pack.homepage) : "",
94
+ repository: cloneGeometryMetadata(pack.repository || null),
95
+ authors: normalizeGeometryAuthors(pack.authors ?? pack.author),
96
+ license: normalizeGeometryLicense(pack.license),
97
+ attribution: String(pack.attribution || ""),
98
+ collections: cloneGeometryMetadata(Array.isArray(pack.collections) ? pack.collections : []),
99
+ sources,
100
+ resources,
101
+ geometries: []
102
+ };
103
+ }
13
104
 
14
105
  function publicGeometryDefinition(definition) {
15
- const { create, ...metadata } = definition;
106
+ const { create, resourceMap, ...metadata } = definition;
16
107
  return {
17
- ...metadata,
108
+ ...cloneGeometryMetadata(metadata),
18
109
  canonicalSize: [...definition.canonicalSize],
19
- parameters: { ...definition.parameters },
20
- palette: { ...definition.palette },
21
- anchors: { ...definition.anchors }
110
+ parameters: cloneGeometryMetadata(definition.parameters),
111
+ palette: cloneGeometryMetadata(definition.palette),
112
+ anchors: cloneGeometryMetadata(definition.anchors)
22
113
  };
23
114
  }
24
115
 
116
+ function publicGeometryPack(pack) {
117
+ return cloneGeometryMetadata(pack);
118
+ }
119
+
25
120
  export function registerGeometryPack(pack) {
26
121
  if (!pack || typeof pack !== "object") throw new TypeError("A geometry pack must be an object.");
27
122
  const namespace = String(pack.namespace || "").trim();
28
- if (!/^[a-z][a-z0-9-]*$/i.test(namespace)) throw new TypeError("A geometry pack requires a valid namespace.");
123
+ if (!/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*$/i.test(namespace)) throw new TypeError("A geometry pack requires a valid dot-separated namespace.");
29
124
  if (!Array.isArray(pack.geometries) || !pack.geometries.length) throw new TypeError("A geometry pack requires at least one geometry.");
30
- const registered = [];
125
+
126
+ const metadata = normalizeGeometryPackMetadata(pack, namespace);
127
+ const resourceMap = Object.freeze(Object.fromEntries(
128
+ metadata.resources.map(resource => [resource.id, Object.freeze(cloneGeometryMetadata(resource))])
129
+ ));
130
+ const definitions = [];
31
131
  for (const source of pack.geometries) {
32
132
  if (!source || typeof source !== "object" || !source.id) throw new TypeError(`Geometry pack '${namespace}' contains an invalid geometry.`);
33
133
  const localId = String(source.id).includes(".") ? String(source.id).split(".").pop() : String(source.id);
34
134
  const id = String(source.id).includes(".") ? String(source.id) : `${namespace}.${localId}`;
35
- const definition = {
135
+ definitions.push({
36
136
  ...source,
37
137
  id,
38
138
  localId,
39
139
  namespace,
40
- pack: pack.name || namespace,
41
- packVersion: pack.version || "0.0.0",
42
- category: source.category || pack.category || pack.name || namespace,
140
+ pack: metadata.name,
141
+ packVersion: metadata.version,
142
+ category: source.category || metadata.category,
143
+ collection: source.collection == null ? "" : String(source.collection).trim(),
144
+ tags: normalizeGeometryStringList(source.tags),
145
+ keywords: normalizeGeometryStringList(source.keywords),
43
146
  canonicalSize: Array.isArray(source.canonicalSize) && source.canonicalSize.length === 3 ? source.canonicalSize.map(Number) : [2.5, 1.5, 2.5],
44
147
  parameters: source.parameters && typeof source.parameters === "object" ? source.parameters : {},
45
148
  palette: { ...(pack.palette || {}), ...(source.palette || {}) },
46
- anchors: { ...(pack.anchors || {}), ...(source.anchors || {}) }
47
- };
48
- GEOMETRY_DEFINITIONS.set(id, definition);
49
- GEOMETRY_ALIASES.set(id, id);
50
- for (const alias of source.aliases || []) GEOMETRY_ALIASES.set(String(alias), id);
51
- registered.push(id);
149
+ anchors: { ...(pack.anchors || {}), ...(source.anchors || {}) },
150
+ resourceMap
151
+ });
52
152
  }
153
+
154
+ unregisterGeometryPack(namespace);
155
+ const registered = [];
156
+ for (const definition of definitions) {
157
+ GEOMETRY_DEFINITIONS.set(definition.id, definition);
158
+ GEOMETRY_ALIASES.set(definition.id, definition.id);
159
+ for (const alias of definition.aliases || []) GEOMETRY_ALIASES.set(String(alias), definition.id);
160
+ registered.push(definition.id);
161
+ }
162
+ metadata.geometries = [...registered];
163
+ GEOMETRY_PACKS.set(namespace, metadata);
164
+ const registeredPack = publicGeometryPack(metadata);
53
165
  if (typeof globalThis.dispatchEvent === "function" && typeof CustomEvent === "function") {
54
- globalThis.dispatchEvent(new CustomEvent("lc-geometry-registry-change", { detail: { namespace, geometries: registered } }));
166
+ globalThis.dispatchEvent(new CustomEvent("lc-geometry-registry-change", { detail: { namespace, geometries: registered, pack: registeredPack } }));
55
167
  }
56
- return { namespace, geometries: registered };
168
+ return registeredPack;
57
169
  }
58
170
 
59
171
  export function unregisterGeometryPack(namespace) {
@@ -64,6 +176,7 @@ export function unregisterGeometryPack(namespace) {
64
176
  removed.push(id);
65
177
  }
66
178
  for (const [alias, id] of GEOMETRY_ALIASES) if (removed.includes(id)) GEOMETRY_ALIASES.delete(alias);
179
+ GEOMETRY_PACKS.delete(namespace);
67
180
  return removed;
68
181
  }
69
182
 
@@ -71,6 +184,10 @@ export function getGeometryCatalog() {
71
184
  return [...GEOMETRY_DEFINITIONS.values()].map(publicGeometryDefinition);
72
185
  }
73
186
 
187
+ export function getGeometryPackCatalog() {
188
+ return [...GEOMETRY_PACKS.values()].map(publicGeometryPack);
189
+ }
190
+
74
191
  function geometryDefinition(kind) {
75
192
  const id = GEOMETRY_ALIASES.get(kind) || kind;
76
193
  return GEOMETRY_DEFINITIONS.get(id) || null;
@@ -116,7 +233,11 @@ registerGeometryPack({
116
233
  namespace: "geo",
117
234
  name: "Geometric primitives",
118
235
  version: "1",
236
+ description: "Built-in Atlas geometric primitives.",
119
237
  category: "Geometric primitives",
238
+ authors: [{ name: "LiquidCars S.L.", url: "https://liquidcars.net" }],
239
+ license: { spdx: "MIT", url: "https://opensource.org/license/mit" },
240
+ homepage: "https://github.com/gaelibus/atlas",
120
241
  geometries: CORE_GEOMETRIES.map(id => ({
121
242
  id,
122
243
  primitive: id,
@@ -383,7 +504,7 @@ function createGeometryContent(definition, entity, modelPalette, invalidate, rep
383
504
  ...(entity.geometryPalette || entity.geometry_palette || {})
384
505
  };
385
506
  let descriptor = typeof definition.create === "function"
386
- ? definition.create({ parameters: { ...parameters }, colors: { ...localPalette }, entity: { ...entity } })
507
+ ? definition.create({ parameters: { ...parameters }, colors: { ...localPalette }, entity: { ...entity }, resources: definition.resourceMap || {} })
387
508
  : { parts: definition.parts || [] };
388
509
  if (Array.isArray(descriptor)) descriptor = { parts: descriptor };
389
510
  const parts = Array.isArray(descriptor?.parts) ? descriptor.parts : [];
@@ -678,11 +799,14 @@ export function captureRendererFrame(renderer, scene, camera, ownerDocument = gl
678
799
  }
679
800
 
680
801
  class LiquidCarsAtlas extends HTMLElement {
802
+ static version = ATLAS_VERSION;
803
+
681
804
  static get observedAttributes() { return ["height", "relation-mode", "selection-mode", "selection-navigation", "item-facing", "shell-labels", "toolbar", "panel", "focus-mode", "focus-effect", "focus-layout"]; }
682
805
 
683
806
  static registerGeometryPack(pack) { return registerGeometryPack(pack); }
684
807
  static unregisterGeometryPack(namespace) { return unregisterGeometryPack(namespace); }
685
808
  static getGeometryCatalog() { return getGeometryCatalog(); }
809
+ static getGeometryPackCatalog() { return getGeometryPackCatalog(); }
686
810
 
687
811
  constructor() {
688
812
  super();
@@ -1436,7 +1560,7 @@ class LiquidCarsAtlas extends HTMLElement {
1436
1560
  return this.panBy(-dx * worldPerPixel, dy * worldPerPixel);
1437
1561
  }
1438
1562
 
1439
- fit() {
1563
+ fit(options = {}) {
1440
1564
  const bounds = new THREE.Box3();
1441
1565
  for (const node of this.nodes.values()) {
1442
1566
  const center = this._worldPosition(node);
@@ -1452,6 +1576,10 @@ class LiquidCarsAtlas extends HTMLElement {
1452
1576
  const baseDistance = this._baseCameraDistance();
1453
1577
  this.targetZoom = Math.max(MIN_ZOOM, Math.min(2.8, baseDistance / requiredDistance));
1454
1578
  this.targetFocus.copy(center);
1579
+ if (options.animate === false) {
1580
+ this.zoom = this.targetZoom;
1581
+ this.focus.copy(this.targetFocus);
1582
+ }
1455
1583
  this.invalidate();
1456
1584
  return this;
1457
1585
  }