worldorbit 2.5.16 → 2.6.0
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 +81 -15
- package/dist/browser/core/dist/index.js +1228 -110
- package/dist/browser/editor/dist/index.js +1896 -180
- package/dist/browser/markdown/dist/index.js +1071 -99
- package/dist/browser/viewer/dist/index.js +1127 -113
- package/dist/unpkg/core/dist/index.js +1228 -110
- package/dist/unpkg/editor/dist/index.js +1896 -180
- package/dist/unpkg/markdown/dist/index.js +1071 -99
- package/dist/unpkg/viewer/dist/index.js +1127 -113
- package/dist/unpkg/worldorbit-core.min.js +12 -12
- package/dist/unpkg/worldorbit-editor.min.js +295 -203
- package/dist/unpkg/worldorbit-markdown.min.js +66 -58
- package/dist/unpkg/worldorbit-viewer.min.js +84 -76
- package/dist/unpkg/worldorbit.js +1304 -124
- package/dist/unpkg/worldorbit.min.js +88 -80
- package/package.json +1 -1
- package/packages/core/dist/atlas-edit.js +75 -1
- package/packages/core/dist/atlas-validate.js +211 -8
- package/packages/core/dist/draft-parse.js +401 -22
- package/packages/core/dist/draft.d.ts +5 -2
- package/packages/core/dist/draft.js +103 -8
- package/packages/core/dist/format.js +99 -6
- package/packages/core/dist/load.js +9 -2
- package/packages/core/dist/normalize.js +1 -0
- package/packages/core/dist/scene.js +400 -64
- package/packages/core/dist/types.d.ts +60 -4
- package/packages/editor/dist/editor.js +702 -65
- package/packages/editor/dist/types.d.ts +3 -1
- package/packages/viewer/dist/atlas-state.js +11 -2
- package/packages/viewer/dist/atlas-viewer.js +19 -7
- package/packages/viewer/dist/render.js +31 -2
- package/packages/viewer/dist/theme.js +1 -0
- package/packages/viewer/dist/tooltip.js +9 -0
- package/packages/viewer/dist/types.d.ts +12 -2
- package/packages/viewer/dist/viewer.js +28 -1
|
@@ -14,12 +14,16 @@ export function renderDocumentToScene(document, options = {}) {
|
|
|
14
14
|
const height = frame.height;
|
|
15
15
|
const padding = frame.padding;
|
|
16
16
|
const layoutPreset = resolveLayoutPreset(document);
|
|
17
|
-
const
|
|
17
|
+
const schemaProjection = resolveProjection(document, options.projection);
|
|
18
|
+
const camera = normalizeViewCamera(options.camera ?? null);
|
|
19
|
+
const renderProjection = resolveRenderProjection(schemaProjection, camera);
|
|
18
20
|
const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
|
|
19
21
|
const spacingFactor = layoutPresetSpacing(layoutPreset);
|
|
20
22
|
const systemId = document.system?.id ?? null;
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
+
const activeEventId = options.activeEventId ?? null;
|
|
24
|
+
const effectiveObjects = createEffectiveObjects(document.objects, document.events ?? [], activeEventId);
|
|
25
|
+
const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
|
|
26
|
+
const relationships = buildSceneRelationships(effectiveObjects, objectMap);
|
|
23
27
|
const positions = new Map();
|
|
24
28
|
const orbitDrafts = [];
|
|
25
29
|
const leaderDrafts = [];
|
|
@@ -28,7 +32,7 @@ export function renderDocumentToScene(document, options = {}) {
|
|
|
28
32
|
const atObjects = [];
|
|
29
33
|
const surfaceChildren = new Map();
|
|
30
34
|
const orbitChildren = new Map();
|
|
31
|
-
for (const object of
|
|
35
|
+
for (const object of effectiveObjects) {
|
|
32
36
|
const placement = object.placement;
|
|
33
37
|
if (!placement) {
|
|
34
38
|
rootObjects.push(object);
|
|
@@ -55,7 +59,7 @@ export function renderDocumentToScene(document, options = {}) {
|
|
|
55
59
|
surfaceChildren,
|
|
56
60
|
objectMap,
|
|
57
61
|
spacingFactor,
|
|
58
|
-
projection,
|
|
62
|
+
projection: renderProjection,
|
|
59
63
|
scaleModel,
|
|
60
64
|
};
|
|
61
65
|
const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
|
|
@@ -70,7 +74,7 @@ export function renderDocumentToScene(document, options = {}) {
|
|
|
70
74
|
scaleModel.orbitDistanceMultiplier;
|
|
71
75
|
secondaryRoots.forEach((object, index) => {
|
|
72
76
|
const angle = angleForIndex(index, secondaryRoots.length, -Math.PI / 2);
|
|
73
|
-
const offset = projectPolarOffset(angle, rootRingRadius,
|
|
77
|
+
const offset = projectPolarOffset(angle, rootRingRadius, renderProjection, 1);
|
|
74
78
|
placeObject(object, centerX + offset.x, centerY + offset.y, 0, positions, orbitDrafts, leaderDrafts, context);
|
|
75
79
|
});
|
|
76
80
|
}
|
|
@@ -129,39 +133,49 @@ export function renderDocumentToScene(document, options = {}) {
|
|
|
129
133
|
const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
|
|
130
134
|
const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
|
|
131
135
|
const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
|
|
132
|
-
const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
|
|
136
|
+
const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
|
|
133
137
|
const relations = createSceneRelations(document, objects);
|
|
134
|
-
const
|
|
135
|
-
const
|
|
138
|
+
const events = createSceneEvents(document.events ?? [], objects, activeEventId);
|
|
139
|
+
const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
|
|
140
|
+
const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
|
|
136
141
|
const semanticGroups = createSceneSemanticGroups(document, objects);
|
|
137
|
-
const viewpoints = createSceneViewpoints(document,
|
|
138
|
-
const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
|
|
142
|
+
const viewpoints = createSceneViewpoints(document, schemaProjection, frame.preset, relationships, objectMap);
|
|
143
|
+
const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
|
|
139
144
|
return {
|
|
140
145
|
width,
|
|
141
146
|
height,
|
|
142
147
|
padding,
|
|
143
148
|
renderPreset: frame.preset,
|
|
144
|
-
projection,
|
|
149
|
+
projection: schemaProjection,
|
|
150
|
+
renderProjection,
|
|
151
|
+
camera,
|
|
145
152
|
scaleModel,
|
|
146
153
|
title: String(document.system?.title ?? document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") ||
|
|
147
154
|
"WorldOrbit",
|
|
148
|
-
subtitle:
|
|
155
|
+
subtitle: buildSceneSubtitle(schemaProjection, renderProjection, layoutPreset, camera),
|
|
149
156
|
systemId,
|
|
150
|
-
viewMode:
|
|
157
|
+
viewMode: schemaProjection,
|
|
151
158
|
layoutPreset,
|
|
152
159
|
metadata: {
|
|
153
160
|
format: document.format,
|
|
154
161
|
version: document.version,
|
|
155
|
-
view:
|
|
162
|
+
view: schemaProjection,
|
|
163
|
+
renderProjection,
|
|
156
164
|
scale: String(document.system?.properties.scale ?? layoutPreset),
|
|
157
165
|
units: String(document.system?.properties.units ?? "mixed"),
|
|
158
166
|
preset: frame.preset ?? "custom",
|
|
167
|
+
...(camera?.azimuth !== null ? { "camera.azimuth": String(camera?.azimuth) } : {}),
|
|
168
|
+
...(camera?.elevation !== null ? { "camera.elevation": String(camera?.elevation) } : {}),
|
|
169
|
+
...(camera?.roll !== null ? { "camera.roll": String(camera?.roll) } : {}),
|
|
170
|
+
...(camera?.distance !== null ? { "camera.distance": String(camera?.distance) } : {}),
|
|
159
171
|
},
|
|
160
172
|
contentBounds,
|
|
161
173
|
layers,
|
|
162
174
|
groups,
|
|
163
175
|
semanticGroups,
|
|
164
176
|
viewpoints,
|
|
177
|
+
events,
|
|
178
|
+
activeEventId,
|
|
165
179
|
objects,
|
|
166
180
|
orbitVisuals,
|
|
167
181
|
relations,
|
|
@@ -180,6 +194,56 @@ export function rotatePoint(point, center, rotationDeg) {
|
|
|
180
194
|
y: center.y + dx * sin + dy * cos,
|
|
181
195
|
};
|
|
182
196
|
}
|
|
197
|
+
function createEffectiveObjects(objects, events, activeEventId) {
|
|
198
|
+
const cloned = objects.map((object) => structuredClone(object));
|
|
199
|
+
if (!activeEventId) {
|
|
200
|
+
return cloned;
|
|
201
|
+
}
|
|
202
|
+
const activeEvent = events.find((event) => event.id === activeEventId);
|
|
203
|
+
if (!activeEvent) {
|
|
204
|
+
return cloned;
|
|
205
|
+
}
|
|
206
|
+
const objectMap = new Map(cloned.map((object) => [object.id, object]));
|
|
207
|
+
const referencedIds = new Set([
|
|
208
|
+
...(activeEvent.targetObjectId ? [activeEvent.targetObjectId] : []),
|
|
209
|
+
...activeEvent.participantObjectIds,
|
|
210
|
+
...activeEvent.positions.map((pose) => pose.objectId),
|
|
211
|
+
]);
|
|
212
|
+
for (const objectId of referencedIds) {
|
|
213
|
+
const object = objectMap.get(objectId);
|
|
214
|
+
if (!object) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (activeEvent.epoch) {
|
|
218
|
+
object.epoch = activeEvent.epoch;
|
|
219
|
+
}
|
|
220
|
+
if (activeEvent.referencePlane) {
|
|
221
|
+
object.referencePlane = activeEvent.referencePlane;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const pose of activeEvent.positions) {
|
|
225
|
+
const object = objectMap.get(pose.objectId);
|
|
226
|
+
if (!object) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (pose.placement) {
|
|
230
|
+
object.placement = structuredClone(pose.placement);
|
|
231
|
+
}
|
|
232
|
+
if (pose.inner) {
|
|
233
|
+
object.properties.inner = { ...pose.inner };
|
|
234
|
+
}
|
|
235
|
+
if (pose.outer) {
|
|
236
|
+
object.properties.outer = { ...pose.outer };
|
|
237
|
+
}
|
|
238
|
+
if (pose.epoch) {
|
|
239
|
+
object.epoch = pose.epoch;
|
|
240
|
+
}
|
|
241
|
+
if (pose.referencePlane) {
|
|
242
|
+
object.referencePlane = pose.referencePlane;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return cloned;
|
|
246
|
+
}
|
|
183
247
|
function resolveLayoutPreset(document) {
|
|
184
248
|
const rawScale = String(document.system?.properties.scale ?? "balanced").toLowerCase();
|
|
185
249
|
switch (rawScale) {
|
|
@@ -216,12 +280,69 @@ function scenePresetDefaults(preset) {
|
|
|
216
280
|
}
|
|
217
281
|
}
|
|
218
282
|
function resolveProjection(document, projection) {
|
|
219
|
-
if (projection === "topdown" ||
|
|
283
|
+
if (projection === "topdown" ||
|
|
284
|
+
projection === "isometric" ||
|
|
285
|
+
projection === "orthographic" ||
|
|
286
|
+
projection === "perspective") {
|
|
220
287
|
return projection;
|
|
221
288
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
289
|
+
const documentView = String(document.system?.properties.view ?? "topdown").toLowerCase();
|
|
290
|
+
return parseViewProjection(documentView) ?? "topdown";
|
|
291
|
+
}
|
|
292
|
+
function resolveRenderProjection(projection, camera) {
|
|
293
|
+
switch (projection) {
|
|
294
|
+
case "topdown":
|
|
295
|
+
return "topdown";
|
|
296
|
+
case "isometric":
|
|
297
|
+
return "isometric";
|
|
298
|
+
case "orthographic":
|
|
299
|
+
return camera && (camera.azimuth !== null || camera.elevation !== null || camera.roll !== null)
|
|
300
|
+
? "isometric"
|
|
301
|
+
: "topdown";
|
|
302
|
+
case "perspective":
|
|
303
|
+
return "isometric";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function normalizeViewCamera(camera) {
|
|
307
|
+
if (!camera) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
const normalized = {
|
|
311
|
+
azimuth: normalizeFiniteCameraValue(camera.azimuth),
|
|
312
|
+
elevation: normalizeFiniteCameraValue(camera.elevation),
|
|
313
|
+
roll: normalizeFiniteCameraValue(camera.roll),
|
|
314
|
+
distance: normalizePositiveCameraDistance(camera.distance),
|
|
315
|
+
};
|
|
316
|
+
return normalized.azimuth !== null ||
|
|
317
|
+
normalized.elevation !== null ||
|
|
318
|
+
normalized.roll !== null ||
|
|
319
|
+
normalized.distance !== null
|
|
320
|
+
? normalized
|
|
321
|
+
: null;
|
|
322
|
+
}
|
|
323
|
+
function normalizeFiniteCameraValue(value) {
|
|
324
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
325
|
+
}
|
|
326
|
+
function normalizePositiveCameraDistance(value) {
|
|
327
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
|
|
328
|
+
}
|
|
329
|
+
function buildSceneSubtitle(projection, renderProjection, layoutPreset, camera) {
|
|
330
|
+
const parts = [`${capitalizeLabel(projection)} view`, `${capitalizeLabel(layoutPreset)} layout`];
|
|
331
|
+
if (projection !== renderProjection) {
|
|
332
|
+
parts.push(`2D ${renderProjection} fallback`);
|
|
333
|
+
}
|
|
334
|
+
if (camera) {
|
|
335
|
+
const cameraParts = [
|
|
336
|
+
camera.azimuth !== null ? `az ${camera.azimuth}` : null,
|
|
337
|
+
camera.elevation !== null ? `el ${camera.elevation}` : null,
|
|
338
|
+
camera.roll !== null ? `roll ${camera.roll}` : null,
|
|
339
|
+
camera.distance !== null ? `dist ${camera.distance}` : null,
|
|
340
|
+
].filter(Boolean);
|
|
341
|
+
if (cameraParts.length > 0) {
|
|
342
|
+
parts.push(`camera ${cameraParts.join(" / ")}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return parts.join(" - ");
|
|
225
346
|
}
|
|
226
347
|
function resolveScaleModel(layoutPreset, overrides) {
|
|
227
348
|
const defaults = defaultScaleModel(layoutPreset);
|
|
@@ -339,26 +460,17 @@ function createLeaderLine(draft) {
|
|
|
339
460
|
hidden: draft.object.properties.hidden === true,
|
|
340
461
|
};
|
|
341
462
|
}
|
|
342
|
-
function createSceneLabels(objects, sceneHeight, labelMultiplier) {
|
|
463
|
+
function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
|
|
343
464
|
const labels = [];
|
|
344
465
|
const occupied = [];
|
|
466
|
+
const objectMap = new Map(objects.map((object) => [object.objectId, object]));
|
|
345
467
|
const visibleObjects = [...objects]
|
|
346
468
|
.filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false)
|
|
347
|
-
.sort(
|
|
469
|
+
.sort(compareLabelPlacementOrder);
|
|
348
470
|
for (const object of visibleObjects) {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
let secondaryY = labelY + direction * (16 * labelMultiplier);
|
|
353
|
-
let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
|
|
354
|
-
let attempts = 0;
|
|
355
|
-
while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
|
|
356
|
-
labelY += direction * 14 * labelMultiplier;
|
|
357
|
-
secondaryY += direction * 14 * labelMultiplier;
|
|
358
|
-
bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
|
|
359
|
-
attempts += 1;
|
|
360
|
-
}
|
|
361
|
-
occupied.push(bounds);
|
|
471
|
+
const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ??
|
|
472
|
+
createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
|
|
473
|
+
occupied.push(createLabelRect(object, placement, labelMultiplier));
|
|
362
474
|
labels.push({
|
|
363
475
|
renderId: `${object.renderId}-label`,
|
|
364
476
|
objectId: object.objectId,
|
|
@@ -367,17 +479,134 @@ function createSceneLabels(objects, sceneHeight, labelMultiplier) {
|
|
|
367
479
|
semanticGroupIds: [...object.semanticGroupIds],
|
|
368
480
|
label: object.label,
|
|
369
481
|
secondaryLabel: object.secondaryLabel,
|
|
370
|
-
x:
|
|
371
|
-
y: labelY,
|
|
372
|
-
secondaryY,
|
|
373
|
-
textAnchor:
|
|
374
|
-
direction: direction
|
|
482
|
+
x: placement.x,
|
|
483
|
+
y: placement.labelY,
|
|
484
|
+
secondaryY: placement.secondaryY,
|
|
485
|
+
textAnchor: placement.textAnchor,
|
|
486
|
+
direction: placement.direction,
|
|
375
487
|
hidden: object.hidden,
|
|
376
488
|
});
|
|
377
489
|
}
|
|
378
490
|
return labels;
|
|
379
491
|
}
|
|
380
|
-
function
|
|
492
|
+
function compareLabelPlacementOrder(left, right) {
|
|
493
|
+
const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
|
|
494
|
+
if (priorityDiff !== 0) {
|
|
495
|
+
return priorityDiff;
|
|
496
|
+
}
|
|
497
|
+
const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
|
|
498
|
+
if (renderPriorityDiff !== 0) {
|
|
499
|
+
return renderPriorityDiff;
|
|
500
|
+
}
|
|
501
|
+
return left.sortKey - right.sortKey;
|
|
502
|
+
}
|
|
503
|
+
function labelPlacementPriority(object) {
|
|
504
|
+
switch (object.object.type) {
|
|
505
|
+
case "star":
|
|
506
|
+
return 0;
|
|
507
|
+
case "planet":
|
|
508
|
+
return 1;
|
|
509
|
+
case "moon":
|
|
510
|
+
return 2;
|
|
511
|
+
case "belt":
|
|
512
|
+
case "ring":
|
|
513
|
+
return 3;
|
|
514
|
+
case "asteroid":
|
|
515
|
+
case "comet":
|
|
516
|
+
return 4;
|
|
517
|
+
case "structure":
|
|
518
|
+
case "phenomenon":
|
|
519
|
+
return 5;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
|
|
523
|
+
for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
|
|
524
|
+
const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
|
|
525
|
+
for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
|
|
526
|
+
const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
|
|
527
|
+
const rect = createLabelRect(object, placement, labelMultiplier);
|
|
528
|
+
if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
|
|
529
|
+
return placement;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
|
|
536
|
+
const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
|
|
537
|
+
const vertical = defaultVerticalDirection(object, parent, sceneHeight);
|
|
538
|
+
const oppositeVertical = vertical === "below" ? "above" : "below";
|
|
539
|
+
const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
|
|
540
|
+
const oppositeHorizontal = horizontal === "right" ? "left" : "right";
|
|
541
|
+
const preferHorizontal = object.object.type === "structure" ||
|
|
542
|
+
object.object.type === "phenomenon" ||
|
|
543
|
+
object.object.placement?.mode === "at" ||
|
|
544
|
+
object.object.placement?.mode === "surface" ||
|
|
545
|
+
object.object.placement?.mode === "free";
|
|
546
|
+
return preferHorizontal
|
|
547
|
+
? [horizontal, vertical, oppositeHorizontal, oppositeVertical]
|
|
548
|
+
: [vertical, horizontal, oppositeVertical, oppositeHorizontal];
|
|
549
|
+
}
|
|
550
|
+
function defaultVerticalDirection(object, parent, sceneHeight) {
|
|
551
|
+
if (parent && Math.abs(object.y - parent.y) > 6) {
|
|
552
|
+
return object.y >= parent.y ? "below" : "above";
|
|
553
|
+
}
|
|
554
|
+
return object.y > sceneHeight * 0.62 ? "above" : "below";
|
|
555
|
+
}
|
|
556
|
+
function defaultHorizontalDirection(object, parent, sceneWidth) {
|
|
557
|
+
if (parent && Math.abs(object.x - parent.x) > 6) {
|
|
558
|
+
return object.x >= parent.x ? "right" : "left";
|
|
559
|
+
}
|
|
560
|
+
return object.x >= sceneWidth / 2 ? "right" : "left";
|
|
561
|
+
}
|
|
562
|
+
function createLabelPlacement(object, direction, attempt, labelMultiplier) {
|
|
563
|
+
const step = 14 * labelMultiplier;
|
|
564
|
+
switch (direction) {
|
|
565
|
+
case "above": {
|
|
566
|
+
const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
|
|
567
|
+
return {
|
|
568
|
+
x: object.x,
|
|
569
|
+
labelY,
|
|
570
|
+
secondaryY: labelY - 16 * labelMultiplier,
|
|
571
|
+
textAnchor: "middle",
|
|
572
|
+
direction,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
case "below": {
|
|
576
|
+
const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
|
|
577
|
+
return {
|
|
578
|
+
x: object.x,
|
|
579
|
+
labelY,
|
|
580
|
+
secondaryY: labelY + 16 * labelMultiplier,
|
|
581
|
+
textAnchor: "middle",
|
|
582
|
+
direction,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
case "left": {
|
|
586
|
+
const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
|
|
587
|
+
const labelY = object.y - 4 * labelMultiplier;
|
|
588
|
+
return {
|
|
589
|
+
x,
|
|
590
|
+
labelY,
|
|
591
|
+
secondaryY: labelY + 16 * labelMultiplier,
|
|
592
|
+
textAnchor: "end",
|
|
593
|
+
direction,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
case "right": {
|
|
597
|
+
const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
|
|
598
|
+
const labelY = object.y - 4 * labelMultiplier;
|
|
599
|
+
return {
|
|
600
|
+
x,
|
|
601
|
+
labelY,
|
|
602
|
+
secondaryY: labelY + 16 * labelMultiplier,
|
|
603
|
+
textAnchor: "start",
|
|
604
|
+
direction,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
|
|
381
610
|
const backOrbitIds = orbitVisuals
|
|
382
611
|
.filter((visual) => !visual.hidden && Boolean(visual.backArcPath))
|
|
383
612
|
.map((visual) => visual.renderId);
|
|
@@ -396,6 +625,10 @@ function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
|
|
|
396
625
|
id: "relations",
|
|
397
626
|
renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId),
|
|
398
627
|
},
|
|
628
|
+
{
|
|
629
|
+
id: "events",
|
|
630
|
+
renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId),
|
|
631
|
+
},
|
|
399
632
|
{
|
|
400
633
|
id: "objects",
|
|
401
634
|
renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId),
|
|
@@ -407,7 +640,7 @@ function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
|
|
|
407
640
|
{ id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] },
|
|
408
641
|
];
|
|
409
642
|
}
|
|
410
|
-
function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
|
|
643
|
+
function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
|
|
411
644
|
const groups = new Map();
|
|
412
645
|
const ensureGroup = (groupId) => {
|
|
413
646
|
if (!groupId) {
|
|
@@ -456,7 +689,7 @@ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships
|
|
|
456
689
|
}
|
|
457
690
|
}
|
|
458
691
|
for (const group of groups.values()) {
|
|
459
|
-
group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
|
|
692
|
+
group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
|
|
460
693
|
}
|
|
461
694
|
return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
|
|
462
695
|
}
|
|
@@ -496,6 +729,40 @@ function createSceneRelations(document, objects) {
|
|
|
496
729
|
})
|
|
497
730
|
.sort((left, right) => left.relation.id.localeCompare(right.relation.id));
|
|
498
731
|
}
|
|
732
|
+
function createSceneEvents(events, objects, activeEventId) {
|
|
733
|
+
const objectMap = new Map(objects.map((object) => [object.objectId, object]));
|
|
734
|
+
return events
|
|
735
|
+
.map((event) => {
|
|
736
|
+
const objectIds = [...new Set([
|
|
737
|
+
...(event.targetObjectId ? [event.targetObjectId] : []),
|
|
738
|
+
...event.participantObjectIds,
|
|
739
|
+
])];
|
|
740
|
+
const positions = objectIds
|
|
741
|
+
.map((objectId) => objectMap.get(objectId))
|
|
742
|
+
.filter(Boolean);
|
|
743
|
+
const centroidX = positions.length > 0
|
|
744
|
+
? positions.reduce((sum, object) => sum + object.x, 0) / positions.length
|
|
745
|
+
: 0;
|
|
746
|
+
const centroidY = positions.length > 0
|
|
747
|
+
? positions.reduce((sum, object) => sum + object.y, 0) / positions.length
|
|
748
|
+
: 0;
|
|
749
|
+
return {
|
|
750
|
+
renderId: `${createRenderId(event.id)}-event`,
|
|
751
|
+
eventId: event.id,
|
|
752
|
+
event,
|
|
753
|
+
objectIds,
|
|
754
|
+
participantIds: [...event.participantObjectIds],
|
|
755
|
+
targetObjectId: event.targetObjectId,
|
|
756
|
+
x: centroidX,
|
|
757
|
+
y: centroidY,
|
|
758
|
+
hidden: event.hidden ||
|
|
759
|
+
positions.length === 0 ||
|
|
760
|
+
positions.every((object) => object.hidden) ||
|
|
761
|
+
(activeEventId !== null && event.id !== activeEventId),
|
|
762
|
+
};
|
|
763
|
+
})
|
|
764
|
+
.sort((left, right) => left.event.id.localeCompare(right.event.id));
|
|
765
|
+
}
|
|
499
766
|
function createSceneViewpoints(document, projection, preset, relationships, objectMap) {
|
|
500
767
|
const generatedOverview = createGeneratedOverviewViewpoint(document, projection, preset);
|
|
501
768
|
const drafts = new Map();
|
|
@@ -546,13 +813,18 @@ function createSceneViewpoints(document, projection, preset, relationships, obje
|
|
|
546
813
|
function createGeneratedOverviewViewpoint(document, projection, preset) {
|
|
547
814
|
const title = document.system?.title ?? document.system?.properties.title;
|
|
548
815
|
const label = title ? `${String(title)} Overview` : "Overview";
|
|
816
|
+
const camera = normalizeViewCamera(null);
|
|
817
|
+
const renderProjection = resolveRenderProjection(projection, camera);
|
|
549
818
|
return {
|
|
550
819
|
id: "overview",
|
|
551
820
|
label,
|
|
552
821
|
summary: "Fit the whole system with the current atlas defaults.",
|
|
553
822
|
objectId: null,
|
|
554
823
|
selectedObjectId: null,
|
|
824
|
+
eventIds: [],
|
|
555
825
|
projection,
|
|
826
|
+
renderProjection,
|
|
827
|
+
camera,
|
|
556
828
|
preset,
|
|
557
829
|
rotationDeg: 0,
|
|
558
830
|
scale: null,
|
|
@@ -588,6 +860,9 @@ function applyViewpointField(draft, field, value, document, projection, preset,
|
|
|
588
860
|
draft.select = normalizedValue;
|
|
589
861
|
}
|
|
590
862
|
return;
|
|
863
|
+
case "events":
|
|
864
|
+
draft.eventIds = splitListValue(normalizedValue);
|
|
865
|
+
return;
|
|
591
866
|
case "projection":
|
|
592
867
|
case "view":
|
|
593
868
|
draft.projection = parseViewProjection(normalizedValue) ?? projection;
|
|
@@ -599,6 +874,30 @@ function applyViewpointField(draft, field, value, document, projection, preset,
|
|
|
599
874
|
case "angle":
|
|
600
875
|
draft.rotationDeg = parseFiniteNumber(normalizedValue) ?? draft.rotationDeg ?? 0;
|
|
601
876
|
return;
|
|
877
|
+
case "camera.azimuth":
|
|
878
|
+
draft.camera = {
|
|
879
|
+
...(draft.camera ?? createEmptyViewCamera()),
|
|
880
|
+
azimuth: parseFiniteNumber(normalizedValue),
|
|
881
|
+
};
|
|
882
|
+
return;
|
|
883
|
+
case "camera.elevation":
|
|
884
|
+
draft.camera = {
|
|
885
|
+
...(draft.camera ?? createEmptyViewCamera()),
|
|
886
|
+
elevation: parseFiniteNumber(normalizedValue),
|
|
887
|
+
};
|
|
888
|
+
return;
|
|
889
|
+
case "camera.roll":
|
|
890
|
+
draft.camera = {
|
|
891
|
+
...(draft.camera ?? createEmptyViewCamera()),
|
|
892
|
+
roll: parseFiniteNumber(normalizedValue),
|
|
893
|
+
};
|
|
894
|
+
return;
|
|
895
|
+
case "camera.distance":
|
|
896
|
+
draft.camera = {
|
|
897
|
+
...(draft.camera ?? createEmptyViewCamera()),
|
|
898
|
+
distance: parsePositiveNumber(normalizedValue),
|
|
899
|
+
};
|
|
900
|
+
return;
|
|
602
901
|
case "zoom":
|
|
603
902
|
case "scale":
|
|
604
903
|
draft.scale = parsePositiveNumber(normalizedValue);
|
|
@@ -640,13 +939,19 @@ function finalizeViewpointDraft(draft, projection, preset, objectMap) {
|
|
|
640
939
|
: objectId;
|
|
641
940
|
const filter = normalizeViewpointFilter(draft.filter);
|
|
642
941
|
const label = draft.label?.trim() || humanizeIdentifier(draft.id);
|
|
942
|
+
const resolvedProjection = draft.projection ?? projection;
|
|
943
|
+
const camera = normalizeViewCamera(draft.camera ?? null);
|
|
944
|
+
const renderProjection = resolveRenderProjection(resolvedProjection, camera);
|
|
643
945
|
return {
|
|
644
946
|
id: draft.id,
|
|
645
947
|
label,
|
|
646
948
|
summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
|
|
647
949
|
objectId,
|
|
648
950
|
selectedObjectId,
|
|
649
|
-
|
|
951
|
+
eventIds: [...new Set(draft.eventIds ?? [])],
|
|
952
|
+
projection: resolvedProjection,
|
|
953
|
+
renderProjection,
|
|
954
|
+
camera,
|
|
650
955
|
preset: draft.preset ?? preset,
|
|
651
956
|
rotationDeg: draft.rotationDeg ?? 0,
|
|
652
957
|
scale: draft.scale ?? null,
|
|
@@ -663,6 +968,14 @@ function createEmptyViewpointFilter() {
|
|
|
663
968
|
groupIds: [],
|
|
664
969
|
};
|
|
665
970
|
}
|
|
971
|
+
function createEmptyViewCamera() {
|
|
972
|
+
return {
|
|
973
|
+
azimuth: null,
|
|
974
|
+
elevation: null,
|
|
975
|
+
roll: null,
|
|
976
|
+
distance: null,
|
|
977
|
+
};
|
|
978
|
+
}
|
|
666
979
|
function normalizeViewpointFilter(filter) {
|
|
667
980
|
if (!filter) {
|
|
668
981
|
return null;
|
|
@@ -681,11 +994,18 @@ function normalizeViewpointFilter(filter) {
|
|
|
681
994
|
: null;
|
|
682
995
|
}
|
|
683
996
|
function parseViewProjection(value) {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
997
|
+
switch (value.toLowerCase()) {
|
|
998
|
+
case "topdown":
|
|
999
|
+
return "topdown";
|
|
1000
|
+
case "isometric":
|
|
1001
|
+
return "isometric";
|
|
1002
|
+
case "orthographic":
|
|
1003
|
+
return "orthographic";
|
|
1004
|
+
case "perspective":
|
|
1005
|
+
return "perspective";
|
|
1006
|
+
default:
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
689
1009
|
}
|
|
690
1010
|
function parseRenderPreset(value) {
|
|
691
1011
|
const normalized = value.toLowerCase();
|
|
@@ -720,6 +1040,7 @@ function parseViewpointLayers(value) {
|
|
|
720
1040
|
rawLayer === "orbits-back" ||
|
|
721
1041
|
rawLayer === "orbits-front" ||
|
|
722
1042
|
rawLayer === "relations" ||
|
|
1043
|
+
rawLayer === "events" ||
|
|
723
1044
|
rawLayer === "objects" ||
|
|
724
1045
|
rawLayer === "labels" ||
|
|
725
1046
|
rawLayer === "metadata") {
|
|
@@ -742,6 +1063,7 @@ function parseViewpointObjectTypes(value) {
|
|
|
742
1063
|
function parseViewpointGroups(value, document, relationships, objectMap) {
|
|
743
1064
|
return splitListValue(value).map((entry) => {
|
|
744
1065
|
if (document.schemaVersion === "2.1" ||
|
|
1066
|
+
document.schemaVersion === "2.5" ||
|
|
745
1067
|
document.groups.some((group) => group.id === entry)) {
|
|
746
1068
|
return entry;
|
|
747
1069
|
}
|
|
@@ -790,7 +1112,7 @@ function createViewpointSummary(label, objectId, filter) {
|
|
|
790
1112
|
}
|
|
791
1113
|
return parts.join(" - ");
|
|
792
1114
|
}
|
|
793
|
-
function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
|
|
1115
|
+
function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
|
|
794
1116
|
let minX = Number.POSITIVE_INFINITY;
|
|
795
1117
|
let minY = Number.POSITIVE_INFINITY;
|
|
796
1118
|
let maxX = Number.NEGATIVE_INFINITY;
|
|
@@ -820,7 +1142,7 @@ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, l
|
|
|
820
1142
|
for (const label of labels) {
|
|
821
1143
|
if (label.hidden)
|
|
822
1144
|
continue;
|
|
823
|
-
includeLabelBounds(label, include);
|
|
1145
|
+
includeLabelBounds(label, include, labelMultiplier);
|
|
824
1146
|
}
|
|
825
1147
|
if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
|
|
826
1148
|
return createBounds(0, 0, width, height);
|
|
@@ -862,13 +1184,10 @@ function includeObjectBounds(object, include) {
|
|
|
862
1184
|
include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
|
|
863
1185
|
include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
|
|
864
1186
|
}
|
|
865
|
-
function includeLabelBounds(label, include) {
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
include(
|
|
869
|
-
include(label.x + labelHalfWidth, label.y + 8);
|
|
870
|
-
include(label.x - labelHalfWidth, label.secondaryY - 14);
|
|
871
|
-
include(label.x + labelHalfWidth, label.secondaryY + 8);
|
|
1187
|
+
function includeLabelBounds(label, include, labelMultiplier) {
|
|
1188
|
+
const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
|
|
1189
|
+
include(bounds.left, bounds.top);
|
|
1190
|
+
include(bounds.right, bounds.bottom);
|
|
872
1191
|
}
|
|
873
1192
|
function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
|
|
874
1193
|
if (positions.has(object.id)) {
|
|
@@ -1286,7 +1605,7 @@ function resolveParentId(object, objectMap) {
|
|
|
1286
1605
|
return null;
|
|
1287
1606
|
}
|
|
1288
1607
|
}
|
|
1289
|
-
function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
|
|
1608
|
+
function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
|
|
1290
1609
|
let minX = Number.POSITIVE_INFINITY;
|
|
1291
1610
|
let minY = Number.POSITIVE_INFINITY;
|
|
1292
1611
|
let maxX = Number.NEGATIVE_INFINITY;
|
|
@@ -1315,7 +1634,7 @@ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
|
|
|
1315
1634
|
}
|
|
1316
1635
|
for (const label of labels) {
|
|
1317
1636
|
if (!label.hidden && group.labelIds.includes(label.objectId)) {
|
|
1318
|
-
includeLabelBounds(label, include);
|
|
1637
|
+
includeLabelBounds(label, include, labelMultiplier);
|
|
1319
1638
|
}
|
|
1320
1639
|
}
|
|
1321
1640
|
if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
|
|
@@ -1340,12 +1659,29 @@ function resolveGroupRootObjectId(object, objectMap) {
|
|
|
1340
1659
|
}
|
|
1341
1660
|
return current.id;
|
|
1342
1661
|
}
|
|
1343
|
-
function createLabelRect(
|
|
1662
|
+
function createLabelRect(object, placement, labelMultiplier) {
|
|
1663
|
+
return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
|
|
1664
|
+
}
|
|
1665
|
+
function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
|
|
1666
|
+
const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
|
|
1667
|
+
const labelWidth = labelHalfWidth * 2;
|
|
1668
|
+
const topPadding = direction === "above" ? 18 : 12;
|
|
1669
|
+
const bottomPadding = direction === "above" ? 8 : 12;
|
|
1670
|
+
let left = x - labelHalfWidth;
|
|
1671
|
+
let right = x + labelHalfWidth;
|
|
1672
|
+
if (textAnchor === "start") {
|
|
1673
|
+
left = x;
|
|
1674
|
+
right = x + labelWidth;
|
|
1675
|
+
}
|
|
1676
|
+
else if (textAnchor === "end") {
|
|
1677
|
+
left = x - labelWidth;
|
|
1678
|
+
right = x;
|
|
1679
|
+
}
|
|
1344
1680
|
return {
|
|
1345
|
-
left
|
|
1346
|
-
right
|
|
1347
|
-
top: Math.min(labelY, secondaryY) -
|
|
1348
|
-
bottom: Math.max(labelY, secondaryY) +
|
|
1681
|
+
left,
|
|
1682
|
+
right,
|
|
1683
|
+
top: Math.min(labelY, secondaryY) - topPadding,
|
|
1684
|
+
bottom: Math.max(labelY, secondaryY) + bottomPadding,
|
|
1349
1685
|
};
|
|
1350
1686
|
}
|
|
1351
1687
|
function rectsOverlap(left, right) {
|