worldorbit 2.5.12 → 2.5.15

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 (47) hide show
  1. package/README.md +37 -11
  2. package/dist/unpkg/worldorbit-core.min.js +12 -5
  3. package/dist/unpkg/worldorbit-markdown.min.js +29 -20
  4. package/dist/unpkg/worldorbit-viewer.min.js +52 -38
  5. package/dist/unpkg/worldorbit.js +1737 -245
  6. package/dist/unpkg/worldorbit.min.js +56 -42
  7. package/package.json +2 -2
  8. package/packages/core/README.md +5 -1
  9. package/packages/core/dist/atlas-edit.d.ts +2 -2
  10. package/packages/core/dist/atlas-edit.js +70 -7
  11. package/packages/core/dist/atlas-utils.d.ts +22 -0
  12. package/packages/core/dist/atlas-utils.js +189 -0
  13. package/packages/core/dist/atlas-validate.d.ts +2 -0
  14. package/packages/core/dist/atlas-validate.js +285 -0
  15. package/packages/core/dist/draft-parse.js +786 -153
  16. package/packages/core/dist/draft.d.ts +3 -0
  17. package/packages/core/dist/draft.js +47 -3
  18. package/packages/core/dist/format.js +165 -9
  19. package/packages/core/dist/load.js +58 -13
  20. package/packages/core/dist/normalize.js +7 -0
  21. package/packages/core/dist/scene.js +92 -27
  22. package/packages/core/dist/types.d.ts +97 -3
  23. package/packages/markdown/README.md +1 -1
  24. package/packages/viewer/README.md +2 -1
  25. package/packages/viewer/dist/atlas-state.js +7 -1
  26. package/packages/viewer/dist/atlas-viewer.js +35 -1
  27. package/packages/viewer/dist/render.js +16 -7
  28. package/packages/viewer/dist/theme.js +4 -0
  29. package/packages/viewer/dist/tooltip.js +35 -0
  30. package/packages/viewer/dist/types.d.ts +7 -0
  31. package/packages/viewer/dist/viewer.js +4 -0
  32. package/packages/editor/dist/editor.d.ts +0 -2
  33. package/packages/editor/dist/editor.js +0 -2626
  34. package/packages/editor/dist/index.d.ts +0 -2
  35. package/packages/editor/dist/index.js +0 -1
  36. package/packages/editor/dist/types.d.ts +0 -53
  37. package/packages/editor/dist/types.js +0 -1
  38. package/packages/markdown/dist/html.d.ts +0 -3
  39. package/packages/markdown/dist/html.js +0 -57
  40. package/packages/markdown/dist/index.d.ts +0 -4
  41. package/packages/markdown/dist/index.js +0 -3
  42. package/packages/markdown/dist/rehype.d.ts +0 -10
  43. package/packages/markdown/dist/rehype.js +0 -49
  44. package/packages/markdown/dist/remark.d.ts +0 -9
  45. package/packages/markdown/dist/remark.js +0 -28
  46. package/packages/markdown/dist/types.d.ts +0 -11
  47. package/packages/markdown/dist/types.js +0 -1
@@ -130,8 +130,10 @@ export function renderDocumentToScene(document, options = {}) {
130
130
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
131
131
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
132
132
  const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
133
- const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
133
+ const relations = createSceneRelations(document, objects);
134
+ const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
134
135
  const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
136
+ const semanticGroups = createSceneSemanticGroups(document, objects);
135
137
  const viewpoints = createSceneViewpoints(document, projection, frame.preset, relationships, objectMap);
136
138
  const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
137
139
  return {
@@ -141,7 +143,7 @@ export function renderDocumentToScene(document, options = {}) {
141
143
  renderPreset: frame.preset,
142
144
  projection,
143
145
  scaleModel,
144
- title: String(document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") ||
146
+ title: String(document.system?.title ?? document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") ||
145
147
  "WorldOrbit",
146
148
  subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
147
149
  systemId,
@@ -158,9 +160,11 @@ export function renderDocumentToScene(document, options = {}) {
158
160
  contentBounds,
159
161
  layers,
160
162
  groups,
163
+ semanticGroups,
161
164
  viewpoints,
162
165
  objects,
163
166
  orbitVisuals,
167
+ relations,
164
168
  leaders,
165
169
  labels,
166
170
  };
@@ -272,6 +276,7 @@ function layoutPresetSpacing(layoutPreset) {
272
276
  }
273
277
  function createSceneObject(position, scaleModel, relationships) {
274
278
  const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
279
+ const renderPriority = object.renderHints?.renderPriority ?? 0;
275
280
  return {
276
281
  renderId: createRenderId(object.id),
277
282
  objectId: object.id,
@@ -280,11 +285,12 @@ function createSceneObject(position, scaleModel, relationships) {
280
285
  ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
281
286
  childIds: relationships.childIds.get(object.id) ?? [],
282
287
  groupId: relationships.groupIds.get(object.id) ?? null,
288
+ semanticGroupIds: [...(object.groups ?? [])],
283
289
  x,
284
290
  y,
285
291
  radius,
286
292
  visualRadius: visualExtentForObject(object, radius, scaleModel),
287
- sortKey,
293
+ sortKey: sortKey + renderPriority * 0.001,
288
294
  anchorX,
289
295
  anchorY,
290
296
  label: object.id,
@@ -303,6 +309,7 @@ function createOrbitVisual(draft, groupId) {
303
309
  object: draft.object,
304
310
  parentId: draft.parentId,
305
311
  groupId,
312
+ semanticGroupIds: [...(draft.object.groups ?? [])],
306
313
  kind: draft.kind,
307
314
  cx: draft.cx,
308
315
  cy: draft.cy,
@@ -314,7 +321,7 @@ function createOrbitVisual(draft, groupId) {
314
321
  bandThickness: draft.bandThickness,
315
322
  frontArcPath: draft.frontArcPath,
316
323
  backArcPath: draft.backArcPath,
317
- hidden: draft.object.properties.hidden === true,
324
+ hidden: draft.object.properties.hidden === true || draft.object.renderHints?.renderOrbit === false,
318
325
  };
319
326
  }
320
327
  function createLeaderLine(draft) {
@@ -323,6 +330,7 @@ function createLeaderLine(draft) {
323
330
  objectId: draft.object.id,
324
331
  object: draft.object,
325
332
  groupId: draft.groupId,
333
+ semanticGroupIds: [...(draft.object.groups ?? [])],
326
334
  x1: draft.x1,
327
335
  y1: draft.y1,
328
336
  x2: draft.x2,
@@ -335,7 +343,7 @@ function createSceneLabels(objects, sceneHeight, labelMultiplier) {
335
343
  const labels = [];
336
344
  const occupied = [];
337
345
  const visibleObjects = [...objects]
338
- .filter((object) => !object.hidden)
346
+ .filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false)
339
347
  .sort((left, right) => left.sortKey - right.sortKey);
340
348
  for (const object of visibleObjects) {
341
349
  const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
@@ -356,6 +364,7 @@ function createSceneLabels(objects, sceneHeight, labelMultiplier) {
356
364
  objectId: object.objectId,
357
365
  object: object.object,
358
366
  groupId: object.groupId,
367
+ semanticGroupIds: [...object.semanticGroupIds],
359
368
  label: object.label,
360
369
  secondaryLabel: object.secondaryLabel,
361
370
  x: object.x,
@@ -368,7 +377,7 @@ function createSceneLabels(objects, sceneHeight, labelMultiplier) {
368
377
  }
369
378
  return labels;
370
379
  }
371
- function createSceneLayers(orbitVisuals, leaders, objects, labels) {
380
+ function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
372
381
  const backOrbitIds = orbitVisuals
373
382
  .filter((visual) => !visual.hidden && Boolean(visual.backArcPath))
374
383
  .map((visual) => visual.renderId);
@@ -383,6 +392,10 @@ function createSceneLayers(orbitVisuals, leaders, objects, labels) {
383
392
  },
384
393
  { id: "orbits-back", renderIds: backOrbitIds },
385
394
  { id: "orbits-front", renderIds: frontOrbitIds },
395
+ {
396
+ id: "relations",
397
+ renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId),
398
+ },
386
399
  {
387
400
  id: "objects",
388
401
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId),
@@ -447,6 +460,42 @@ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships
447
460
  }
448
461
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
449
462
  }
463
+ function createSceneSemanticGroups(document, objects) {
464
+ return [...document.groups]
465
+ .map((group) => ({
466
+ id: group.id,
467
+ label: group.label,
468
+ summary: group.summary,
469
+ color: group.color,
470
+ tags: [...group.tags],
471
+ hidden: group.hidden,
472
+ objectIds: objects
473
+ .filter((object) => !object.hidden && object.semanticGroupIds.includes(group.id))
474
+ .map((object) => object.objectId),
475
+ }))
476
+ .sort((left, right) => left.label.localeCompare(right.label));
477
+ }
478
+ function createSceneRelations(document, objects) {
479
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
480
+ return document.relations
481
+ .map((relation) => {
482
+ const from = objectMap.get(relation.from);
483
+ const to = objectMap.get(relation.to);
484
+ return {
485
+ renderId: `${createRenderId(relation.id)}-relation`,
486
+ relationId: relation.id,
487
+ relation,
488
+ fromObjectId: relation.from,
489
+ toObjectId: relation.to,
490
+ x1: from?.x ?? 0,
491
+ y1: from?.y ?? 0,
492
+ x2: to?.x ?? 0,
493
+ y2: to?.y ?? 0,
494
+ hidden: relation.hidden || !from || !to || from.hidden || to.hidden,
495
+ };
496
+ })
497
+ .sort((left, right) => left.relation.id.localeCompare(right.relation.id));
498
+ }
450
499
  function createSceneViewpoints(document, projection, preset, relationships, objectMap) {
451
500
  const generatedOverview = createGeneratedOverviewViewpoint(document, projection, preset);
452
501
  const drafts = new Map();
@@ -464,7 +513,7 @@ function createSceneViewpoints(document, projection, preset, relationships, obje
464
513
  }
465
514
  const field = fieldParts.join(".").toLowerCase();
466
515
  const draft = drafts.get(id) ?? { id };
467
- applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
516
+ applyViewpointField(draft, field, value, document, projection, preset, relationships, objectMap);
468
517
  drafts.set(id, draft);
469
518
  }
470
519
  const viewpoints = [...drafts.values()]
@@ -495,9 +544,8 @@ function createSceneViewpoints(document, projection, preset, relationships, obje
495
544
  });
496
545
  }
497
546
  function createGeneratedOverviewViewpoint(document, projection, preset) {
498
- const label = document.system?.properties.title
499
- ? `${String(document.system.properties.title)} Overview`
500
- : "Overview";
547
+ const title = document.system?.title ?? document.system?.properties.title;
548
+ const label = title ? `${String(title)} Overview` : "Overview";
501
549
  return {
502
550
  id: "overview",
503
551
  label,
@@ -513,7 +561,7 @@ function createGeneratedOverviewViewpoint(document, projection, preset) {
513
561
  generated: true,
514
562
  };
515
563
  }
516
- function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
564
+ function applyViewpointField(draft, field, value, document, projection, preset, relationships, objectMap) {
517
565
  const normalizedValue = value.trim();
518
566
  switch (field) {
519
567
  case "label":
@@ -580,7 +628,7 @@ function applyViewpointField(draft, field, value, projection, preset, relationsh
580
628
  case "groups":
581
629
  draft.filter = {
582
630
  ...(draft.filter ?? createEmptyViewpointFilter()),
583
- groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap),
631
+ groupIds: parseViewpointGroups(normalizedValue, document, relationships, objectMap),
584
632
  };
585
633
  return;
586
634
  }
@@ -671,6 +719,7 @@ function parseViewpointLayers(value) {
671
719
  rawLayer === "guides" ||
672
720
  rawLayer === "orbits-back" ||
673
721
  rawLayer === "orbits-front" ||
722
+ rawLayer === "relations" ||
674
723
  rawLayer === "objects" ||
675
724
  rawLayer === "labels" ||
676
725
  rawLayer === "metadata") {
@@ -690,8 +739,12 @@ function parseViewpointObjectTypes(value) {
690
739
  entry === "structure" ||
691
740
  entry === "phenomenon");
692
741
  }
693
- function parseViewpointGroups(value, relationships, objectMap) {
742
+ function parseViewpointGroups(value, document, relationships, objectMap) {
694
743
  return splitListValue(value).map((entry) => {
744
+ if (document.schemaVersion === "2.1" ||
745
+ document.groups.some((group) => group.id === entry)) {
746
+ return entry;
747
+ }
695
748
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
696
749
  return entry;
697
750
  }
@@ -837,8 +890,9 @@ function placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, con
837
890
  }
838
891
  const orbiting = [...(context.orbitChildren.get(object.id) ?? [])].sort(compareOrbiting);
839
892
  const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
893
+ const orbitRadiiPx = resolveOrbitRadiiPx(orbiting, orbitMetricContext);
840
894
  orbiting.forEach((child, index) => {
841
- const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, context);
895
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, orbitRadiiPx[index] ?? orbitMetricContext.innerPx, context);
842
896
  orbitDrafts.push({
843
897
  object: child,
844
898
  parentId: object.id,
@@ -913,6 +967,7 @@ function computeOrbitMetricContext(objects, parentRadius, spacingFactor, scaleMo
913
967
  innerPx,
914
968
  stepPx,
915
969
  pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
970
+ minimumGapPx: stepPx * 0.42,
916
971
  };
917
972
  }
918
973
  const minMetric = Math.min(...presentMetrics);
@@ -926,9 +981,10 @@ function computeOrbitMetricContext(objects, parentRadius, spacingFactor, scaleMo
926
981
  innerPx,
927
982
  stepPx,
928
983
  pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
984
+ minimumGapPx: stepPx * 0.42,
929
985
  };
930
986
  }
931
- function resolveOrbitGeometry(object, index, count, parent, metricContext, context) {
987
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, orbitRadiusPx, context) {
932
988
  const placement = object.placement;
933
989
  const band = object.type === "belt" || object.type === "ring";
934
990
  if (!placement || placement.mode !== "orbit") {
@@ -948,7 +1004,7 @@ function resolveOrbitGeometry(object, index, count, parent, metricContext, conte
948
1004
  };
949
1005
  }
950
1006
  const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
951
- const semiMajor = resolveOrbitRadiusPx(object, index, metricContext);
1007
+ const semiMajor = orbitRadiusPx;
952
1008
  const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
953
1009
  const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
954
1010
  const inclinationScale = context.projection === "isometric"
@@ -989,17 +1045,23 @@ function resolveOrbitGeometry(object, index, count, parent, metricContext, conte
989
1045
  objectY: objectPoint.y,
990
1046
  };
991
1047
  }
992
- function resolveOrbitRadiusPx(object, index, metricContext) {
993
- const metric = orbitMetric(object);
994
- if (metric === null) {
995
- return metricContext.innerPx + index * metricContext.stepPx;
996
- }
997
- if (metricContext.metricSpread > 0) {
998
- return (metricContext.innerPx +
999
- ((metric - metricContext.minMetric) / metricContext.metricSpread) *
1000
- metricContext.pixelSpread);
1001
- }
1002
- return metricContext.innerPx + Math.log10(metric + 1) * metricContext.stepPx;
1048
+ function resolveOrbitRadiusPx(metric, metricContext) {
1049
+ return metricContext.innerPx + metricContext.stepPx * log2(Math.max(metric, 0) + 1);
1050
+ }
1051
+ function resolveOrbitRadiiPx(objects, metricContext) {
1052
+ const radii = [];
1053
+ objects.forEach((object, index) => {
1054
+ const metric = orbitMetric(object);
1055
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
1056
+ const baseRadius = metric === null
1057
+ ? fallbackRadius
1058
+ : resolveOrbitRadiusPx(metric, metricContext);
1059
+ const minimumRadius = index === 0
1060
+ ? metricContext.innerPx
1061
+ : (radii[index - 1] ?? metricContext.innerPx) + metricContext.minimumGapPx;
1062
+ radii.push(Math.max(baseRadius, minimumRadius));
1063
+ });
1064
+ return radii;
1003
1065
  }
1004
1066
  function orbitMetric(object) {
1005
1067
  if (!object.placement || object.placement.mode !== "orbit") {
@@ -1007,6 +1069,9 @@ function orbitMetric(object) {
1007
1069
  }
1008
1070
  return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
1009
1071
  }
1072
+ function log2(value) {
1073
+ return Math.log(value) / Math.log(2);
1074
+ }
1010
1075
  function resolveOrbitPhase(phase, index, count) {
1011
1076
  const degreeValue = phase ? unitValueToDegrees(phase) : null;
1012
1077
  if (degreeValue !== null) {
@@ -2,7 +2,7 @@ export type WorldOrbitObjectType = "system" | "star" | "planet" | "moon" | "belt
2
2
  export type PlacementMode = "orbit" | "at" | "surface" | "free";
3
3
  export type Unit = "au" | "km" | "m" | "ly" | "pc" | "kpc" | "re" | "rj" | "sol" | "me" | "mj" | "s" | "min" | "h" | "d" | "y" | "ky" | "my" | "gy" | "K" | "deg";
4
4
  export type WorldOrbitDocumentVersion = "1.0";
5
- export type WorldOrbitAtlasDocumentVersion = "2.0";
5
+ export type WorldOrbitAtlasDocumentVersion = "2.0" | "2.1";
6
6
  export type WorldOrbitDraftDocumentVersion = "2.0-draft";
7
7
  export type WorldOrbitAnyDocumentVersion = WorldOrbitDocumentVersion | WorldOrbitAtlasDocumentVersion | WorldOrbitDraftDocumentVersion;
8
8
  export type ViewProjection = "topdown" | "isometric";
@@ -56,38 +56,102 @@ export interface AstInfoEntryNode {
56
56
  export interface WorldOrbitDocument {
57
57
  format: "worldorbit";
58
58
  version: WorldOrbitDocumentVersion;
59
+ schemaVersion: WorldOrbitAnyDocumentVersion;
59
60
  system: WorldOrbitSystem | null;
61
+ groups: WorldOrbitGroup[];
62
+ relations: WorldOrbitRelation[];
60
63
  objects: WorldOrbitObject[];
61
64
  }
62
65
  export interface WorldOrbitAtlasDocument {
63
66
  format: "worldorbit";
64
67
  version: WorldOrbitAtlasDocumentVersion;
68
+ schemaVersion: WorldOrbitAtlasDocumentVersion;
65
69
  sourceVersion: WorldOrbitDocumentVersion;
66
70
  system: WorldOrbitAtlasSystem | null;
71
+ groups: WorldOrbitGroup[];
72
+ relations: WorldOrbitRelation[];
67
73
  objects: WorldOrbitObject[];
68
74
  diagnostics: WorldOrbitDiagnostic[];
69
75
  }
70
76
  export interface WorldOrbitDraftDocument {
71
77
  format: "worldorbit";
72
78
  version: WorldOrbitDraftDocumentVersion;
79
+ schemaVersion: WorldOrbitDraftDocumentVersion;
73
80
  sourceVersion: WorldOrbitDocumentVersion;
74
81
  system: WorldOrbitAtlasSystem | null;
82
+ groups: WorldOrbitGroup[];
83
+ relations: WorldOrbitRelation[];
75
84
  objects: WorldOrbitObject[];
76
85
  diagnostics: WorldOrbitDiagnostic[];
77
86
  }
78
87
  export interface WorldOrbitSystem {
79
88
  type: "system";
80
89
  id: string;
90
+ title?: string | null;
91
+ description?: string | null;
92
+ epoch?: string | null;
93
+ referencePlane?: string | null;
81
94
  properties: Record<string, NormalizedValue>;
82
95
  info: Record<string, string>;
83
96
  }
84
97
  export interface WorldOrbitObject {
85
98
  type: Exclude<WorldOrbitObjectType, "system">;
86
99
  id: string;
100
+ groups?: string[];
101
+ epoch?: string | null;
102
+ referencePlane?: string | null;
103
+ tidalLock?: boolean;
104
+ resonance?: WorldOrbitResonance | null;
105
+ renderHints?: WorldOrbitRenderHints | null;
106
+ deriveRules?: WorldOrbitDeriveRule[];
107
+ validationRules?: WorldOrbitValidationRule[];
108
+ lockedFields?: string[];
109
+ tolerances?: WorldOrbitToleranceRule[];
110
+ typedBlocks?: Partial<Record<WorldOrbitTypedBlockName, Record<string, string>>>;
87
111
  properties: Record<string, NormalizedValue>;
88
112
  placement: Placement | null;
89
113
  info: Record<string, string>;
90
114
  }
115
+ export type WorldOrbitTypedBlockName = "climate" | "habitability" | "settlement";
116
+ export interface WorldOrbitGroup {
117
+ id: string;
118
+ label: string;
119
+ summary: string;
120
+ color: string | null;
121
+ tags: string[];
122
+ hidden: boolean;
123
+ }
124
+ export interface WorldOrbitRelation {
125
+ id: string;
126
+ from: string;
127
+ to: string;
128
+ kind: string;
129
+ label: string | null;
130
+ summary: string | null;
131
+ tags: string[];
132
+ color: string | null;
133
+ hidden: boolean;
134
+ }
135
+ export interface WorldOrbitResonance {
136
+ targetObjectId: string;
137
+ ratio: string;
138
+ }
139
+ export interface WorldOrbitRenderHints {
140
+ renderLabel?: boolean;
141
+ renderOrbit?: boolean;
142
+ renderPriority?: number;
143
+ }
144
+ export interface WorldOrbitDeriveRule {
145
+ field: string;
146
+ strategy: string;
147
+ }
148
+ export interface WorldOrbitValidationRule {
149
+ rule: string;
150
+ }
151
+ export interface WorldOrbitToleranceRule {
152
+ field: string;
153
+ value: NormalizedValue;
154
+ }
91
155
  export type NormalizedValue = string | number | boolean | string[] | UnitValue;
92
156
  export type Placement = OrbitPlacement | AtPlacement | SurfacePlacement | FreePlacement;
93
157
  export interface OrbitPlacement {
@@ -167,6 +231,7 @@ export interface RenderSceneObject {
167
231
  ancestorIds: string[];
168
232
  childIds: string[];
169
233
  groupId: string | null;
234
+ semanticGroupIds: string[];
170
235
  x: number;
171
236
  y: number;
172
237
  radius: number;
@@ -186,6 +251,7 @@ export interface RenderOrbitVisual {
186
251
  object: WorldOrbitObject;
187
252
  parentId: string;
188
253
  groupId: string | null;
254
+ semanticGroupIds: string[];
189
255
  kind: "circle" | "ellipse";
190
256
  cx: number;
191
257
  cy: number;
@@ -205,6 +271,7 @@ export interface RenderLeaderLine {
205
271
  objectId: string;
206
272
  object: WorldOrbitObject;
207
273
  groupId: string | null;
274
+ semanticGroupIds: string[];
208
275
  x1: number;
209
276
  y1: number;
210
277
  x2: number;
@@ -217,6 +284,7 @@ export interface RenderSceneLabel {
217
284
  objectId: string;
218
285
  object: WorldOrbitObject;
219
286
  groupId: string | null;
287
+ semanticGroupIds: string[];
220
288
  label: string;
221
289
  secondaryLabel: string;
222
290
  x: number;
@@ -226,7 +294,7 @@ export interface RenderSceneLabel {
226
294
  direction: "above" | "below" | "left" | "right";
227
295
  hidden: boolean;
228
296
  }
229
- export type SceneLayerId = "background" | "guides" | "orbits-back" | "orbits-front" | "objects" | "labels" | "metadata";
297
+ export type SceneLayerId = "background" | "guides" | "orbits-back" | "orbits-front" | "relations" | "objects" | "labels" | "metadata";
230
298
  export interface RenderSceneViewpointFilter {
231
299
  query: string | null;
232
300
  objectTypes: Array<Exclude<WorldOrbitObjectType, "system">>;
@@ -261,6 +329,27 @@ export interface RenderSceneGroup {
261
329
  leaderIds: string[];
262
330
  contentBounds: RenderBounds;
263
331
  }
332
+ export interface RenderSceneSemanticGroup {
333
+ id: string;
334
+ label: string;
335
+ summary: string;
336
+ color: string | null;
337
+ tags: string[];
338
+ hidden: boolean;
339
+ objectIds: string[];
340
+ }
341
+ export interface RenderSceneRelation {
342
+ renderId: string;
343
+ relationId: string;
344
+ relation: WorldOrbitRelation;
345
+ fromObjectId: string;
346
+ toObjectId: string;
347
+ x1: number;
348
+ y1: number;
349
+ x2: number;
350
+ y2: number;
351
+ hidden: boolean;
352
+ }
264
353
  export interface RenderScene {
265
354
  width: number;
266
355
  height: number;
@@ -277,9 +366,11 @@ export interface RenderScene {
277
366
  contentBounds: RenderBounds;
278
367
  layers: RenderSceneLayer[];
279
368
  groups: RenderSceneGroup[];
369
+ semanticGroups: RenderSceneSemanticGroup[];
280
370
  viewpoints: RenderSceneViewpoint[];
281
371
  objects: RenderSceneObject[];
282
372
  orbitVisuals: RenderOrbitVisual[];
373
+ relations: RenderSceneRelation[];
283
374
  leaders: RenderLeaderLine[];
284
375
  labels: RenderSceneLabel[];
285
376
  }
@@ -349,12 +440,15 @@ export interface WorldOrbitAtlasSystem {
349
440
  type: "system";
350
441
  id: string;
351
442
  title: string | null;
443
+ description: string | null;
444
+ epoch: string | null;
445
+ referencePlane: string | null;
352
446
  defaults: WorldOrbitAtlasDefaults;
353
447
  atlasMetadata: Record<string, string>;
354
448
  viewpoints: WorldOrbitAtlasViewpoint[];
355
449
  annotations: WorldOrbitAtlasAnnotation[];
356
450
  }
357
- export type AtlasDocumentPathKind = "system" | "defaults" | "metadata" | "object" | "viewpoint" | "annotation";
451
+ export type AtlasDocumentPathKind = "system" | "defaults" | "metadata" | "group" | "object" | "viewpoint" | "annotation" | "relation";
358
452
  export interface AtlasDocumentPath {
359
453
  kind: AtlasDocumentPathKind;
360
454
  id?: string;
@@ -1,6 +1,6 @@
1
1
  # @worldorbit/markdown
2
2
 
3
- WorldOrbit markdown contains build-time integration for fenced `worldorbit` blocks in Unified, Remark, and Rehype pipelines.
3
+ WorldOrbit markdown contains build-time integration for fenced `worldorbit` blocks in Unified, Remark, and Rehype pipelines and accepts the same Schema 2.1 atlas source as the core loader.
4
4
 
5
5
  Main exports:
6
6
 
@@ -1,6 +1,6 @@
1
1
  # @worldorbit/viewer
2
2
 
3
- WorldOrbit viewer contains scene-to-SVG rendering, theme presets, browser interactivity, and embed hydration helpers.
3
+ WorldOrbit viewer contains scene-to-SVG rendering, theme presets, browser interactivity, atlas controls, and embed hydration helpers for WorldOrbit documents up to Schema 2.1.
4
4
 
5
5
  Main exports:
6
6
 
@@ -8,5 +8,6 @@ Main exports:
8
8
  - `renderDocumentToSvg(document, options?)`
9
9
  - `renderSourceToSvg(source, options?)`
10
10
  - `createInteractiveViewer(container, options)`
11
+ - `createAtlasViewer(container, options)`
11
12
  - `createWorldOrbitEmbedMarkup(payload, options?)`
12
13
  - `mountWorldOrbitEmbeds(root?, options?)`
@@ -133,6 +133,7 @@ export function sceneViewpointToLayerOptions(viewpoint) {
133
133
  return {
134
134
  background: viewpoint.layers.background,
135
135
  guides: viewpoint.layers.guides,
136
+ relations: viewpoint.layers.relations,
136
137
  orbits: viewpoint.layers["orbits-front"] === undefined && viewpoint.layers["orbits-back"] === undefined
137
138
  ? undefined
138
139
  : viewpoint.layers["orbits-front"] !== false || viewpoint.layers["orbits-back"] !== false,
@@ -174,7 +175,12 @@ function matchesObjectFilter(object, filter) {
174
175
  return false;
175
176
  }
176
177
  if (filter.groupIds?.length && (!object.groupId || !filter.groupIds.includes(object.groupId))) {
177
- return false;
178
+ const hasSemanticMatch = object.semanticGroupIds.length > 0 &&
179
+ filter.groupIds.some((groupId) => object.semanticGroupIds.includes(groupId));
180
+ const hasLegacyMatch = Boolean(object.groupId && filter.groupIds.includes(object.groupId));
181
+ if (!hasSemanticMatch && !hasLegacyMatch) {
182
+ return false;
183
+ }
178
184
  }
179
185
  if (filter.tags?.length) {
180
186
  const objectTags = Array.isArray(object.object.properties.tags)
@@ -9,6 +9,7 @@ export function createAtlasViewer(container, options) {
9
9
  const controls = {
10
10
  search: options.controls?.search ?? true,
11
11
  typeFilter: options.controls?.typeFilter ?? true,
12
+ groupFilter: options.controls?.groupFilter ?? true,
12
13
  viewpointSelect: options.controls?.viewpointSelect ?? true,
13
14
  inspector: options.controls?.inspector ?? true,
14
15
  bookmarks: options.controls?.bookmarks ?? true,
@@ -18,6 +19,7 @@ export function createAtlasViewer(container, options) {
18
19
  const toolbar = container.querySelector("[data-atlas-toolbar]");
19
20
  const searchInput = container.querySelector("[data-atlas-search]");
20
21
  const typeFilterSelect = container.querySelector("[data-atlas-type-filter]");
22
+ const groupFilterSelect = container.querySelector("[data-atlas-group-filter]");
21
23
  const viewpointSelect = container.querySelector("[data-atlas-viewpoint]");
22
24
  const bookmarkButton = container.querySelector("[data-atlas-bookmark]");
23
25
  const bookmarkList = container.querySelector("[data-atlas-bookmarks]");
@@ -31,6 +33,7 @@ export function createAtlasViewer(container, options) {
31
33
  let searchQuery = options.initialQuery?.trim() ?? baseFilter?.query ?? "";
32
34
  let objectTypeFilter = options.initialObjectType ??
33
35
  (baseFilter?.objectTypes?.length === 1 ? baseFilter.objectTypes[0] : null);
36
+ let groupFilter = baseFilter?.groupIds?.[0] ?? null;
34
37
  let bookmarks = [];
35
38
  let viewer;
36
39
  viewer = createInteractiveViewer(stage, {
@@ -78,6 +81,7 @@ export function createAtlasViewer(container, options) {
78
81
  });
79
82
  applyCurrentFilter();
80
83
  populateViewpoints();
84
+ populateGroups();
81
85
  syncControlsFromFilter(viewer.getFilter());
82
86
  renderBookmarks();
83
87
  updateSearchResults();
@@ -90,6 +94,10 @@ export function createAtlasViewer(container, options) {
90
94
  objectTypeFilter = (typeFilterSelect.value || null);
91
95
  applyCurrentFilter();
92
96
  });
97
+ groupFilterSelect?.addEventListener("change", () => {
98
+ groupFilter = groupFilterSelect.value || null;
99
+ applyCurrentFilter();
100
+ });
93
101
  viewpointSelect?.addEventListener("change", () => {
94
102
  const activeViewer = requireViewer();
95
103
  if (!viewpointSelect.value) {
@@ -233,6 +241,7 @@ export function createAtlasViewer(container, options) {
233
241
  return api;
234
242
  function refreshAfterInputChange() {
235
243
  populateViewpoints();
244
+ populateGroups();
236
245
  applyCurrentFilter();
237
246
  renderBookmarks();
238
247
  updateSearchResults();
@@ -249,7 +258,7 @@ export function createAtlasViewer(container, options) {
249
258
  query: searchQuery || undefined,
250
259
  objectTypes: objectTypeFilter ? [objectTypeFilter] : undefined,
251
260
  tags: baseFilter?.tags,
252
- groupIds: baseFilter?.groupIds,
261
+ groupIds: groupFilter ? [groupFilter] : baseFilter?.groupIds,
253
262
  includeAncestors: baseFilter?.includeAncestors ?? true,
254
263
  });
255
264
  }
@@ -257,12 +266,16 @@ export function createAtlasViewer(container, options) {
257
266
  searchQuery = filter?.query?.trim() ?? "";
258
267
  objectTypeFilter =
259
268
  filter?.objectTypes?.length === 1 ? filter.objectTypes[0] : null;
269
+ groupFilter = filter?.groupIds?.length === 1 ? filter.groupIds[0] : null;
260
270
  if (searchInput && document.activeElement !== searchInput) {
261
271
  searchInput.value = searchQuery;
262
272
  }
263
273
  if (typeFilterSelect) {
264
274
  typeFilterSelect.value = objectTypeFilter ?? "";
265
275
  }
276
+ if (groupFilterSelect) {
277
+ groupFilterSelect.value = groupFilter ?? "";
278
+ }
266
279
  }
267
280
  function populateViewpoints() {
268
281
  if (!viewpointSelect) {
@@ -278,6 +291,17 @@ export function createAtlasViewer(container, options) {
278
291
  ].join("");
279
292
  viewpointSelect.value = active;
280
293
  }
294
+ function populateGroups() {
295
+ if (!groupFilterSelect) {
296
+ return;
297
+ }
298
+ const activeViewer = requireViewer();
299
+ groupFilterSelect.innerHTML = [
300
+ `<option value="">All groups</option>`,
301
+ ...activeViewer.getScene().semanticGroups.map((group) => `<option value="${escapeHtml(group.id)}">${escapeHtml(group.label)}</option>`),
302
+ ].join("");
303
+ groupFilterSelect.value = groupFilter ?? "";
304
+ }
281
305
  function syncViewpointControl() {
282
306
  if (!viewpointSelect) {
283
307
  return;
@@ -313,6 +337,8 @@ export function createAtlasViewer(container, options) {
313
337
  projection: activeViewer.getScene().projection,
314
338
  renderPreset: activeViewer.getScene().renderPreset,
315
339
  groupCount: activeViewer.getScene().groups.length,
340
+ semanticGroupCount: activeViewer.getScene().semanticGroups.length,
341
+ relationCount: activeViewer.getScene().relations.length,
316
342
  viewpointCount: activeViewer.getScene().viewpoints.length,
317
343
  },
318
344
  };
@@ -351,6 +377,14 @@ function buildAtlasViewerMarkup(controls) {
351
377
  </select>
352
378
  </label>`
353
379
  : "",
380
+ controls.groupFilter
381
+ ? `<label class="wo-atlas-field">
382
+ <span>Group</span>
383
+ <select data-atlas-group-filter>
384
+ <option value="">All groups</option>
385
+ </select>
386
+ </label>`
387
+ : "",
354
388
  controls.viewpointSelect
355
389
  ? `<label class="wo-atlas-field">
356
390
  <span>Viewpoint</span>