worldorbit 2.5.17 → 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.
@@ -64,7 +64,11 @@ const OBJECT_NUMBER_FIELDS = ["albedo"];
64
64
  const FIELD_HELP = {
65
65
  "defaults-view": {
66
66
  description: "Sets the default camera projection for the atlas.",
67
- references: ["Topdown = map-like", "Isometric = angled overview"],
67
+ references: [
68
+ "Topdown = map-like",
69
+ "Isometric = angled overview",
70
+ "Orthographic/Perspective = 3D-ready semantic views",
71
+ ],
68
72
  },
69
73
  "defaults-scale": {
70
74
  description: "Chooses the overall spacing/style preset used by the renderer.",
@@ -76,15 +80,35 @@ const FIELD_HELP = {
76
80
  },
77
81
  "viewpoint-projection": {
78
82
  description: "Overrides the projection for this saved viewpoint.",
79
- references: ["Topdown = flat orbital map", "Isometric = angled scene"],
83
+ references: [
84
+ "Topdown = flat orbital map",
85
+ "Isometric = angled scene",
86
+ "Orthographic/Perspective = stored with current 2D fallback",
87
+ ],
80
88
  },
81
89
  "viewpoint-zoom": {
82
90
  description: "Controls how closely this viewpoint frames the system.",
83
91
  references: ["1 = scene fit", "2+ = close-up"],
84
92
  },
85
93
  "viewpoint-rotation": {
86
- description: "Rotates the saved camera angle in degrees.",
87
- references: ["90deg = quarter turn", "180deg = flip"],
94
+ description: "Legacy 2D screen rotation. This is separate from the Schema 2.5 camera block.",
95
+ references: ["90deg = quarter turn", "Use camera.azimuth for semantic view direction"],
96
+ },
97
+ "viewpoint-camera-azimuth": {
98
+ description: "Horizontal camera direction in degrees for Schema 2.5 viewpoints.",
99
+ references: ["0 = forward/default", "90 = quarter orbit around the scene"],
100
+ },
101
+ "viewpoint-camera-elevation": {
102
+ description: "Vertical camera tilt in degrees for 3D-ready viewpoints.",
103
+ references: ["0 = level", "30 = gentle look down"],
104
+ },
105
+ "viewpoint-camera-roll": {
106
+ description: "Rolls the camera around its forward axis.",
107
+ references: ["0 = upright", "15 = slight bank"],
108
+ },
109
+ "viewpoint-camera-distance": {
110
+ description: "Semantic camera distance for perspective viewpoints.",
111
+ references: ["4 = close", "12 = wide framing"],
88
112
  },
89
113
  "viewpoint-events": {
90
114
  description: "Lists event IDs that this viewpoint should feature in its detail panel.",
@@ -110,6 +134,14 @@ const FIELD_HELP = {
110
134
  description: "Notes where or how the event is visible.",
111
135
  references: ['"Visible from Naar"', '"Southern hemisphere only"'],
112
136
  },
137
+ "event-epoch": {
138
+ description: "Optional event-wide epoch that event poses inherit unless they override it.",
139
+ references: ['"JY-0001.0"', '"Naar bloom cycle year 18"'],
140
+ },
141
+ "event-referencePlane": {
142
+ description: "Optional event-wide reference plane for all poses in this snapshot.",
143
+ references: ["ecliptic", "naar-equatorial"],
144
+ },
113
145
  "event-viewpoints": {
114
146
  description: "Viewpoint IDs that should list this event prominently.",
115
147
  references: ["naar-system", "overview inner-system"],
@@ -150,6 +182,14 @@ const FIELD_HELP = {
150
182
  description: "Starting position of the object along its orbit.",
151
183
  references: ["0deg = start position", "180deg = opposite side"],
152
184
  },
185
+ "pose-epoch": {
186
+ description: "Overrides the effective epoch for this pose only.",
187
+ references: ['"JY-0001.0"', "Falls back to event, object, then system"],
188
+ },
189
+ "pose-referencePlane": {
190
+ description: "Overrides the effective reference plane for this pose only.",
191
+ references: ["naar-equatorial", "Falls back to event, object, then system"],
192
+ },
153
193
  "prop-radius": {
154
194
  description: "Visual body size or real-world-inspired radius value.",
155
195
  references: ["1re = Earth radius", "1sol = Sun radius"],
@@ -348,6 +388,8 @@ export function createWorldOrbitEditor(container, options = {}) {
348
388
  participantObjectIds: [],
349
389
  timing: null,
350
390
  visibility: null,
391
+ epoch: null,
392
+ referencePlane: null,
351
393
  tags: [],
352
394
  color: null,
353
395
  hidden: false,
@@ -372,6 +414,7 @@ export function createWorldOrbitEditor(container, options = {}) {
372
414
  preset: atlasDocument.system?.defaults.preset ?? null,
373
415
  zoom: null,
374
416
  rotationDeg: 0,
417
+ camera: null,
375
418
  layers: {},
376
419
  filter: null,
377
420
  };
@@ -1335,6 +1378,7 @@ export function createWorldOrbitEditor(container, options = {}) {
1335
1378
  null,
1336
1379
  zoom: parseNullableNumber(readOptionalTextInput(form, "viewpoint-zoom")),
1337
1380
  rotationDeg: parseNullableNumber(readOptionalTextInput(form, "viewpoint-rotation")) ?? 0,
1381
+ camera: buildViewCameraFromForm(form),
1338
1382
  layers: {
1339
1383
  background: readCheckbox(form, "layer-background"),
1340
1384
  guides: readCheckbox(form, "layer-guides"),
@@ -1380,6 +1424,8 @@ export function createWorldOrbitEditor(container, options = {}) {
1380
1424
  participantObjectIds: splitTokens(readOptionalTextInput(form, "event-participants")),
1381
1425
  timing: readOptionalTextInput(form, "event-timing"),
1382
1426
  visibility: readOptionalTextInput(form, "event-visibility"),
1427
+ epoch: readOptionalTextInput(form, "event-epoch"),
1428
+ referencePlane: readOptionalTextInput(form, "event-referencePlane"),
1383
1429
  tags: splitTokens(readOptionalTextInput(form, "event-tags")),
1384
1430
  color: readOptionalTextInput(form, "event-color"),
1385
1431
  hidden: readCheckbox(form, "event-hidden"),
@@ -1406,6 +1452,8 @@ export function createWorldOrbitEditor(container, options = {}) {
1406
1452
  const replacement = {
1407
1453
  objectId: nextObjectId,
1408
1454
  placement: buildPlacementFromPoseForm(form, currentPose),
1455
+ epoch: readOptionalTextInput(form, "pose-epoch"),
1456
+ referencePlane: readOptionalTextInput(form, "pose-referencePlane"),
1409
1457
  };
1410
1458
  const inner = parseOptionalUnit(readOptionalTextInput(form, "prop-inner"));
1411
1459
  const outer = parseOptionalUnit(readOptionalTextInput(form, "prop-outer"));
@@ -1806,6 +1854,8 @@ function renderDefaultsInspector(formState) {
1806
1854
  ${renderInspectorSection("defaults", "basics", "Basics", `${renderSelectField("Projection", "defaults-view", [
1807
1855
  ["topdown", "Topdown"],
1808
1856
  ["isometric", "Isometric"],
1857
+ ["orthographic", "Orthographic"],
1858
+ ["perspective", "Perspective"],
1809
1859
  ], defaults?.view ?? "topdown")}
1810
1860
  ${renderTextField("Scale preset", "defaults-scale", defaults?.scale ?? "")}
1811
1861
  ${renderTextField("Units", "defaults-units", defaults?.units ?? "")}
@@ -1842,6 +1892,8 @@ function renderViewpointInspector(formState, id) {
1842
1892
  ${renderSelectField("Projection", "viewpoint-projection", [
1843
1893
  ["topdown", "Topdown"],
1844
1894
  ["isometric", "Isometric"],
1895
+ ["orthographic", "Orthographic"],
1896
+ ["perspective", "Perspective"],
1845
1897
  ], viewpoint.projection)}
1846
1898
  ${renderSelectField("Preset", "viewpoint-preset", [
1847
1899
  ["", "Document default"],
@@ -1852,6 +1904,11 @@ function renderViewpointInspector(formState, id) {
1852
1904
  ], viewpoint.preset ?? "")}
1853
1905
  ${renderTextField("Zoom", "viewpoint-zoom", viewpoint.zoom === null ? "" : String(viewpoint.zoom))}
1854
1906
  ${renderTextField("Rotation", "viewpoint-rotation", String(viewpoint.rotationDeg))}`, true)}
1907
+ ${renderInspectorSection("viewpoint", "camera", "Camera", `${renderTextField("Azimuth", "viewpoint-camera-azimuth", viewpoint.camera?.azimuth === null || viewpoint.camera?.azimuth === undefined ? "" : String(viewpoint.camera.azimuth))}
1908
+ ${renderTextField("Elevation", "viewpoint-camera-elevation", viewpoint.camera?.elevation === null || viewpoint.camera?.elevation === undefined ? "" : String(viewpoint.camera.elevation))}
1909
+ ${renderTextField("Roll", "viewpoint-camera-roll", viewpoint.camera?.roll === null || viewpoint.camera?.roll === undefined ? "" : String(viewpoint.camera.roll))}
1910
+ ${renderTextField("Distance", "viewpoint-camera-distance", viewpoint.camera?.distance === null || viewpoint.camera?.distance === undefined ? "" : String(viewpoint.camera.distance))}
1911
+ <p class="wo-editor-inline-note">Rotation stays a 2D screen-rotation hint. The camera block stores Schema 2.5 view direction and framing.</p>`)}
1855
1912
  ${renderInspectorSection("viewpoint", "layers", "Layers", `<fieldset class="wo-editor-fieldset">
1856
1913
  <legend>Layers</legend>
1857
1914
  ${renderCheckboxField("Background", "layer-background", viewpoint.layers.background !== false)}
@@ -1889,6 +1946,8 @@ function renderEventInspector(formState, id) {
1889
1946
  ${renderTextField("Participants", "event-participants", eventEntry.participantObjectIds.join(" "))}
1890
1947
  ${renderTextField("Timing", "event-timing", eventEntry.timing ?? "")}
1891
1948
  ${renderTextField("Visibility", "event-visibility", eventEntry.visibility ?? "")}
1949
+ ${renderTextField("Epoch", "event-epoch", eventEntry.epoch ?? "")}
1950
+ ${renderTextField("Reference plane", "event-referencePlane", eventEntry.referencePlane ?? "")}
1892
1951
  ${renderTextField("Tags", "event-tags", eventEntry.tags.join(" "))}
1893
1952
  ${renderTextField("Color", "event-color", eventEntry.color ?? "")}
1894
1953
  ${renderCheckboxField("Hidden", "event-hidden", eventEntry.hidden === true)}`, true)}
@@ -1943,6 +2002,9 @@ function renderEventPoseInspector(formState, eventId, objectId) {
1943
2002
  ${renderTextField("Phase", "placement-phase", pose.placement?.mode === "orbit" && pose.placement.phase ? formatUnitValue(pose.placement.phase) : "")}
1944
2003
  ${renderTextField("Inner", "prop-inner", pose.inner ? formatUnitValue(pose.inner) : "")}
1945
2004
  ${renderTextField("Outer", "prop-outer", pose.outer ? formatUnitValue(pose.outer) : "")}`, true)}
2005
+ ${renderInspectorSection("event-pose", "context", "Context", `${renderTextField("Epoch", "pose-epoch", pose.epoch ?? "")}
2006
+ ${renderTextField("Reference plane", "pose-referencePlane", pose.referencePlane ?? "")}
2007
+ <p class="wo-editor-inline-note">Falls back to event, then object, then system context when left empty.</p>`)}
1946
2008
  </form>`;
1947
2009
  }
1948
2010
  function renderAnnotationInspector(formState, id) {
@@ -2138,6 +2200,20 @@ function parseNullableNumber(value) {
2138
2200
  const parsed = Number(value);
2139
2201
  return Number.isFinite(parsed) ? parsed : null;
2140
2202
  }
2203
+ function buildViewCameraFromForm(form) {
2204
+ const camera = {
2205
+ azimuth: parseNullableNumber(readOptionalTextInput(form, "viewpoint-camera-azimuth")),
2206
+ elevation: parseNullableNumber(readOptionalTextInput(form, "viewpoint-camera-elevation")),
2207
+ roll: parseNullableNumber(readOptionalTextInput(form, "viewpoint-camera-roll")),
2208
+ distance: parseNullableNumber(readOptionalTextInput(form, "viewpoint-camera-distance")),
2209
+ };
2210
+ return camera.azimuth !== null ||
2211
+ camera.elevation !== null ||
2212
+ camera.roll !== null ||
2213
+ camera.distance !== null
2214
+ ? camera
2215
+ : null;
2216
+ }
2141
2217
  function parseObjectTypes(value) {
2142
2218
  const tokens = splitTokens(value);
2143
2219
  return tokens.filter((token) => OBJECT_TYPES.includes(token));
@@ -2746,6 +2822,21 @@ function mapDiagnosticFieldToInputNames(selection, field) {
2746
2822
  return ["viewpoint-zoom"];
2747
2823
  case "rotationDeg":
2748
2824
  return ["viewpoint-rotation"];
2825
+ case "camera":
2826
+ return [
2827
+ "viewpoint-camera-azimuth",
2828
+ "viewpoint-camera-elevation",
2829
+ "viewpoint-camera-roll",
2830
+ "viewpoint-camera-distance",
2831
+ ];
2832
+ case "camera.azimuth":
2833
+ return ["viewpoint-camera-azimuth"];
2834
+ case "camera.elevation":
2835
+ return ["viewpoint-camera-elevation"];
2836
+ case "camera.roll":
2837
+ return ["viewpoint-camera-roll"];
2838
+ case "camera.distance":
2839
+ return ["viewpoint-camera-distance"];
2749
2840
  case "events":
2750
2841
  return ["viewpoint-events"];
2751
2842
  default:
@@ -2771,6 +2862,10 @@ function mapDiagnosticFieldToInputNames(selection, field) {
2771
2862
  return ["event-timing"];
2772
2863
  case "visibility":
2773
2864
  return ["event-visibility"];
2865
+ case "epoch":
2866
+ return ["event-epoch"];
2867
+ case "referencePlane":
2868
+ return ["event-referencePlane"];
2774
2869
  case "tags":
2775
2870
  return ["event-tags"];
2776
2871
  case "color":
@@ -2799,6 +2894,12 @@ function mapDiagnosticFieldToInputNames(selection, field) {
2799
2894
  if (field === "inner" || field === "outer") {
2800
2895
  return [`prop-${field}`];
2801
2896
  }
2897
+ if (field === "epoch") {
2898
+ return ["pose-epoch"];
2899
+ }
2900
+ if (field === "referencePlane") {
2901
+ return ["pose-referencePlane"];
2902
+ }
2802
2903
  return [];
2803
2904
  case "annotation":
2804
2905
  switch (field) {
@@ -62,13 +62,14 @@ export function searchSceneObjects(scene, query, limit = 12) {
62
62
  }
63
63
  export function createAtlasStateSnapshot(viewerState, renderOptions, filter, viewpointId) {
64
64
  return {
65
- version: "2.0",
65
+ version: "2.5",
66
66
  viewpointId,
67
67
  activeEventId: renderOptions.activeEventId ?? null,
68
68
  viewerState: { ...viewerState },
69
69
  renderOptions: {
70
70
  preset: renderOptions.preset,
71
71
  projection: renderOptions.projection,
72
+ camera: renderOptions.camera ? { ...renderOptions.camera } : null,
72
73
  layers: renderOptions.layers ? { ...renderOptions.layers } : undefined,
73
74
  scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : undefined,
74
75
  activeEventId: renderOptions.activeEventId ?? null,
@@ -82,7 +83,7 @@ export function serializeViewerAtlasState(state) {
82
83
  export function deserializeViewerAtlasState(serialized) {
83
84
  const raw = JSON.parse(decodeURIComponent(serialized));
84
85
  return {
85
- version: "2.0",
86
+ version: raw.version === "2.0" ? "2.0" : "2.5",
86
87
  viewpointId: raw.viewpointId ?? null,
87
88
  activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null,
88
89
  viewerState: {
@@ -95,6 +96,7 @@ export function deserializeViewerAtlasState(serialized) {
95
96
  renderOptions: {
96
97
  preset: raw.renderOptions?.preset,
97
98
  projection: raw.renderOptions?.projection,
99
+ camera: raw.renderOptions?.camera ? { ...raw.renderOptions.camera } : null,
98
100
  layers: raw.renderOptions?.layers ? { ...raw.renderOptions.layers } : undefined,
99
101
  scaleModel: raw.renderOptions?.scaleModel
100
102
  ? { ...raw.renderOptions.scaleModel }
@@ -117,6 +119,7 @@ export function createViewerBookmark(name, label, atlasState) {
117
119
  viewerState: { ...atlasState.viewerState },
118
120
  renderOptions: {
119
121
  ...atlasState.renderOptions,
122
+ camera: atlasState.renderOptions.camera ? { ...atlasState.renderOptions.camera } : null,
120
123
  layers: atlasState.renderOptions.layers ? { ...atlasState.renderOptions.layers } : undefined,
121
124
  scaleModel: atlasState.renderOptions.scaleModel
122
125
  ? { ...atlasState.renderOptions.scaleModel }
@@ -326,6 +326,8 @@ export function createAtlasViewer(container, options) {
326
326
  }
327
327
  function buildInspectorSnapshot() {
328
328
  const activeViewer = requireViewer();
329
+ const scene = activeViewer.getScene();
330
+ const camera = scene.camera;
329
331
  return {
330
332
  selection: activeViewer.getSelectionDetails(),
331
333
  activeViewpoint: activeViewer.getActiveViewpoint(),
@@ -333,14 +335,23 @@ export function createAtlasViewer(container, options) {
333
335
  atlasState: activeViewer.getAtlasState(),
334
336
  visibleObjectIds: activeViewer.getVisibleObjects().map((object) => object.objectId),
335
337
  scene: {
336
- title: activeViewer.getScene().title,
337
- projection: activeViewer.getScene().projection,
338
- renderPreset: activeViewer.getScene().renderPreset,
339
- groupCount: activeViewer.getScene().groups.length,
340
- semanticGroupCount: activeViewer.getScene().semanticGroups.length,
341
- relationCount: activeViewer.getScene().relations.length,
342
- eventCount: activeViewer.getScene().events.length,
343
- viewpointCount: activeViewer.getScene().viewpoints.length,
338
+ title: scene.title,
339
+ projection: scene.projection,
340
+ renderProjection: scene.renderProjection,
341
+ camera: camera
342
+ ? {
343
+ azimuth: camera.azimuth,
344
+ elevation: camera.elevation,
345
+ roll: camera.roll,
346
+ distance: camera.distance,
347
+ }
348
+ : null,
349
+ renderPreset: scene.renderPreset,
350
+ groupCount: scene.groups.length,
351
+ semanticGroupCount: scene.semanticGroups.length,
352
+ relationCount: scene.relations.length,
353
+ eventCount: scene.events.length,
354
+ viewpointCount: scene.viewpoints.length,
344
355
  },
345
356
  };
346
357
  }
@@ -1,4 +1,4 @@
1
- import type { CoordinatePoint, RenderOrbitVisual, RenderPresetName, RenderSceneEvent, RenderSceneGroup, RenderSceneLabel, RenderScaleModel, RenderScene, RenderSceneObject, RenderSceneViewpoint, SceneRenderOptions, ViewProjection, WorldOrbitObject, WorldOrbitDocument } from "@worldorbit/core";
1
+ import type { CoordinatePoint, RenderOrbitVisual, RenderProjectionFallback, RenderPresetName, RenderSceneEvent, RenderSceneGroup, RenderSceneLabel, RenderScaleModel, RenderScene, RenderSceneObject, RenderSceneViewpoint, SceneRenderOptions, ViewProjection, WorldOrbitObject, WorldOrbitDocument, WorldOrbitViewCamera } from "@worldorbit/core";
2
2
  export type WorldOrbitThemeName = "atlas" | "nightglass" | "ember";
3
3
  export type WorldOrbitEmbedMode = "static" | "interactive";
4
4
  export type TooltipMode = "hover" | "pinned" | "disabled";
@@ -104,13 +104,14 @@ export interface ViewerTooltipDetails {
104
104
  details: ViewerObjectDetails;
105
105
  }
106
106
  export interface ViewerAtlasState {
107
- version: "2.0";
107
+ version: "2.0" | "2.5";
108
108
  viewpointId: string | null;
109
109
  activeEventId?: string | null;
110
110
  viewerState: ViewerState;
111
111
  renderOptions: {
112
112
  preset?: RenderPresetName;
113
113
  projection?: "document" | ViewProjection;
114
+ camera?: WorldOrbitViewCamera | null;
114
115
  layers?: ViewerLayerOptions;
115
116
  scaleModel?: Partial<RenderScaleModel>;
116
117
  activeEventId?: string | null;
@@ -139,6 +140,8 @@ export interface AtlasInspectorSnapshot {
139
140
  scene: {
140
141
  title: string;
141
142
  projection: ViewProjection;
143
+ renderProjection: RenderProjectionFallback;
144
+ camera: WorldOrbitViewCamera | null;
142
145
  renderPreset: RenderPresetName | null;
143
146
  groupCount: number;
144
147
  semanticGroupCount: number;
@@ -43,6 +43,7 @@ export function createInteractiveViewer(container, options) {
43
43
  padding: options.padding,
44
44
  preset: options.preset,
45
45
  projection: options.projection,
46
+ camera: options.camera ? { ...options.camera } : null,
46
47
  scaleModel: options.scaleModel ? { ...options.scaleModel } : undefined,
47
48
  theme: options.theme,
48
49
  layers: options.layers,
@@ -333,6 +334,12 @@ export function createInteractiveViewer(container, options) {
333
334
  if (currentInput.kind !== "scene" && viewpoint.projection !== scene.projection) {
334
335
  nextRenderOptions.projection = viewpoint.projection;
335
336
  }
337
+ if (viewpoint.camera) {
338
+ nextRenderOptions.camera = { ...viewpoint.camera };
339
+ }
340
+ else if (renderOptions.camera) {
341
+ nextRenderOptions.camera = null;
342
+ }
336
343
  if (viewpointLayers) {
337
344
  nextRenderOptions.layers = viewpointLayers;
338
345
  }
@@ -992,6 +999,7 @@ function renderSceneFromInput(input, renderOptions) {
992
999
  function cloneRenderOptions(renderOptions) {
993
1000
  return {
994
1001
  ...renderOptions,
1002
+ camera: renderOptions.camera ? { ...renderOptions.camera } : null,
995
1003
  filter: renderOptions.filter ? { ...renderOptions.filter } : undefined,
996
1004
  scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : undefined,
997
1005
  layers: renderOptions.layers ? { ...renderOptions.layers } : undefined,
@@ -1005,6 +1013,13 @@ function mergeRenderOptions(current, next) {
1005
1013
  return {
1006
1014
  ...current,
1007
1015
  ...next,
1016
+ camera: next.camera !== undefined
1017
+ ? next.camera
1018
+ ? { ...next.camera }
1019
+ : null
1020
+ : current.camera
1021
+ ? { ...current.camera }
1022
+ : null,
1008
1023
  filter: next.filter !== undefined
1009
1024
  ? normalizeViewerFilter(next.filter)
1010
1025
  : current.filter
@@ -1037,6 +1052,7 @@ function hasSceneAffectingRenderOptions(options) {
1037
1052
  options.padding !== undefined ||
1038
1053
  options.preset !== undefined ||
1039
1054
  options.projection !== undefined ||
1055
+ options.camera !== undefined ||
1040
1056
  options.scaleModel !== undefined ||
1041
1057
  options.activeEventId !== undefined);
1042
1058
  }