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.
Files changed (35) hide show
  1. package/README.md +81 -15
  2. package/dist/browser/core/dist/index.js +1228 -110
  3. package/dist/browser/editor/dist/index.js +1896 -180
  4. package/dist/browser/markdown/dist/index.js +1071 -99
  5. package/dist/browser/viewer/dist/index.js +1127 -113
  6. package/dist/unpkg/core/dist/index.js +1228 -110
  7. package/dist/unpkg/editor/dist/index.js +1896 -180
  8. package/dist/unpkg/markdown/dist/index.js +1071 -99
  9. package/dist/unpkg/viewer/dist/index.js +1127 -113
  10. package/dist/unpkg/worldorbit-core.min.js +12 -12
  11. package/dist/unpkg/worldorbit-editor.min.js +295 -203
  12. package/dist/unpkg/worldorbit-markdown.min.js +66 -58
  13. package/dist/unpkg/worldorbit-viewer.min.js +84 -76
  14. package/dist/unpkg/worldorbit.js +1304 -124
  15. package/dist/unpkg/worldorbit.min.js +88 -80
  16. package/package.json +1 -1
  17. package/packages/core/dist/atlas-edit.js +75 -1
  18. package/packages/core/dist/atlas-validate.js +211 -8
  19. package/packages/core/dist/draft-parse.js +401 -22
  20. package/packages/core/dist/draft.d.ts +5 -2
  21. package/packages/core/dist/draft.js +103 -8
  22. package/packages/core/dist/format.js +99 -6
  23. package/packages/core/dist/load.js +9 -2
  24. package/packages/core/dist/normalize.js +1 -0
  25. package/packages/core/dist/scene.js +400 -64
  26. package/packages/core/dist/types.d.ts +60 -4
  27. package/packages/editor/dist/editor.js +702 -65
  28. package/packages/editor/dist/types.d.ts +3 -1
  29. package/packages/viewer/dist/atlas-state.js +11 -2
  30. package/packages/viewer/dist/atlas-viewer.js +19 -7
  31. package/packages/viewer/dist/render.js +31 -2
  32. package/packages/viewer/dist/theme.js +1 -0
  33. package/packages/viewer/dist/tooltip.js +9 -0
  34. package/packages/viewer/dist/types.d.ts +12 -2
  35. 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 projection = resolveProjection(document, options.projection);
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 objectMap = new Map(document.objects.map((object) => [object.id, object]));
22
- const relationships = buildSceneRelationships(document.objects, objectMap);
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 document.objects) {
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, projection, 1);
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 layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
135
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
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, projection, frame.preset, relationships, objectMap);
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: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
155
+ subtitle: buildSceneSubtitle(schemaProjection, renderProjection, layoutPreset, camera),
149
156
  systemId,
150
- viewMode: projection,
157
+ viewMode: schemaProjection,
151
158
  layoutPreset,
152
159
  metadata: {
153
160
  format: document.format,
154
161
  version: document.version,
155
- view: projection,
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" || projection === "isometric") {
283
+ if (projection === "topdown" ||
284
+ projection === "isometric" ||
285
+ projection === "orthographic" ||
286
+ projection === "perspective") {
220
287
  return projection;
221
288
  }
222
- return String(document.system?.properties.view ?? "topdown").toLowerCase() === "isometric"
223
- ? "isometric"
224
- : "topdown";
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((left, right) => left.sortKey - right.sortKey);
469
+ .sort(compareLabelPlacementOrder);
348
470
  for (const object of visibleObjects) {
349
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
350
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
351
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
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: object.x,
371
- y: labelY,
372
- secondaryY,
373
- textAnchor: "middle",
374
- direction: direction < 0 ? "above" : "below",
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 createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
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
- projection: draft.projection ?? projection,
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
- return value.toLowerCase() === "isometric"
685
- ? "isometric"
686
- : value.toLowerCase() === "topdown"
687
- ? "topdown"
688
- : null;
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 labelScale = 1;
867
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
868
- include(label.x - labelHalfWidth, label.y - 18);
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(x, labelY, secondaryY, labelHalfWidth, direction) {
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: x - labelHalfWidth,
1346
- right: x + labelHalfWidth,
1347
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
1348
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12),
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) {