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
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { collectAtlasDiagnostics } from "./atlas-validate.js";
|
|
2
|
-
export function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.
|
|
2
|
+
export function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.5") {
|
|
3
3
|
return {
|
|
4
4
|
format: "worldorbit",
|
|
5
5
|
version,
|
|
@@ -25,6 +25,7 @@ export function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.0
|
|
|
25
25
|
},
|
|
26
26
|
groups: [],
|
|
27
27
|
relations: [],
|
|
28
|
+
events: [],
|
|
28
29
|
objects: [],
|
|
29
30
|
diagnostics: [],
|
|
30
31
|
};
|
|
@@ -51,6 +52,12 @@ export function listAtlasDocumentPaths(document) {
|
|
|
51
52
|
for (const relation of [...document.relations].sort(compareIdLike)) {
|
|
52
53
|
paths.push({ kind: "relation", id: relation.id });
|
|
53
54
|
}
|
|
55
|
+
for (const event of [...document.events].sort(compareIdLike)) {
|
|
56
|
+
paths.push({ kind: "event", id: event.id });
|
|
57
|
+
for (const pose of [...event.positions].sort(comparePoseObjectId)) {
|
|
58
|
+
paths.push({ kind: "event-pose", id: event.id, key: pose.objectId });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
54
61
|
for (const object of [...document.objects].sort(compareIdLike)) {
|
|
55
62
|
paths.push({ kind: "object", id: object.id });
|
|
56
63
|
}
|
|
@@ -66,6 +73,10 @@ export function getAtlasDocumentNode(document, path) {
|
|
|
66
73
|
return path.key ? (document.system?.atlasMetadata[path.key] ?? null) : null;
|
|
67
74
|
case "group":
|
|
68
75
|
return path.id ? findGroup(document, path.id) : null;
|
|
76
|
+
case "event":
|
|
77
|
+
return path.id ? findEvent(document, path.id) : null;
|
|
78
|
+
case "event-pose":
|
|
79
|
+
return path.id && path.key ? findEventPose(document, path.id, path.key) : null;
|
|
69
80
|
case "object":
|
|
70
81
|
return path.id ? findObject(document, path.id) : null;
|
|
71
82
|
case "viewpoint":
|
|
@@ -106,6 +117,18 @@ export function upsertAtlasDocumentNode(document, path, value) {
|
|
|
106
117
|
}
|
|
107
118
|
upsertById(next.groups, value);
|
|
108
119
|
return next;
|
|
120
|
+
case "event":
|
|
121
|
+
if (!path.id) {
|
|
122
|
+
throw new Error('Event updates require an "id" value.');
|
|
123
|
+
}
|
|
124
|
+
upsertById(next.events, value);
|
|
125
|
+
return next;
|
|
126
|
+
case "event-pose":
|
|
127
|
+
if (!path.id || !path.key) {
|
|
128
|
+
throw new Error('Event pose updates require an event "id" and pose "key" value.');
|
|
129
|
+
}
|
|
130
|
+
upsertEventPose(next.events, path.id, value);
|
|
131
|
+
return next;
|
|
109
132
|
case "object":
|
|
110
133
|
if (!path.id) {
|
|
111
134
|
throw new Error('Object updates require an "id" value.');
|
|
@@ -154,6 +177,19 @@ export function removeAtlasDocumentNode(document, path) {
|
|
|
154
177
|
next.groups = next.groups.filter((group) => group.id !== path.id);
|
|
155
178
|
}
|
|
156
179
|
return next;
|
|
180
|
+
case "event":
|
|
181
|
+
if (path.id) {
|
|
182
|
+
next.events = next.events.filter((event) => event.id !== path.id);
|
|
183
|
+
}
|
|
184
|
+
return next;
|
|
185
|
+
case "event-pose":
|
|
186
|
+
if (path.id && path.key) {
|
|
187
|
+
const event = findEvent(next, path.id);
|
|
188
|
+
if (event) {
|
|
189
|
+
event.positions = event.positions.filter((pose) => pose.objectId !== path.key);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return next;
|
|
157
193
|
case "viewpoint":
|
|
158
194
|
if (path.id) {
|
|
159
195
|
system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
|
|
@@ -222,6 +258,22 @@ export function resolveAtlasDiagnosticPath(document, diagnostic) {
|
|
|
222
258
|
};
|
|
223
259
|
}
|
|
224
260
|
}
|
|
261
|
+
if (diagnostic.field?.startsWith("event.")) {
|
|
262
|
+
const parts = diagnostic.field.split(".");
|
|
263
|
+
if (parts[1] && findEvent(document, parts[1])) {
|
|
264
|
+
if (parts[2] === "pose" && parts[3] && findEventPose(document, parts[1], parts[3])) {
|
|
265
|
+
return {
|
|
266
|
+
kind: "event-pose",
|
|
267
|
+
id: parts[1],
|
|
268
|
+
key: parts[3],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
kind: "event",
|
|
273
|
+
id: parts[1],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
225
277
|
if (diagnostic.field && diagnostic.field in ensureSystem(document).atlasMetadata) {
|
|
226
278
|
return {
|
|
227
279
|
kind: "metadata",
|
|
@@ -253,6 +305,12 @@ function findGroup(document, groupId) {
|
|
|
253
305
|
function findRelation(document, relationId) {
|
|
254
306
|
return document.relations.find((relation) => relation.id === relationId) ?? null;
|
|
255
307
|
}
|
|
308
|
+
function findEvent(document, eventId) {
|
|
309
|
+
return document.events.find((event) => event.id === eventId) ?? null;
|
|
310
|
+
}
|
|
311
|
+
function findEventPose(document, eventId, objectId) {
|
|
312
|
+
return findEvent(document, eventId)?.positions.find((pose) => pose.objectId === objectId) ?? null;
|
|
313
|
+
}
|
|
256
314
|
function findViewpoint(system, viewpointId) {
|
|
257
315
|
return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
|
|
258
316
|
}
|
|
@@ -268,6 +326,22 @@ function upsertById(items, value) {
|
|
|
268
326
|
}
|
|
269
327
|
items[index] = value;
|
|
270
328
|
}
|
|
329
|
+
function upsertEventPose(events, eventId, value) {
|
|
330
|
+
const event = events.find((entry) => entry.id === eventId);
|
|
331
|
+
if (!event) {
|
|
332
|
+
throw new Error(`Unknown event "${eventId}" for pose update.`);
|
|
333
|
+
}
|
|
334
|
+
const index = event.positions.findIndex((entry) => entry.objectId === value.objectId);
|
|
335
|
+
if (index === -1) {
|
|
336
|
+
event.positions.push(value);
|
|
337
|
+
event.positions.sort(comparePoseObjectId);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
event.positions[index] = value;
|
|
341
|
+
}
|
|
271
342
|
function compareIdLike(left, right) {
|
|
272
343
|
return left.id.localeCompare(right.id);
|
|
273
344
|
}
|
|
345
|
+
function comparePoseObjectId(left, right) {
|
|
346
|
+
return left.objectId.localeCompare(right.objectId);
|
|
347
|
+
}
|
|
@@ -11,6 +11,7 @@ export function collectAtlasDiagnostics(document, sourceSchemaVersion) {
|
|
|
11
11
|
const diagnostics = [];
|
|
12
12
|
const objectMap = new Map(document.objects.map((object) => [object.id, object]));
|
|
13
13
|
const groupIds = new Set(document.groups.map((group) => group.id));
|
|
14
|
+
const eventIds = new Set(document.events.map((event) => event.id));
|
|
14
15
|
if (!document.system) {
|
|
15
16
|
diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
|
|
16
17
|
}
|
|
@@ -20,6 +21,7 @@ export function collectAtlasDiagnostics(document, sourceSchemaVersion) {
|
|
|
20
21
|
["viewpoint", document.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
|
|
21
22
|
["annotation", document.system?.annotations.map((annotation) => annotation.id) ?? []],
|
|
22
23
|
["relation", document.relations.map((relation) => relation.id)],
|
|
24
|
+
["event", document.events.map((event) => event.id)],
|
|
23
25
|
["object", document.objects.map((object) => object.id)],
|
|
24
26
|
]) {
|
|
25
27
|
for (const id of ids) {
|
|
@@ -36,11 +38,14 @@ export function collectAtlasDiagnostics(document, sourceSchemaVersion) {
|
|
|
36
38
|
validateRelation(relation, objectMap, diagnostics);
|
|
37
39
|
}
|
|
38
40
|
for (const viewpoint of document.system?.viewpoints ?? []) {
|
|
39
|
-
|
|
41
|
+
validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap);
|
|
40
42
|
}
|
|
41
43
|
for (const object of document.objects) {
|
|
42
44
|
validateObject(object, document.system, objectMap, groupIds, diagnostics);
|
|
43
45
|
}
|
|
46
|
+
for (const event of document.events) {
|
|
47
|
+
validateEvent(event, document.system, objectMap, diagnostics);
|
|
48
|
+
}
|
|
44
49
|
return diagnostics;
|
|
45
50
|
}
|
|
46
51
|
function validateRelation(relation, objectMap, diagnostics) {
|
|
@@ -60,15 +65,24 @@ function validateRelation(relation, objectMap, diagnostics) {
|
|
|
60
65
|
diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
function validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap) {
|
|
69
|
+
const filter = viewpoint.filter;
|
|
70
|
+
if (sourceSchemaVersion === "2.1" || sourceSchemaVersion === "2.5") {
|
|
71
|
+
if (filter) {
|
|
72
|
+
for (const groupId of filter.groupIds) {
|
|
73
|
+
if (!groupIds.has(groupId)) {
|
|
74
|
+
diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpoint.id}".`, undefined, `viewpoint.${viewpoint.id}.groups`));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const eventId of viewpoint.events ?? []) {
|
|
79
|
+
if (!eventIds.has(eventId)) {
|
|
80
|
+
diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpoint.id}".`, undefined, `viewpoint.${viewpoint.id}.events`));
|
|
81
|
+
}
|
|
70
82
|
}
|
|
71
83
|
}
|
|
84
|
+
validateProjection(viewpoint.projection, diagnostics, `viewpoint.${viewpoint.id}.projection`, viewpoint.id);
|
|
85
|
+
validateCamera(viewpoint.camera, viewpoint.projection, viewpoint.rotationDeg, diagnostics, viewpoint.id, viewpoint.focusObjectId, viewpoint.selectedObjectId, filter, objectMap);
|
|
72
86
|
}
|
|
73
87
|
function validateObject(object, system, objectMap, groupIds, diagnostics) {
|
|
74
88
|
const placement = object.placement;
|
|
@@ -81,6 +95,12 @@ function validateObject(object, system, objectMap, groupIds, diagnostics) {
|
|
|
81
95
|
}
|
|
82
96
|
}
|
|
83
97
|
}
|
|
98
|
+
if (typeof object.epoch === "string" && !object.epoch.trim()) {
|
|
99
|
+
diagnostics.push(warn("validate.epoch.empty", `Object "${object.id}" defines an empty epoch string.`, object.id, "epoch"));
|
|
100
|
+
}
|
|
101
|
+
if (typeof object.referencePlane === "string" && !object.referencePlane.trim()) {
|
|
102
|
+
diagnostics.push(warn("validate.referencePlane.empty", `Object "${object.id}" defines an empty reference plane string.`, object.id, "referencePlane"));
|
|
103
|
+
}
|
|
84
104
|
if (orbitPlacement) {
|
|
85
105
|
if (!objectMap.has(orbitPlacement.target)) {
|
|
86
106
|
diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
|
|
@@ -156,6 +176,128 @@ function validateObject(object, system, objectMap, groupIds, diagnostics) {
|
|
|
156
176
|
}
|
|
157
177
|
}
|
|
158
178
|
}
|
|
179
|
+
function validateEvent(event, system, objectMap, diagnostics) {
|
|
180
|
+
const fieldPrefix = `event.${event.id}`;
|
|
181
|
+
const referencedIds = new Set();
|
|
182
|
+
if (!event.kind.trim()) {
|
|
183
|
+
diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, undefined, `${fieldPrefix}.kind`));
|
|
184
|
+
}
|
|
185
|
+
if (typeof event.epoch === "string" && !event.epoch.trim()) {
|
|
186
|
+
diagnostics.push(warn("validate.event.epoch.empty", `Event "${event.id}" defines an empty epoch string.`, undefined, `${fieldPrefix}.epoch`));
|
|
187
|
+
}
|
|
188
|
+
if (typeof event.referencePlane === "string" && !event.referencePlane.trim()) {
|
|
189
|
+
diagnostics.push(warn("validate.event.referencePlane.empty", `Event "${event.id}" defines an empty reference plane string.`, undefined, `${fieldPrefix}.referencePlane`));
|
|
190
|
+
}
|
|
191
|
+
if (!event.targetObjectId && event.participantObjectIds.length === 0) {
|
|
192
|
+
diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, undefined, `${fieldPrefix}.participants`));
|
|
193
|
+
}
|
|
194
|
+
if (event.targetObjectId) {
|
|
195
|
+
referencedIds.add(event.targetObjectId);
|
|
196
|
+
if (!objectMap.has(event.targetObjectId)) {
|
|
197
|
+
diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, undefined, `${fieldPrefix}.target`));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const seenParticipants = new Set();
|
|
201
|
+
for (const participantId of event.participantObjectIds) {
|
|
202
|
+
referencedIds.add(participantId);
|
|
203
|
+
if (seenParticipants.has(participantId)) {
|
|
204
|
+
diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, undefined, `${fieldPrefix}.participants`));
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
seenParticipants.add(participantId);
|
|
208
|
+
if (!objectMap.has(participantId)) {
|
|
209
|
+
diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, undefined, `${fieldPrefix}.participants`));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (event.targetObjectId &&
|
|
213
|
+
event.participantObjectIds.length > 0 &&
|
|
214
|
+
!event.participantObjectIds.includes(event.targetObjectId)) {
|
|
215
|
+
diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, undefined, `${fieldPrefix}.target`));
|
|
216
|
+
}
|
|
217
|
+
if (event.positions.length === 0) {
|
|
218
|
+
diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, undefined, `${fieldPrefix}.positions`));
|
|
219
|
+
}
|
|
220
|
+
if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
|
|
221
|
+
diagnostics.push(warn("validate.event.kind.participants", `Event "${event.id}" looks like an eclipse or transit but references fewer than three bodies.`, undefined, `${fieldPrefix}.participants`));
|
|
222
|
+
}
|
|
223
|
+
const poseIds = new Set();
|
|
224
|
+
for (const pose of event.positions) {
|
|
225
|
+
const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
|
|
226
|
+
if (poseIds.has(pose.objectId)) {
|
|
227
|
+
diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, undefined, poseFieldPrefix));
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
poseIds.add(pose.objectId);
|
|
231
|
+
const object = objectMap.get(pose.objectId);
|
|
232
|
+
if (!object) {
|
|
233
|
+
diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, undefined, poseFieldPrefix));
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (!referencedIds.has(pose.objectId)) {
|
|
237
|
+
diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, undefined, poseFieldPrefix));
|
|
238
|
+
}
|
|
239
|
+
validateEventPose(pose, object, event, system, objectMap, diagnostics, poseFieldPrefix, event.id);
|
|
240
|
+
}
|
|
241
|
+
const missingPoseIds = [...referencedIds].filter((objectId) => !poseIds.has(objectId));
|
|
242
|
+
if (event.positions.length > 0 && missingPoseIds.length > 0) {
|
|
243
|
+
diagnostics.push(warn("validate.event.positions.partial", `Event "${event.id}" leaves ${missingPoseIds.length} referenced object(s) on their base placement.`, undefined, `${fieldPrefix}.positions`));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function validateEventPose(pose, object, event, system, objectMap, diagnostics, fieldPrefix, eventId) {
|
|
247
|
+
const placement = pose.placement;
|
|
248
|
+
if (!placement) {
|
|
249
|
+
diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, undefined, fieldPrefix));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (placement.mode === "orbit") {
|
|
253
|
+
if (!objectMap.has(placement.target)) {
|
|
254
|
+
diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, undefined, `${fieldPrefix}.orbit`));
|
|
255
|
+
}
|
|
256
|
+
if (placement.distance && placement.semiMajor) {
|
|
257
|
+
diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, undefined, `${fieldPrefix}.distance`));
|
|
258
|
+
}
|
|
259
|
+
if (placement.phase && !resolveEffectiveEpoch(system, object, event, pose)) {
|
|
260
|
+
diagnostics.push(warn("validate.event.pose.phase.epochMissing", `Event "${eventId}" pose "${pose.objectId}" sets "phase" without an effective epoch.`, undefined, `${fieldPrefix}.phase`));
|
|
261
|
+
}
|
|
262
|
+
if (placement.inclination && !resolveEffectiveReferencePlane(system, object, event, pose)) {
|
|
263
|
+
diagnostics.push(warn("validate.event.pose.inclination.referencePlaneMissing", `Event "${eventId}" pose "${pose.objectId}" sets "inclination" without an effective reference plane.`, undefined, `${fieldPrefix}.inclination`));
|
|
264
|
+
}
|
|
265
|
+
if (placement.period && !massInSolar(objectMap.get(placement.target)?.properties.mass)) {
|
|
266
|
+
diagnostics.push(warn("validate.event.pose.period.massMissing", `Event "${eventId}" pose "${pose.objectId}" sets "period" but its central mass cannot be derived.`, undefined, `${fieldPrefix}.period`));
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (placement.mode === "surface") {
|
|
271
|
+
const target = objectMap.get(placement.target);
|
|
272
|
+
if (!target) {
|
|
273
|
+
diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, undefined, `${fieldPrefix}.surface`));
|
|
274
|
+
}
|
|
275
|
+
else if (!SURFACE_TARGET_TYPES.has(target.type)) {
|
|
276
|
+
diagnostics.push(error("validate.event.pose.surface.target.invalid", `Event surface target "${placement.target}" on "${eventId}:${pose.objectId}" is not surface-capable.`, undefined, `${fieldPrefix}.surface`));
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (placement.mode === "at") {
|
|
281
|
+
if (object.type !== "structure" && object.type !== "phenomenon") {
|
|
282
|
+
diagnostics.push(error("validate.event.pose.at.objectType", `Only structures and phenomena may use "at" placement in events; found "${object.type}" on "${eventId}:${pose.objectId}".`, undefined, `${fieldPrefix}.at`));
|
|
283
|
+
}
|
|
284
|
+
const reference = placement.reference;
|
|
285
|
+
if (reference.kind === "named" && !objectMap.has(reference.name)) {
|
|
286
|
+
diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, undefined, `${fieldPrefix}.at`));
|
|
287
|
+
}
|
|
288
|
+
else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
|
|
289
|
+
diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, undefined, `${fieldPrefix}.at`));
|
|
290
|
+
}
|
|
291
|
+
else if (reference.kind === "lagrange") {
|
|
292
|
+
if (!objectMap.has(reference.primary)) {
|
|
293
|
+
diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, undefined, `${fieldPrefix}.at`));
|
|
294
|
+
}
|
|
295
|
+
else if (reference.secondary && !objectMap.has(reference.secondary)) {
|
|
296
|
+
diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, undefined, `${fieldPrefix}.at`));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
159
301
|
function validateAtTarget(object, objectMap, diagnostics) {
|
|
160
302
|
const reference = object.placement?.mode === "at" ? object.placement.reference : null;
|
|
161
303
|
if (!reference) {
|
|
@@ -261,6 +403,67 @@ function durationInDays(value) {
|
|
|
261
403
|
return null;
|
|
262
404
|
}
|
|
263
405
|
}
|
|
406
|
+
function validateProjection(projection, diagnostics, field, viewpointId) {
|
|
407
|
+
if (projection !== "topdown" &&
|
|
408
|
+
projection !== "isometric" &&
|
|
409
|
+
projection !== "orthographic" &&
|
|
410
|
+
projection !== "perspective") {
|
|
411
|
+
diagnostics.push(error("validate.viewpoint.projection.invalid", `Unknown projection "${String(projection)}" in viewpoint "${viewpointId}".`, undefined, field));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function validateCamera(camera, projection, rotationDeg, diagnostics, viewpointId, focusObjectId, selectedObjectId, filter, objectMap) {
|
|
415
|
+
if (!camera) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const prefix = `viewpoint.${viewpointId}.camera`;
|
|
419
|
+
for (const [key, value] of [
|
|
420
|
+
["azimuth", camera.azimuth],
|
|
421
|
+
["elevation", camera.elevation],
|
|
422
|
+
["roll", camera.roll],
|
|
423
|
+
["distance", camera.distance],
|
|
424
|
+
]) {
|
|
425
|
+
if (value !== null && (!Number.isFinite(value) || (key === "distance" && value <= 0))) {
|
|
426
|
+
diagnostics.push(error("validate.viewpoint.camera.invalid", `Invalid camera ${key} "${String(value)}" in viewpoint "${viewpointId}".`, undefined, `${prefix}.${key}`));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (camera.distance !== null && projection !== "perspective") {
|
|
430
|
+
diagnostics.push(warn("validate.viewpoint.camera.distance.partialEffect", `Camera "distance" only has a semantic effect in perspective viewpoints; "${viewpointId}" uses "${projection}".`, undefined, `${prefix}.distance`));
|
|
431
|
+
}
|
|
432
|
+
if (projection === "topdown" &&
|
|
433
|
+
(camera.elevation !== null || camera.roll !== null)) {
|
|
434
|
+
diagnostics.push(warn("validate.viewpoint.camera.topdownPartial", `Camera elevation/roll on topdown viewpoint "${viewpointId}" are currently stored for future 3D use and only partially affect 2D rendering.`, undefined, prefix));
|
|
435
|
+
}
|
|
436
|
+
if (projection === "isometric" &&
|
|
437
|
+
camera.elevation !== null) {
|
|
438
|
+
diagnostics.push(info("validate.viewpoint.camera.isometricStored", `Camera elevation on isometric viewpoint "${viewpointId}" is preserved semantically for future 3D rendering.`, undefined, `${prefix}.elevation`));
|
|
439
|
+
}
|
|
440
|
+
if (camera.azimuth !== null && camera.azimuth !== 0 && rotationDeg !== 0) {
|
|
441
|
+
diagnostics.push(warn("validate.viewpoint.rotation.cameraOverlap", `Viewpoint "${viewpointId}" uses camera.azimuth; keep "rotation" only for 2D screen rotation to avoid ambiguity.`, undefined, `${prefix}.azimuth`));
|
|
442
|
+
}
|
|
443
|
+
const hasAnchor = (focusObjectId !== null && objectMap.has(focusObjectId)) ||
|
|
444
|
+
(selectedObjectId !== null && objectMap.has(selectedObjectId)) ||
|
|
445
|
+
!!filter;
|
|
446
|
+
if (!hasAnchor) {
|
|
447
|
+
diagnostics.push(info("validate.viewpoint.camera.anchorMissing", `Viewpoint "${viewpointId}" stores camera settings without a focus object, selection, or filter anchor.`, undefined, prefix));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function resolveEffectiveEpoch(system, object, event, pose) {
|
|
451
|
+
return normalizeOptionalContextString(pose?.epoch) ??
|
|
452
|
+
normalizeOptionalContextString(event?.epoch) ??
|
|
453
|
+
normalizeOptionalContextString(object.epoch) ??
|
|
454
|
+
normalizeOptionalContextString(system?.epoch) ??
|
|
455
|
+
null;
|
|
456
|
+
}
|
|
457
|
+
function resolveEffectiveReferencePlane(system, object, event, pose) {
|
|
458
|
+
return normalizeOptionalContextString(pose?.referencePlane) ??
|
|
459
|
+
normalizeOptionalContextString(event?.referencePlane) ??
|
|
460
|
+
normalizeOptionalContextString(object.referencePlane) ??
|
|
461
|
+
normalizeOptionalContextString(system?.referencePlane) ??
|
|
462
|
+
null;
|
|
463
|
+
}
|
|
464
|
+
function normalizeOptionalContextString(value) {
|
|
465
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
466
|
+
}
|
|
264
467
|
function toleranceForField(object, field) {
|
|
265
468
|
const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
|
|
266
469
|
if (typeof tolerance === "number") {
|