@liquidcars/atlas 0.1.38 → 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/README.md +55 -33
- package/dist/liquidcars-atlas.js +395 -307
- package/dist/liquidcars-atlas.js.map +1 -1
- package/package.json +1 -1
- package/src/liquidcars-atlas.js +141 -21
package/package.json
CHANGED
package/src/liquidcars-atlas.js
CHANGED
|
@@ -16,50 +16,156 @@ const RELATION_RENDER_ORDER = 15;
|
|
|
16
16
|
|
|
17
17
|
const GEOMETRY_DEFINITIONS = new Map();
|
|
18
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
|
+
}
|
|
19
104
|
|
|
20
105
|
function publicGeometryDefinition(definition) {
|
|
21
|
-
const { create, ...metadata } = definition;
|
|
106
|
+
const { create, resourceMap, ...metadata } = definition;
|
|
22
107
|
return {
|
|
23
|
-
...metadata,
|
|
108
|
+
...cloneGeometryMetadata(metadata),
|
|
24
109
|
canonicalSize: [...definition.canonicalSize],
|
|
25
|
-
parameters:
|
|
26
|
-
palette:
|
|
27
|
-
anchors:
|
|
110
|
+
parameters: cloneGeometryMetadata(definition.parameters),
|
|
111
|
+
palette: cloneGeometryMetadata(definition.palette),
|
|
112
|
+
anchors: cloneGeometryMetadata(definition.anchors)
|
|
28
113
|
};
|
|
29
114
|
}
|
|
30
115
|
|
|
116
|
+
function publicGeometryPack(pack) {
|
|
117
|
+
return cloneGeometryMetadata(pack);
|
|
118
|
+
}
|
|
119
|
+
|
|
31
120
|
export function registerGeometryPack(pack) {
|
|
32
121
|
if (!pack || typeof pack !== "object") throw new TypeError("A geometry pack must be an object.");
|
|
33
122
|
const namespace = String(pack.namespace || "").trim();
|
|
34
|
-
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.");
|
|
35
124
|
if (!Array.isArray(pack.geometries) || !pack.geometries.length) throw new TypeError("A geometry pack requires at least one geometry.");
|
|
36
|
-
|
|
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 = [];
|
|
37
131
|
for (const source of pack.geometries) {
|
|
38
132
|
if (!source || typeof source !== "object" || !source.id) throw new TypeError(`Geometry pack '${namespace}' contains an invalid geometry.`);
|
|
39
133
|
const localId = String(source.id).includes(".") ? String(source.id).split(".").pop() : String(source.id);
|
|
40
134
|
const id = String(source.id).includes(".") ? String(source.id) : `${namespace}.${localId}`;
|
|
41
|
-
|
|
135
|
+
definitions.push({
|
|
42
136
|
...source,
|
|
43
137
|
id,
|
|
44
138
|
localId,
|
|
45
139
|
namespace,
|
|
46
|
-
pack:
|
|
47
|
-
packVersion:
|
|
48
|
-
category: source.category ||
|
|
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),
|
|
49
146
|
canonicalSize: Array.isArray(source.canonicalSize) && source.canonicalSize.length === 3 ? source.canonicalSize.map(Number) : [2.5, 1.5, 2.5],
|
|
50
147
|
parameters: source.parameters && typeof source.parameters === "object" ? source.parameters : {},
|
|
51
148
|
palette: { ...(pack.palette || {}), ...(source.palette || {}) },
|
|
52
|
-
anchors: { ...(pack.anchors || {}), ...(source.anchors || {}) }
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
GEOMETRY_ALIASES.set(id, id);
|
|
56
|
-
for (const alias of source.aliases || []) GEOMETRY_ALIASES.set(String(alias), id);
|
|
57
|
-
registered.push(id);
|
|
149
|
+
anchors: { ...(pack.anchors || {}), ...(source.anchors || {}) },
|
|
150
|
+
resourceMap
|
|
151
|
+
});
|
|
58
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);
|
|
59
165
|
if (typeof globalThis.dispatchEvent === "function" && typeof CustomEvent === "function") {
|
|
60
|
-
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 } }));
|
|
61
167
|
}
|
|
62
|
-
return
|
|
168
|
+
return registeredPack;
|
|
63
169
|
}
|
|
64
170
|
|
|
65
171
|
export function unregisterGeometryPack(namespace) {
|
|
@@ -70,6 +176,7 @@ export function unregisterGeometryPack(namespace) {
|
|
|
70
176
|
removed.push(id);
|
|
71
177
|
}
|
|
72
178
|
for (const [alias, id] of GEOMETRY_ALIASES) if (removed.includes(id)) GEOMETRY_ALIASES.delete(alias);
|
|
179
|
+
GEOMETRY_PACKS.delete(namespace);
|
|
73
180
|
return removed;
|
|
74
181
|
}
|
|
75
182
|
|
|
@@ -77,6 +184,10 @@ export function getGeometryCatalog() {
|
|
|
77
184
|
return [...GEOMETRY_DEFINITIONS.values()].map(publicGeometryDefinition);
|
|
78
185
|
}
|
|
79
186
|
|
|
187
|
+
export function getGeometryPackCatalog() {
|
|
188
|
+
return [...GEOMETRY_PACKS.values()].map(publicGeometryPack);
|
|
189
|
+
}
|
|
190
|
+
|
|
80
191
|
function geometryDefinition(kind) {
|
|
81
192
|
const id = GEOMETRY_ALIASES.get(kind) || kind;
|
|
82
193
|
return GEOMETRY_DEFINITIONS.get(id) || null;
|
|
@@ -122,7 +233,11 @@ registerGeometryPack({
|
|
|
122
233
|
namespace: "geo",
|
|
123
234
|
name: "Geometric primitives",
|
|
124
235
|
version: "1",
|
|
236
|
+
description: "Built-in Atlas geometric primitives.",
|
|
125
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",
|
|
126
241
|
geometries: CORE_GEOMETRIES.map(id => ({
|
|
127
242
|
id,
|
|
128
243
|
primitive: id,
|
|
@@ -389,7 +504,7 @@ function createGeometryContent(definition, entity, modelPalette, invalidate, rep
|
|
|
389
504
|
...(entity.geometryPalette || entity.geometry_palette || {})
|
|
390
505
|
};
|
|
391
506
|
let descriptor = typeof definition.create === "function"
|
|
392
|
-
? definition.create({ parameters: { ...parameters }, colors: { ...localPalette }, entity: { ...entity } })
|
|
507
|
+
? definition.create({ parameters: { ...parameters }, colors: { ...localPalette }, entity: { ...entity }, resources: definition.resourceMap || {} })
|
|
393
508
|
: { parts: definition.parts || [] };
|
|
394
509
|
if (Array.isArray(descriptor)) descriptor = { parts: descriptor };
|
|
395
510
|
const parts = Array.isArray(descriptor?.parts) ? descriptor.parts : [];
|
|
@@ -691,6 +806,7 @@ class LiquidCarsAtlas extends HTMLElement {
|
|
|
691
806
|
static registerGeometryPack(pack) { return registerGeometryPack(pack); }
|
|
692
807
|
static unregisterGeometryPack(namespace) { return unregisterGeometryPack(namespace); }
|
|
693
808
|
static getGeometryCatalog() { return getGeometryCatalog(); }
|
|
809
|
+
static getGeometryPackCatalog() { return getGeometryPackCatalog(); }
|
|
694
810
|
|
|
695
811
|
constructor() {
|
|
696
812
|
super();
|
|
@@ -1444,7 +1560,7 @@ class LiquidCarsAtlas extends HTMLElement {
|
|
|
1444
1560
|
return this.panBy(-dx * worldPerPixel, dy * worldPerPixel);
|
|
1445
1561
|
}
|
|
1446
1562
|
|
|
1447
|
-
fit() {
|
|
1563
|
+
fit(options = {}) {
|
|
1448
1564
|
const bounds = new THREE.Box3();
|
|
1449
1565
|
for (const node of this.nodes.values()) {
|
|
1450
1566
|
const center = this._worldPosition(node);
|
|
@@ -1460,6 +1576,10 @@ class LiquidCarsAtlas extends HTMLElement {
|
|
|
1460
1576
|
const baseDistance = this._baseCameraDistance();
|
|
1461
1577
|
this.targetZoom = Math.max(MIN_ZOOM, Math.min(2.8, baseDistance / requiredDistance));
|
|
1462
1578
|
this.targetFocus.copy(center);
|
|
1579
|
+
if (options.animate === false) {
|
|
1580
|
+
this.zoom = this.targetZoom;
|
|
1581
|
+
this.focus.copy(this.targetFocus);
|
|
1582
|
+
}
|
|
1463
1583
|
this.invalidate();
|
|
1464
1584
|
return this;
|
|
1465
1585
|
}
|