worldorbit 2.5.13 → 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.
- package/README.md +37 -11
- package/dist/unpkg/worldorbit-core.min.js +12 -5
- package/dist/unpkg/worldorbit-markdown.min.js +32 -23
- package/dist/unpkg/worldorbit-viewer.min.js +55 -41
- package/dist/unpkg/worldorbit.js +1713 -231
- package/dist/unpkg/worldorbit.min.js +58 -44
- package/package.json +2 -2
- package/packages/core/README.md +5 -1
- package/packages/core/dist/atlas-edit.d.ts +2 -2
- package/packages/core/dist/atlas-edit.js +70 -7
- package/packages/core/dist/atlas-utils.d.ts +22 -0
- package/packages/core/dist/atlas-utils.js +189 -0
- package/packages/core/dist/atlas-validate.d.ts +2 -0
- package/packages/core/dist/atlas-validate.js +285 -0
- package/packages/core/dist/draft-parse.js +786 -153
- package/packages/core/dist/draft.d.ts +3 -0
- package/packages/core/dist/draft.js +47 -3
- package/packages/core/dist/format.js +165 -9
- package/packages/core/dist/load.js +58 -13
- package/packages/core/dist/normalize.js +7 -0
- package/packages/core/dist/scene.js +66 -13
- package/packages/core/dist/types.d.ts +97 -3
- package/packages/markdown/README.md +1 -1
- package/packages/viewer/README.md +2 -1
- package/packages/viewer/dist/atlas-state.js +7 -1
- package/packages/viewer/dist/atlas-viewer.js +35 -1
- package/packages/viewer/dist/render.js +16 -7
- package/packages/viewer/dist/theme.js +4 -0
- package/packages/viewer/dist/tooltip.js +35 -0
- package/packages/viewer/dist/types.d.ts +7 -0
- package/packages/viewer/dist/viewer.js +4 -0
- package/packages/editor/dist/editor.d.ts +0 -2
- package/packages/editor/dist/editor.js +0 -2998
- package/packages/editor/dist/index.d.ts +0 -2
- package/packages/editor/dist/index.js +0 -1
- package/packages/editor/dist/types.d.ts +0 -53
- package/packages/editor/dist/types.js +0 -1
- package/packages/markdown/dist/html.d.ts +0 -3
- package/packages/markdown/dist/html.js +0 -57
- package/packages/markdown/dist/index.d.ts +0 -4
- package/packages/markdown/dist/index.js +0 -3
- package/packages/markdown/dist/rehype.d.ts +0 -10
- package/packages/markdown/dist/rehype.js +0 -49
- package/packages/markdown/dist/remark.d.ts +0 -9
- package/packages/markdown/dist/remark.js +0 -28
- package/packages/markdown/dist/types.d.ts +0 -11
- package/packages/markdown/dist/types.js +0 -1
|
@@ -1,25 +1,102 @@
|
|
|
1
1
|
import { WorldOrbitError } from "./errors.js";
|
|
2
|
-
import {
|
|
3
|
-
import { getFieldSchema, isKnownFieldKey, WORLDORBIT_OBJECT_TYPES } from "./schema.js";
|
|
2
|
+
import { getFieldSchema, WORLDORBIT_OBJECT_TYPES } from "./schema.js";
|
|
4
3
|
import { getIndent, tokenizeLineDetailed } from "./tokenize.js";
|
|
5
|
-
import {
|
|
4
|
+
import { ensureAtlasFieldSupported, humanizeIdentifier, normalizeIdentifier, normalizeLegacyScalarValue, parseAtlasAtReference, parseAtlasBoolean, parseAtlasNumber, parseAtlasUnitValue, singleAtlasValue, tryParseAtlasUnitValue, } from "./atlas-utils.js";
|
|
5
|
+
import { collectAtlasDiagnostics } from "./atlas-validate.js";
|
|
6
|
+
const STRUCTURED_TYPED_BLOCKS = new Set([
|
|
7
|
+
"climate",
|
|
8
|
+
"habitability",
|
|
9
|
+
"settlement",
|
|
10
|
+
]);
|
|
11
|
+
const DRAFT_OBJECT_FIELD_SPECS = new Map();
|
|
12
|
+
for (const key of [
|
|
13
|
+
"orbit",
|
|
14
|
+
"distance",
|
|
15
|
+
"semiMajor",
|
|
16
|
+
"eccentricity",
|
|
17
|
+
"period",
|
|
18
|
+
"angle",
|
|
19
|
+
"inclination",
|
|
20
|
+
"phase",
|
|
21
|
+
"at",
|
|
22
|
+
"surface",
|
|
23
|
+
"free",
|
|
24
|
+
"kind",
|
|
25
|
+
"class",
|
|
26
|
+
"culture",
|
|
27
|
+
"tags",
|
|
28
|
+
"color",
|
|
29
|
+
"image",
|
|
30
|
+
"hidden",
|
|
31
|
+
"radius",
|
|
32
|
+
"mass",
|
|
33
|
+
"density",
|
|
34
|
+
"gravity",
|
|
35
|
+
"temperature",
|
|
36
|
+
"albedo",
|
|
37
|
+
"atmosphere",
|
|
38
|
+
"inner",
|
|
39
|
+
"outer",
|
|
40
|
+
"on",
|
|
41
|
+
"source",
|
|
42
|
+
"cycle",
|
|
43
|
+
]) {
|
|
44
|
+
const schema = getFieldSchema(key);
|
|
45
|
+
if (schema) {
|
|
46
|
+
DRAFT_OBJECT_FIELD_SPECS.set(key, {
|
|
47
|
+
key,
|
|
48
|
+
version: "2.0",
|
|
49
|
+
inlineMode: schema.arity === "multiple" ? "multiple" : "single",
|
|
50
|
+
allowRepeat: false,
|
|
51
|
+
legacySchema: schema,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const spec of [
|
|
56
|
+
{ key: "groups", inlineMode: "multiple", allowRepeat: false },
|
|
57
|
+
{ key: "epoch", inlineMode: "single", allowRepeat: false },
|
|
58
|
+
{ key: "referencePlane", inlineMode: "single", allowRepeat: false },
|
|
59
|
+
{ key: "tidalLock", inlineMode: "single", allowRepeat: false },
|
|
60
|
+
{ key: "renderLabel", inlineMode: "single", allowRepeat: false },
|
|
61
|
+
{ key: "renderOrbit", inlineMode: "single", allowRepeat: false },
|
|
62
|
+
{ key: "renderPriority", inlineMode: "single", allowRepeat: false },
|
|
63
|
+
{ key: "resonance", inlineMode: "pair", allowRepeat: false },
|
|
64
|
+
{ key: "derive", inlineMode: "pair", allowRepeat: true },
|
|
65
|
+
{ key: "validate", inlineMode: "single", allowRepeat: true },
|
|
66
|
+
{ key: "locked", inlineMode: "multiple", allowRepeat: false },
|
|
67
|
+
{ key: "tolerance", inlineMode: "pair", allowRepeat: true },
|
|
68
|
+
]) {
|
|
69
|
+
DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
|
|
70
|
+
key: spec.key,
|
|
71
|
+
version: "2.1",
|
|
72
|
+
inlineMode: spec.inlineMode,
|
|
73
|
+
allowRepeat: spec.allowRepeat,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
|
|
6
77
|
export function parseWorldOrbitAtlas(source) {
|
|
7
|
-
return parseAtlasSource(source
|
|
78
|
+
return parseAtlasSource(source);
|
|
8
79
|
}
|
|
9
80
|
export function parseWorldOrbitDraft(source) {
|
|
10
81
|
return parseAtlasSource(source, "2.0-draft");
|
|
11
82
|
}
|
|
12
|
-
function parseAtlasSource(source,
|
|
13
|
-
const
|
|
83
|
+
function parseAtlasSource(source, forcedOutputVersion) {
|
|
84
|
+
const prepared = preprocessAtlasSource(source);
|
|
85
|
+
const lines = prepared.source.split(/\r?\n/);
|
|
86
|
+
const diagnostics = [];
|
|
14
87
|
let sawSchemaHeader = false;
|
|
15
|
-
let
|
|
88
|
+
let sourceSchemaVersion = "2.0";
|
|
16
89
|
let system = null;
|
|
17
90
|
let section = null;
|
|
18
91
|
const objectNodes = [];
|
|
92
|
+
const groups = [];
|
|
93
|
+
const relations = [];
|
|
19
94
|
let sawDefaults = false;
|
|
20
95
|
let sawAtlas = false;
|
|
21
96
|
const viewpointIds = new Set();
|
|
22
97
|
const annotationIds = new Set();
|
|
98
|
+
const groupIds = new Set();
|
|
99
|
+
const relationIds = new Set();
|
|
23
100
|
for (let index = 0; index < lines.length; index++) {
|
|
24
101
|
const rawLine = lines[index];
|
|
25
102
|
const lineNumber = index + 1;
|
|
@@ -35,15 +112,22 @@ function parseAtlasSource(source, outputVersion) {
|
|
|
35
112
|
continue;
|
|
36
113
|
}
|
|
37
114
|
if (!sawSchemaHeader) {
|
|
38
|
-
|
|
115
|
+
sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
|
|
39
116
|
sawSchemaHeader = true;
|
|
117
|
+
if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
|
|
118
|
+
diagnostics.push({
|
|
119
|
+
code: "parse.schema21.commentCompatibility",
|
|
120
|
+
severity: "warning",
|
|
121
|
+
source: "parse",
|
|
122
|
+
message: `Comments require schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
|
|
123
|
+
line: prepared.comments[0].line,
|
|
124
|
+
column: prepared.comments[0].column,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
40
127
|
continue;
|
|
41
128
|
}
|
|
42
129
|
if (indent === 0) {
|
|
43
|
-
section = startTopLevelSection(tokens, lineNumber, system, objectNodes, viewpointIds, annotationIds, {
|
|
44
|
-
sawDefaults,
|
|
45
|
-
sawAtlas,
|
|
46
|
-
});
|
|
130
|
+
section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
|
|
47
131
|
if (section.kind === "system") {
|
|
48
132
|
system = section.system;
|
|
49
133
|
}
|
|
@@ -63,53 +147,64 @@ function parseAtlasSource(source, outputVersion) {
|
|
|
63
147
|
if (!sawSchemaHeader) {
|
|
64
148
|
throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
|
|
65
149
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const normalizedObjects = normalizeDocument(ast).objects;
|
|
71
|
-
validateDocument({
|
|
72
|
-
format: "worldorbit",
|
|
73
|
-
version: "1.0",
|
|
74
|
-
system: null,
|
|
75
|
-
objects: normalizedObjects,
|
|
76
|
-
});
|
|
77
|
-
const diagnostics = schemaVersion === "2.0-draft" && outputVersion === "2.0"
|
|
78
|
-
? [
|
|
79
|
-
{
|
|
80
|
-
code: "load.schema.deprecatedDraft",
|
|
81
|
-
severity: "warning",
|
|
82
|
-
source: "upgrade",
|
|
83
|
-
message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".',
|
|
84
|
-
},
|
|
85
|
-
]
|
|
86
|
-
: [];
|
|
87
|
-
return {
|
|
150
|
+
const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
|
|
151
|
+
const outputVersion = forcedOutputVersion ??
|
|
152
|
+
(sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
|
|
153
|
+
const baseDocument = {
|
|
88
154
|
format: "worldorbit",
|
|
89
|
-
version: outputVersion,
|
|
90
155
|
sourceVersion: "1.0",
|
|
91
156
|
system,
|
|
92
|
-
|
|
157
|
+
groups,
|
|
158
|
+
relations,
|
|
159
|
+
objects,
|
|
93
160
|
diagnostics,
|
|
94
161
|
};
|
|
162
|
+
if (outputVersion === "2.0-draft") {
|
|
163
|
+
const document = {
|
|
164
|
+
...baseDocument,
|
|
165
|
+
version: "2.0-draft",
|
|
166
|
+
schemaVersion: "2.0-draft",
|
|
167
|
+
};
|
|
168
|
+
document.diagnostics.push(...collectAtlasDiagnostics(document, sourceSchemaVersion));
|
|
169
|
+
return document;
|
|
170
|
+
}
|
|
171
|
+
const document = {
|
|
172
|
+
...baseDocument,
|
|
173
|
+
version: outputVersion,
|
|
174
|
+
schemaVersion: outputVersion,
|
|
175
|
+
};
|
|
176
|
+
if (sourceSchemaVersion === "2.0-draft") {
|
|
177
|
+
document.diagnostics.push({
|
|
178
|
+
code: "load.schema.deprecatedDraft",
|
|
179
|
+
severity: "warning",
|
|
180
|
+
source: "upgrade",
|
|
181
|
+
message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
document.diagnostics.push(...collectAtlasDiagnostics(document, sourceSchemaVersion));
|
|
185
|
+
return document;
|
|
95
186
|
}
|
|
96
187
|
function assertDraftSchemaHeader(tokens, line) {
|
|
97
188
|
if (tokens.length !== 2 ||
|
|
98
189
|
tokens[0].value.toLowerCase() !== "schema" ||
|
|
99
|
-
(tokens[1].value.toLowerCase()
|
|
100
|
-
|
|
101
|
-
throw new WorldOrbitError('Expected atlas header "schema 2.0" or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
|
|
190
|
+
!["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
|
|
191
|
+
throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
|
|
102
192
|
}
|
|
103
|
-
|
|
193
|
+
const version = tokens[1].value.toLowerCase();
|
|
194
|
+
return version === "2.1"
|
|
195
|
+
? "2.1"
|
|
196
|
+
: version === "2.0-draft"
|
|
197
|
+
? "2.0-draft"
|
|
198
|
+
: "2.0";
|
|
104
199
|
}
|
|
105
|
-
function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
|
|
200
|
+
function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
|
|
106
201
|
const keyword = tokens[0]?.value.toLowerCase();
|
|
107
202
|
switch (keyword) {
|
|
108
203
|
case "system":
|
|
109
204
|
if (system) {
|
|
110
205
|
throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
|
|
111
206
|
}
|
|
112
|
-
return startSystemSection(tokens, line);
|
|
207
|
+
return startSystemSection(tokens, line, sourceSchemaVersion, diagnostics);
|
|
113
208
|
case "defaults":
|
|
114
209
|
if (!system) {
|
|
115
210
|
throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
|
|
@@ -145,13 +240,19 @@ function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, a
|
|
|
145
240
|
throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
|
|
146
241
|
}
|
|
147
242
|
return startAnnotationSection(tokens, line, system, annotationIds);
|
|
243
|
+
case "group":
|
|
244
|
+
warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "group", { line, column: tokens[0].column });
|
|
245
|
+
return startGroupSection(tokens, line, groups, groupIds);
|
|
246
|
+
case "relation":
|
|
247
|
+
warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
|
|
248
|
+
return startRelationSection(tokens, line, relations, relationIds);
|
|
148
249
|
case "object":
|
|
149
|
-
return startObjectSection(tokens, line, objectNodes);
|
|
250
|
+
return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
|
|
150
251
|
default:
|
|
151
252
|
throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
|
|
152
253
|
}
|
|
153
254
|
}
|
|
154
|
-
function startSystemSection(tokens, line) {
|
|
255
|
+
function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
|
|
155
256
|
if (tokens.length !== 2) {
|
|
156
257
|
throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
|
|
157
258
|
}
|
|
@@ -159,6 +260,9 @@ function startSystemSection(tokens, line) {
|
|
|
159
260
|
type: "system",
|
|
160
261
|
id: tokens[1].value,
|
|
161
262
|
title: null,
|
|
263
|
+
description: null,
|
|
264
|
+
epoch: null,
|
|
265
|
+
referencePlane: null,
|
|
162
266
|
defaults: {
|
|
163
267
|
view: "topdown",
|
|
164
268
|
scale: null,
|
|
@@ -173,6 +277,8 @@ function startSystemSection(tokens, line) {
|
|
|
173
277
|
return {
|
|
174
278
|
kind: "system",
|
|
175
279
|
system,
|
|
280
|
+
sourceSchemaVersion,
|
|
281
|
+
diagnostics,
|
|
176
282
|
seenFields: new Set(),
|
|
177
283
|
};
|
|
178
284
|
}
|
|
@@ -238,24 +344,79 @@ function startAnnotationSection(tokens, line, system, annotationIds) {
|
|
|
238
344
|
seenFields: new Set(),
|
|
239
345
|
};
|
|
240
346
|
}
|
|
241
|
-
function
|
|
347
|
+
function startGroupSection(tokens, line, groups, groupIds) {
|
|
348
|
+
if (tokens.length !== 2) {
|
|
349
|
+
throw new WorldOrbitError("Invalid group declaration", line, tokens[0]?.column ?? 1);
|
|
350
|
+
}
|
|
351
|
+
const id = normalizeIdentifier(tokens[1].value);
|
|
352
|
+
if (!id) {
|
|
353
|
+
throw new WorldOrbitError("Group id must not be empty", line, tokens[1].column);
|
|
354
|
+
}
|
|
355
|
+
if (groupIds.has(id)) {
|
|
356
|
+
throw new WorldOrbitError(`Duplicate group id "${id}"`, line, tokens[1].column);
|
|
357
|
+
}
|
|
358
|
+
const group = {
|
|
359
|
+
id,
|
|
360
|
+
label: humanizeIdentifier(id),
|
|
361
|
+
summary: "",
|
|
362
|
+
color: null,
|
|
363
|
+
tags: [],
|
|
364
|
+
hidden: false,
|
|
365
|
+
};
|
|
366
|
+
groups.push(group);
|
|
367
|
+
groupIds.add(id);
|
|
368
|
+
return {
|
|
369
|
+
kind: "group",
|
|
370
|
+
group,
|
|
371
|
+
seenFields: new Set(),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function startRelationSection(tokens, line, relations, relationIds) {
|
|
375
|
+
if (tokens.length !== 2) {
|
|
376
|
+
throw new WorldOrbitError("Invalid relation declaration", line, tokens[0]?.column ?? 1);
|
|
377
|
+
}
|
|
378
|
+
const id = normalizeIdentifier(tokens[1].value);
|
|
379
|
+
if (!id) {
|
|
380
|
+
throw new WorldOrbitError("Relation id must not be empty", line, tokens[1].column);
|
|
381
|
+
}
|
|
382
|
+
if (relationIds.has(id)) {
|
|
383
|
+
throw new WorldOrbitError(`Duplicate relation id "${id}"`, line, tokens[1].column);
|
|
384
|
+
}
|
|
385
|
+
const relation = {
|
|
386
|
+
id,
|
|
387
|
+
from: "",
|
|
388
|
+
to: "",
|
|
389
|
+
kind: "",
|
|
390
|
+
label: null,
|
|
391
|
+
summary: null,
|
|
392
|
+
tags: [],
|
|
393
|
+
color: null,
|
|
394
|
+
hidden: false,
|
|
395
|
+
};
|
|
396
|
+
relations.push(relation);
|
|
397
|
+
relationIds.add(id);
|
|
398
|
+
return {
|
|
399
|
+
kind: "relation",
|
|
400
|
+
relation,
|
|
401
|
+
seenFields: new Set(),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
|
|
242
405
|
if (tokens.length < 3) {
|
|
243
406
|
throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
|
|
244
407
|
}
|
|
245
408
|
const objectTypeToken = tokens[1];
|
|
246
409
|
const idToken = tokens[2];
|
|
247
410
|
const objectType = objectTypeToken.value;
|
|
248
|
-
if (!WORLDORBIT_OBJECT_TYPES.has(objectType) ||
|
|
249
|
-
objectType === "system") {
|
|
411
|
+
if (!WORLDORBIT_OBJECT_TYPES.has(objectType) || objectType === "system") {
|
|
250
412
|
throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
|
|
251
413
|
}
|
|
252
414
|
const objectNode = {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
inlineFields: parseInlineFields(tokens.slice(3), line),
|
|
257
|
-
blockFields: [],
|
|
415
|
+
objectType: objectType,
|
|
416
|
+
id: idToken.value,
|
|
417
|
+
fields: parseInlineObjectFields(tokens.slice(3), line, objectType, sourceSchemaVersion, diagnostics),
|
|
258
418
|
infoEntries: [],
|
|
419
|
+
typedBlockEntries: {},
|
|
259
420
|
location: {
|
|
260
421
|
line,
|
|
261
422
|
column: objectTypeToken.column,
|
|
@@ -265,8 +426,12 @@ function startObjectSection(tokens, line, objectNodes) {
|
|
|
265
426
|
return {
|
|
266
427
|
kind: "object",
|
|
267
428
|
objectNode,
|
|
268
|
-
|
|
269
|
-
|
|
429
|
+
sourceSchemaVersion,
|
|
430
|
+
diagnostics,
|
|
431
|
+
activeBlock: null,
|
|
432
|
+
blockIndent: null,
|
|
433
|
+
seenInfoKeys: new Set(),
|
|
434
|
+
seenTypedBlockKeys: {},
|
|
270
435
|
};
|
|
271
436
|
}
|
|
272
437
|
function handleSectionLine(section, indent, tokens, line) {
|
|
@@ -286,6 +451,12 @@ function handleSectionLine(section, indent, tokens, line) {
|
|
|
286
451
|
case "annotation":
|
|
287
452
|
applyAnnotationField(section, tokens, line);
|
|
288
453
|
return;
|
|
454
|
+
case "group":
|
|
455
|
+
applyGroupField(section, tokens, line);
|
|
456
|
+
return;
|
|
457
|
+
case "relation":
|
|
458
|
+
applyRelationField(section, tokens, line);
|
|
459
|
+
return;
|
|
289
460
|
case "object":
|
|
290
461
|
applyObjectField(section, indent, tokens, line);
|
|
291
462
|
return;
|
|
@@ -293,10 +464,35 @@ function handleSectionLine(section, indent, tokens, line) {
|
|
|
293
464
|
}
|
|
294
465
|
function applySystemField(section, tokens, line) {
|
|
295
466
|
const key = requireUniqueField(tokens, section.seenFields, line);
|
|
296
|
-
|
|
297
|
-
|
|
467
|
+
const value = joinFieldValue(tokens, line);
|
|
468
|
+
switch (key) {
|
|
469
|
+
case "title":
|
|
470
|
+
section.system.title = value;
|
|
471
|
+
return;
|
|
472
|
+
case "description":
|
|
473
|
+
warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
|
|
474
|
+
line,
|
|
475
|
+
column: tokens[0].column,
|
|
476
|
+
});
|
|
477
|
+
section.system.description = value;
|
|
478
|
+
return;
|
|
479
|
+
case "epoch":
|
|
480
|
+
warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
|
|
481
|
+
line,
|
|
482
|
+
column: tokens[0].column,
|
|
483
|
+
});
|
|
484
|
+
section.system.epoch = value;
|
|
485
|
+
return;
|
|
486
|
+
case "referenceplane":
|
|
487
|
+
warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "referencePlane", {
|
|
488
|
+
line,
|
|
489
|
+
column: tokens[0].column,
|
|
490
|
+
});
|
|
491
|
+
section.system.referencePlane = value;
|
|
492
|
+
return;
|
|
493
|
+
default:
|
|
494
|
+
throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
|
|
298
495
|
}
|
|
299
|
-
section.system.title = joinFieldValue(tokens, line);
|
|
300
496
|
}
|
|
301
497
|
function applyDefaultsField(section, tokens, line) {
|
|
302
498
|
const key = requireUniqueField(tokens, section.seenFields, line);
|
|
@@ -327,14 +523,11 @@ function applyAtlasField(section, indent, tokens, line) {
|
|
|
327
523
|
section.metadataIndent = null;
|
|
328
524
|
}
|
|
329
525
|
if (section.inMetadata) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const key = tokens[0].value;
|
|
334
|
-
if (key in section.system.atlasMetadata) {
|
|
335
|
-
throw new WorldOrbitError(`Duplicate atlas metadata key "${key}"`, line, tokens[0].column);
|
|
526
|
+
const entry = parseInfoLikeEntry(tokens, line, "Invalid atlas metadata entry");
|
|
527
|
+
if (entry.key in section.system.atlasMetadata) {
|
|
528
|
+
throw new WorldOrbitError(`Duplicate atlas metadata key "${entry.key}"`, line, tokens[0].column);
|
|
336
529
|
}
|
|
337
|
-
section.system.atlasMetadata[key] =
|
|
530
|
+
section.system.atlasMetadata[entry.key] = entry.value;
|
|
338
531
|
return;
|
|
339
532
|
}
|
|
340
533
|
if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
|
|
@@ -436,21 +629,104 @@ function applyAnnotationField(section, tokens, line) {
|
|
|
436
629
|
throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
|
|
437
630
|
}
|
|
438
631
|
}
|
|
632
|
+
function applyGroupField(section, tokens, line) {
|
|
633
|
+
const key = requireUniqueField(tokens, section.seenFields, line);
|
|
634
|
+
switch (key) {
|
|
635
|
+
case "label":
|
|
636
|
+
section.group.label = joinFieldValue(tokens, line);
|
|
637
|
+
return;
|
|
638
|
+
case "summary":
|
|
639
|
+
section.group.summary = joinFieldValue(tokens, line);
|
|
640
|
+
return;
|
|
641
|
+
case "color":
|
|
642
|
+
section.group.color = joinFieldValue(tokens, line);
|
|
643
|
+
return;
|
|
644
|
+
case "tags":
|
|
645
|
+
section.group.tags = parseTokenList(tokens.slice(1), line, "tags");
|
|
646
|
+
return;
|
|
647
|
+
case "hidden":
|
|
648
|
+
section.group.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
|
|
649
|
+
line,
|
|
650
|
+
column: tokens[0].column,
|
|
651
|
+
});
|
|
652
|
+
return;
|
|
653
|
+
default:
|
|
654
|
+
throw new WorldOrbitError(`Unknown group field "${tokens[0].value}"`, line, tokens[0].column);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function applyRelationField(section, tokens, line) {
|
|
658
|
+
const key = requireUniqueField(tokens, section.seenFields, line);
|
|
659
|
+
switch (key) {
|
|
660
|
+
case "from":
|
|
661
|
+
section.relation.from = joinFieldValue(tokens, line);
|
|
662
|
+
return;
|
|
663
|
+
case "to":
|
|
664
|
+
section.relation.to = joinFieldValue(tokens, line);
|
|
665
|
+
return;
|
|
666
|
+
case "kind":
|
|
667
|
+
section.relation.kind = joinFieldValue(tokens, line);
|
|
668
|
+
return;
|
|
669
|
+
case "label":
|
|
670
|
+
section.relation.label = joinFieldValue(tokens, line);
|
|
671
|
+
return;
|
|
672
|
+
case "summary":
|
|
673
|
+
section.relation.summary = joinFieldValue(tokens, line);
|
|
674
|
+
return;
|
|
675
|
+
case "tags":
|
|
676
|
+
section.relation.tags = parseTokenList(tokens.slice(1), line, "tags");
|
|
677
|
+
return;
|
|
678
|
+
case "color":
|
|
679
|
+
section.relation.color = joinFieldValue(tokens, line);
|
|
680
|
+
return;
|
|
681
|
+
case "hidden":
|
|
682
|
+
section.relation.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
|
|
683
|
+
line,
|
|
684
|
+
column: tokens[0].column,
|
|
685
|
+
});
|
|
686
|
+
return;
|
|
687
|
+
default:
|
|
688
|
+
throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
439
691
|
function applyObjectField(section, indent, tokens, line) {
|
|
440
|
-
if (
|
|
441
|
-
section.
|
|
442
|
-
section.
|
|
443
|
-
return;
|
|
692
|
+
if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
|
|
693
|
+
section.activeBlock = null;
|
|
694
|
+
section.blockIndent = null;
|
|
444
695
|
}
|
|
445
|
-
if (
|
|
446
|
-
|
|
447
|
-
|
|
696
|
+
if (tokens.length === 1) {
|
|
697
|
+
const blockName = tokens[0].value.toLowerCase();
|
|
698
|
+
if (blockName === "info" || STRUCTURED_TYPED_BLOCKS.has(blockName)) {
|
|
699
|
+
if (blockName !== "info") {
|
|
700
|
+
warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, blockName, { line, column: tokens[0].column });
|
|
701
|
+
}
|
|
702
|
+
section.activeBlock = blockName;
|
|
703
|
+
section.blockIndent = indent;
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
448
706
|
}
|
|
449
|
-
if (section.
|
|
450
|
-
|
|
707
|
+
if (section.activeBlock) {
|
|
708
|
+
const entry = parseInfoLikeEntry(tokens, line, `Invalid ${section.activeBlock} entry`);
|
|
709
|
+
if (section.activeBlock === "info") {
|
|
710
|
+
if (section.seenInfoKeys.has(entry.key)) {
|
|
711
|
+
throw new WorldOrbitError(`Duplicate info key "${entry.key}"`, line, tokens[0].column);
|
|
712
|
+
}
|
|
713
|
+
section.seenInfoKeys.add(entry.key);
|
|
714
|
+
section.objectNode.infoEntries.push(entry);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const typedBlock = section.activeBlock;
|
|
718
|
+
const seenKeys = section.seenTypedBlockKeys[typedBlock] ??
|
|
719
|
+
(section.seenTypedBlockKeys[typedBlock] = new Set());
|
|
720
|
+
if (seenKeys.has(entry.key)) {
|
|
721
|
+
throw new WorldOrbitError(`Duplicate ${typedBlock} key "${entry.key}"`, line, tokens[0].column);
|
|
722
|
+
}
|
|
723
|
+
seenKeys.add(entry.key);
|
|
724
|
+
const entries = section.objectNode.typedBlockEntries[typedBlock] ??
|
|
725
|
+
(section.objectNode.typedBlockEntries[typedBlock] = []);
|
|
726
|
+
entries.push(entry);
|
|
451
727
|
return;
|
|
452
728
|
}
|
|
453
|
-
section.objectNode.
|
|
729
|
+
section.objectNode.fields.push(parseObjectField(tokens, line, section.objectNode.objectType, section.sourceSchemaVersion, section.diagnostics));
|
|
454
730
|
}
|
|
455
731
|
function requireUniqueField(tokens, seenFields, line) {
|
|
456
732
|
if (tokens.length < 2) {
|
|
@@ -474,64 +750,55 @@ function joinFieldValue(tokens, line) {
|
|
|
474
750
|
.trim();
|
|
475
751
|
}
|
|
476
752
|
function parseObjectTypeTokens(tokens, line) {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
value !== "asteroid" &&
|
|
487
|
-
value !== "comet" &&
|
|
488
|
-
value !== "ring" &&
|
|
489
|
-
value !== "structure" &&
|
|
490
|
-
value !== "phenomenon") {
|
|
491
|
-
throw new WorldOrbitError(`Unknown viewpoint object type "${token.value}"`, line, token.column);
|
|
492
|
-
}
|
|
493
|
-
return value;
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
function parseTokenList(tokens, line, field) {
|
|
497
|
-
if (tokens.length === 0) {
|
|
498
|
-
throw new WorldOrbitError(`Missing value for field "${field}"`, line);
|
|
499
|
-
}
|
|
500
|
-
return tokens.map((token) => token.value);
|
|
753
|
+
return parseTokenList(tokens, line, "objectTypes").filter((value) => value === "star" ||
|
|
754
|
+
value === "planet" ||
|
|
755
|
+
value === "moon" ||
|
|
756
|
+
value === "belt" ||
|
|
757
|
+
value === "asteroid" ||
|
|
758
|
+
value === "comet" ||
|
|
759
|
+
value === "ring" ||
|
|
760
|
+
value === "structure" ||
|
|
761
|
+
value === "phenomenon");
|
|
501
762
|
}
|
|
502
763
|
function parseLayerTokens(tokens, line) {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if (rawLayer === "orbits") {
|
|
511
|
-
next["orbits-back"] = enabled;
|
|
512
|
-
next["orbits-front"] = enabled;
|
|
764
|
+
const layers = {};
|
|
765
|
+
for (const token of parseTokenList(tokens, line, "layers")) {
|
|
766
|
+
const enabled = !token.startsWith("-") && !token.startsWith("!");
|
|
767
|
+
const raw = token.replace(/^[-!]+/, "").toLowerCase();
|
|
768
|
+
if (raw === "orbits") {
|
|
769
|
+
layers["orbits-back"] = enabled;
|
|
770
|
+
layers["orbits-front"] = enabled;
|
|
513
771
|
continue;
|
|
514
772
|
}
|
|
515
|
-
if (
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
773
|
+
if (raw === "background" ||
|
|
774
|
+
raw === "guides" ||
|
|
775
|
+
raw === "orbits-back" ||
|
|
776
|
+
raw === "orbits-front" ||
|
|
777
|
+
raw === "relations" ||
|
|
778
|
+
raw === "objects" ||
|
|
779
|
+
raw === "labels" ||
|
|
780
|
+
raw === "metadata") {
|
|
781
|
+
layers[raw] = enabled;
|
|
524
782
|
}
|
|
525
|
-
throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
|
|
526
783
|
}
|
|
527
|
-
return
|
|
784
|
+
return layers;
|
|
785
|
+
}
|
|
786
|
+
function parseTokenList(tokens, line, fieldName) {
|
|
787
|
+
if (tokens.length === 0) {
|
|
788
|
+
throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, 1);
|
|
789
|
+
}
|
|
790
|
+
const values = tokens.map((token) => token.value).filter(Boolean);
|
|
791
|
+
if (values.length === 0) {
|
|
792
|
+
throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, tokens[0]?.column ?? 1);
|
|
793
|
+
}
|
|
794
|
+
return values;
|
|
528
795
|
}
|
|
529
796
|
function parseProjectionValue(value, line, column) {
|
|
530
797
|
const normalized = value.toLowerCase();
|
|
531
|
-
if (normalized
|
|
532
|
-
|
|
798
|
+
if (normalized !== "topdown" && normalized !== "isometric") {
|
|
799
|
+
throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
|
|
533
800
|
}
|
|
534
|
-
|
|
801
|
+
return normalized;
|
|
535
802
|
}
|
|
536
803
|
function parsePresetValue(value, line, column) {
|
|
537
804
|
const normalized = value.toLowerCase();
|
|
@@ -544,16 +811,16 @@ function parsePresetValue(value, line, column) {
|
|
|
544
811
|
throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
|
|
545
812
|
}
|
|
546
813
|
function parsePositiveNumber(value, line, column, field) {
|
|
547
|
-
const parsed =
|
|
548
|
-
if (
|
|
549
|
-
throw new WorldOrbitError(`Field "${field}"
|
|
814
|
+
const parsed = parseFiniteNumber(value, line, column, field);
|
|
815
|
+
if (parsed <= 0) {
|
|
816
|
+
throw new WorldOrbitError(`Field "${field}" must be greater than zero`, line, column);
|
|
550
817
|
}
|
|
551
818
|
return parsed;
|
|
552
819
|
}
|
|
553
820
|
function parseFiniteNumber(value, line, column, field) {
|
|
554
821
|
const parsed = Number(value);
|
|
555
822
|
if (!Number.isFinite(parsed)) {
|
|
556
|
-
throw new WorldOrbitError(`
|
|
823
|
+
throw new WorldOrbitError(`Invalid numeric value "${value}" for "${field}"`, line, column);
|
|
557
824
|
}
|
|
558
825
|
return parsed;
|
|
559
826
|
}
|
|
@@ -565,30 +832,46 @@ function createEmptyViewpointFilter() {
|
|
|
565
832
|
groupIds: [],
|
|
566
833
|
};
|
|
567
834
|
}
|
|
568
|
-
function
|
|
835
|
+
function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
|
|
569
836
|
const fields = [];
|
|
570
837
|
let index = 0;
|
|
571
838
|
while (index < tokens.length) {
|
|
572
839
|
const keyToken = tokens[index];
|
|
573
|
-
const
|
|
574
|
-
if (!
|
|
840
|
+
const spec = getDraftObjectFieldSpec(keyToken.value);
|
|
841
|
+
if (!spec) {
|
|
575
842
|
throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
|
|
576
843
|
}
|
|
844
|
+
if (spec.version === "2.1") {
|
|
845
|
+
warnIfSchema21Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
|
|
846
|
+
line,
|
|
847
|
+
column: keyToken.column,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
577
850
|
index++;
|
|
578
851
|
const valueTokens = [];
|
|
579
|
-
if (
|
|
580
|
-
|
|
581
|
-
|
|
852
|
+
if (spec.inlineMode === "single") {
|
|
853
|
+
const nextToken = tokens[index];
|
|
854
|
+
if (nextToken) {
|
|
855
|
+
valueTokens.push(nextToken);
|
|
582
856
|
index++;
|
|
583
857
|
}
|
|
584
858
|
}
|
|
585
|
-
else {
|
|
586
|
-
|
|
587
|
-
|
|
859
|
+
else if (spec.inlineMode === "pair") {
|
|
860
|
+
for (let count = 0; count < 2; count++) {
|
|
861
|
+
const nextToken = tokens[index];
|
|
862
|
+
if (!nextToken) {
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
588
865
|
valueTokens.push(nextToken);
|
|
589
866
|
index++;
|
|
590
867
|
}
|
|
591
868
|
}
|
|
869
|
+
else {
|
|
870
|
+
while (index < tokens.length && !DRAFT_OBJECT_FIELD_KEYS.has(tokens[index].value)) {
|
|
871
|
+
valueTokens.push(tokens[index]);
|
|
872
|
+
index++;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
592
875
|
if (valueTokens.length === 0) {
|
|
593
876
|
throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
|
|
594
877
|
}
|
|
@@ -599,25 +882,35 @@ function parseInlineFields(tokens, line) {
|
|
|
599
882
|
location: { line, column: keyToken.column },
|
|
600
883
|
});
|
|
601
884
|
}
|
|
885
|
+
validateDraftObjectFieldCompatibility(fields, objectType);
|
|
602
886
|
return fields;
|
|
603
887
|
}
|
|
604
|
-
function
|
|
888
|
+
function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
|
|
605
889
|
if (tokens.length < 2) {
|
|
606
890
|
throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
|
|
607
891
|
}
|
|
608
|
-
|
|
892
|
+
const spec = getDraftObjectFieldSpec(tokens[0].value);
|
|
893
|
+
if (!spec) {
|
|
609
894
|
throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
|
|
610
895
|
}
|
|
611
|
-
|
|
896
|
+
if (spec.version === "2.1") {
|
|
897
|
+
warnIfSchema21Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
|
|
898
|
+
line,
|
|
899
|
+
column: tokens[0].column,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
const field = {
|
|
612
903
|
type: "field",
|
|
613
904
|
key: tokens[0].value,
|
|
614
905
|
values: tokens.slice(1).map((token) => token.value),
|
|
615
906
|
location: { line, column: tokens[0].column },
|
|
616
907
|
};
|
|
908
|
+
validateDraftObjectFieldCompatibility([field], objectType);
|
|
909
|
+
return field;
|
|
617
910
|
}
|
|
618
|
-
function
|
|
911
|
+
function parseInfoLikeEntry(tokens, line, errorMessage) {
|
|
619
912
|
if (tokens.length < 2) {
|
|
620
|
-
throw new WorldOrbitError(
|
|
913
|
+
throw new WorldOrbitError(errorMessage, line, tokens[0]?.column ?? 1);
|
|
621
914
|
}
|
|
622
915
|
return {
|
|
623
916
|
type: "info-entry",
|
|
@@ -626,17 +919,357 @@ function parseInfoEntry(tokens, line) {
|
|
|
626
919
|
location: { line, column: tokens[0].column },
|
|
627
920
|
};
|
|
628
921
|
}
|
|
629
|
-
function
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
.
|
|
641
|
-
|
|
922
|
+
function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
|
|
923
|
+
const fieldMap = collectDraftFields(node.fields);
|
|
924
|
+
const placement = extractDraftPlacement(node.objectType, fieldMap);
|
|
925
|
+
const properties = normalizeDraftProperties(node.objectType, fieldMap);
|
|
926
|
+
const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
|
|
927
|
+
const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
|
|
928
|
+
const referencePlane = parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]);
|
|
929
|
+
const tidalLock = fieldMap.has("tidalLock")
|
|
930
|
+
? parseAtlasBoolean(singleFieldValue(fieldMap.get("tidalLock")[0]), "tidalLock", fieldMap.get("tidalLock")[0].location)
|
|
931
|
+
: undefined;
|
|
932
|
+
const resonance = fieldMap.has("resonance")
|
|
933
|
+
? parseResonanceField(fieldMap.get("resonance")[0])
|
|
934
|
+
: undefined;
|
|
935
|
+
const renderHints = extractRenderHints(fieldMap);
|
|
936
|
+
const deriveRules = fieldMap.get("derive")?.map((field) => parseDeriveField(field));
|
|
937
|
+
const validationRules = fieldMap.get("validate")?.map((field) => ({
|
|
938
|
+
rule: singleFieldValue(field),
|
|
939
|
+
}));
|
|
940
|
+
const lockedFields = fieldMap.has("locked")
|
|
941
|
+
? [...new Set(fieldMap.get("locked").flatMap((field) => field.values))]
|
|
942
|
+
: undefined;
|
|
943
|
+
const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
|
|
944
|
+
const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
|
|
945
|
+
const info = normalizeInfoEntries(node.infoEntries, "info");
|
|
946
|
+
const object = {
|
|
947
|
+
type: node.objectType,
|
|
948
|
+
id: node.id,
|
|
949
|
+
properties,
|
|
950
|
+
placement,
|
|
951
|
+
info,
|
|
952
|
+
};
|
|
953
|
+
if (groups.length > 0)
|
|
954
|
+
object.groups = groups;
|
|
955
|
+
if (epoch)
|
|
956
|
+
object.epoch = epoch;
|
|
957
|
+
if (referencePlane)
|
|
958
|
+
object.referencePlane = referencePlane;
|
|
959
|
+
if (tidalLock !== undefined)
|
|
960
|
+
object.tidalLock = tidalLock;
|
|
961
|
+
if (resonance)
|
|
962
|
+
object.resonance = resonance;
|
|
963
|
+
if (renderHints)
|
|
964
|
+
object.renderHints = renderHints;
|
|
965
|
+
if (deriveRules?.length)
|
|
966
|
+
object.deriveRules = deriveRules;
|
|
967
|
+
if (validationRules?.length)
|
|
968
|
+
object.validationRules = validationRules;
|
|
969
|
+
if (lockedFields?.length)
|
|
970
|
+
object.lockedFields = lockedFields;
|
|
971
|
+
if (tolerances?.length)
|
|
972
|
+
object.tolerances = tolerances;
|
|
973
|
+
if (typedBlocks && Object.keys(typedBlocks).length > 0)
|
|
974
|
+
object.typedBlocks = typedBlocks;
|
|
975
|
+
if (sourceSchemaVersion !== "2.1") {
|
|
976
|
+
if (object.groups ||
|
|
977
|
+
object.epoch ||
|
|
978
|
+
object.referencePlane ||
|
|
979
|
+
object.tidalLock !== undefined ||
|
|
980
|
+
object.resonance ||
|
|
981
|
+
object.renderHints ||
|
|
982
|
+
object.deriveRules?.length ||
|
|
983
|
+
object.validationRules?.length ||
|
|
984
|
+
object.lockedFields?.length ||
|
|
985
|
+
object.tolerances?.length ||
|
|
986
|
+
object.typedBlocks) {
|
|
987
|
+
warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return object;
|
|
991
|
+
}
|
|
992
|
+
function collectDraftFields(fields) {
|
|
993
|
+
const grouped = new Map();
|
|
994
|
+
for (const field of fields) {
|
|
995
|
+
const spec = getDraftObjectFieldSpec(field.key);
|
|
996
|
+
if (!spec) {
|
|
997
|
+
throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
|
|
998
|
+
}
|
|
999
|
+
if (!spec.allowRepeat && grouped.has(field.key)) {
|
|
1000
|
+
throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
|
|
1001
|
+
}
|
|
1002
|
+
const existing = grouped.get(field.key) ?? [];
|
|
1003
|
+
existing.push(field);
|
|
1004
|
+
grouped.set(field.key, existing);
|
|
1005
|
+
}
|
|
1006
|
+
return grouped;
|
|
1007
|
+
}
|
|
1008
|
+
function extractDraftPlacement(objectType, fieldMap) {
|
|
1009
|
+
const orbitField = fieldMap.get("orbit")?.[0];
|
|
1010
|
+
const atField = fieldMap.get("at")?.[0];
|
|
1011
|
+
const surfaceField = fieldMap.get("surface")?.[0];
|
|
1012
|
+
const freeField = fieldMap.get("free")?.[0];
|
|
1013
|
+
const count = [orbitField, atField, surfaceField, freeField].filter(Boolean).length;
|
|
1014
|
+
if (count > 1) {
|
|
1015
|
+
const conflictingField = orbitField ?? atField ?? surfaceField ?? freeField;
|
|
1016
|
+
throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
|
|
1017
|
+
}
|
|
1018
|
+
if (orbitField) {
|
|
1019
|
+
return {
|
|
1020
|
+
mode: "orbit",
|
|
1021
|
+
target: singleFieldValue(orbitField),
|
|
1022
|
+
distance: parseOptionalUnitField(fieldMap.get("distance")?.[0], "distance"),
|
|
1023
|
+
semiMajor: parseOptionalUnitField(fieldMap.get("semiMajor")?.[0], "semiMajor"),
|
|
1024
|
+
eccentricity: parseOptionalNumberField(fieldMap.get("eccentricity")?.[0], "eccentricity"),
|
|
1025
|
+
period: parseOptionalUnitField(fieldMap.get("period")?.[0], "period"),
|
|
1026
|
+
angle: parseOptionalUnitField(fieldMap.get("angle")?.[0], "angle"),
|
|
1027
|
+
inclination: parseOptionalUnitField(fieldMap.get("inclination")?.[0], "inclination"),
|
|
1028
|
+
phase: parseOptionalUnitField(fieldMap.get("phase")?.[0], "phase"),
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
if (atField) {
|
|
1032
|
+
const target = singleFieldValue(atField);
|
|
1033
|
+
return {
|
|
1034
|
+
mode: "at",
|
|
1035
|
+
target,
|
|
1036
|
+
reference: parseAtlasAtReference(target, atField.location),
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
if (surfaceField) {
|
|
1040
|
+
return {
|
|
1041
|
+
mode: "surface",
|
|
1042
|
+
target: singleFieldValue(surfaceField),
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
if (freeField) {
|
|
1046
|
+
const raw = singleFieldValue(freeField);
|
|
1047
|
+
const distance = tryParseAtlasUnitValue(raw);
|
|
1048
|
+
return {
|
|
1049
|
+
mode: "free",
|
|
1050
|
+
distance: distance ?? undefined,
|
|
1051
|
+
descriptor: distance ? undefined : raw,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
return null;
|
|
1055
|
+
}
|
|
1056
|
+
function normalizeDraftProperties(objectType, fieldMap) {
|
|
1057
|
+
const properties = {};
|
|
1058
|
+
for (const [key, fields] of fieldMap.entries()) {
|
|
1059
|
+
const field = fields[0];
|
|
1060
|
+
const spec = getDraftObjectFieldSpec(key);
|
|
1061
|
+
if (!field || !spec?.legacySchema || spec.legacySchema.placement) {
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
ensureAtlasFieldSupported(key, objectType, field.location);
|
|
1065
|
+
properties[key] = normalizeLegacyScalarValue(key, field.values, field.location);
|
|
1066
|
+
}
|
|
1067
|
+
return properties;
|
|
1068
|
+
}
|
|
1069
|
+
function normalizeInfoEntries(entries, label) {
|
|
1070
|
+
const normalized = {};
|
|
1071
|
+
for (const entry of entries) {
|
|
1072
|
+
if (entry.key in normalized) {
|
|
1073
|
+
throw WorldOrbitError.fromLocation(`Duplicate ${label} key "${entry.key}"`, entry.location);
|
|
1074
|
+
}
|
|
1075
|
+
normalized[entry.key] = entry.value;
|
|
1076
|
+
}
|
|
1077
|
+
return normalized;
|
|
1078
|
+
}
|
|
1079
|
+
function normalizeTypedBlocks(typedBlockEntries) {
|
|
1080
|
+
const typedBlocks = {};
|
|
1081
|
+
for (const blockName of Object.keys(typedBlockEntries)) {
|
|
1082
|
+
const entries = typedBlockEntries[blockName];
|
|
1083
|
+
if (entries?.length) {
|
|
1084
|
+
typedBlocks[blockName] = normalizeInfoEntries(entries, blockName);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return typedBlocks;
|
|
1088
|
+
}
|
|
1089
|
+
function extractRenderHints(fieldMap) {
|
|
1090
|
+
const renderHints = {};
|
|
1091
|
+
const renderLabelField = fieldMap.get("renderLabel")?.[0];
|
|
1092
|
+
const renderOrbitField = fieldMap.get("renderOrbit")?.[0];
|
|
1093
|
+
const renderPriorityField = fieldMap.get("renderPriority")?.[0];
|
|
1094
|
+
if (renderLabelField) {
|
|
1095
|
+
renderHints.renderLabel = parseAtlasBoolean(singleFieldValue(renderLabelField), "renderLabel", renderLabelField.location);
|
|
1096
|
+
}
|
|
1097
|
+
if (renderOrbitField) {
|
|
1098
|
+
renderHints.renderOrbit = parseAtlasBoolean(singleFieldValue(renderOrbitField), "renderOrbit", renderOrbitField.location);
|
|
1099
|
+
}
|
|
1100
|
+
if (renderPriorityField) {
|
|
1101
|
+
renderHints.renderPriority = parseAtlasNumber(singleFieldValue(renderPriorityField), "renderPriority", renderPriorityField.location);
|
|
1102
|
+
}
|
|
1103
|
+
return Object.keys(renderHints).length > 0 ? renderHints : undefined;
|
|
1104
|
+
}
|
|
1105
|
+
function parseResonanceField(field) {
|
|
1106
|
+
if (field.values.length !== 2) {
|
|
1107
|
+
throw WorldOrbitError.fromLocation('Field "resonance" expects "<targetObjectId> <ratio>"', field.location);
|
|
1108
|
+
}
|
|
1109
|
+
const ratio = field.values[1];
|
|
1110
|
+
if (!/^\d+:\d+$/.test(ratio)) {
|
|
1111
|
+
throw WorldOrbitError.fromLocation(`Invalid resonance ratio "${ratio}"`, field.location);
|
|
1112
|
+
}
|
|
1113
|
+
return {
|
|
1114
|
+
targetObjectId: field.values[0],
|
|
1115
|
+
ratio,
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
function parseDeriveField(field) {
|
|
1119
|
+
if (field.values.length !== 2) {
|
|
1120
|
+
throw WorldOrbitError.fromLocation('Field "derive" expects "<field> <strategy>"', field.location);
|
|
1121
|
+
}
|
|
1122
|
+
return {
|
|
1123
|
+
field: field.values[0],
|
|
1124
|
+
strategy: field.values[1],
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function parseToleranceField(field) {
|
|
1128
|
+
if (field.values.length !== 2) {
|
|
1129
|
+
throw WorldOrbitError.fromLocation('Field "tolerance" expects "<field> <value>"', field.location);
|
|
1130
|
+
}
|
|
1131
|
+
const rawValue = field.values[1];
|
|
1132
|
+
const unitValue = tryParseAtlasUnitValue(rawValue);
|
|
1133
|
+
const numericValue = Number(rawValue);
|
|
1134
|
+
return {
|
|
1135
|
+
field: field.values[0],
|
|
1136
|
+
value: unitValue ?? (Number.isFinite(numericValue) ? numericValue : rawValue),
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function parseOptionalTokenList(field) {
|
|
1140
|
+
return field ? [...new Set(field.values)] : [];
|
|
1141
|
+
}
|
|
1142
|
+
function parseOptionalJoinedValue(field) {
|
|
1143
|
+
if (!field) {
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
return field.values.join(" ").trim() || null;
|
|
1147
|
+
}
|
|
1148
|
+
function parseOptionalUnitField(field, key) {
|
|
1149
|
+
return field ? parseAtlasUnitValue(singleFieldValue(field), field.location, key) : undefined;
|
|
1150
|
+
}
|
|
1151
|
+
function parseOptionalNumberField(field, key) {
|
|
1152
|
+
return field ? parseAtlasNumber(singleFieldValue(field), key, field.location) : undefined;
|
|
1153
|
+
}
|
|
1154
|
+
function singleFieldValue(field) {
|
|
1155
|
+
return singleAtlasValue(field.values, field.key, field.location);
|
|
1156
|
+
}
|
|
1157
|
+
function getDraftObjectFieldSpec(key) {
|
|
1158
|
+
return DRAFT_OBJECT_FIELD_SPECS.get(key);
|
|
1159
|
+
}
|
|
1160
|
+
function validateDraftObjectFieldCompatibility(fields, objectType) {
|
|
1161
|
+
for (const field of fields) {
|
|
1162
|
+
const spec = getDraftObjectFieldSpec(field.key);
|
|
1163
|
+
if (!spec) {
|
|
1164
|
+
throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
|
|
1165
|
+
}
|
|
1166
|
+
if (spec.legacySchema) {
|
|
1167
|
+
ensureAtlasFieldSupported(field.key, objectType, field.location);
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
if ((field.key === "renderLabel" || field.key === "renderOrbit" || field.key === "tidalLock") &&
|
|
1171
|
+
field.values.length !== 1) {
|
|
1172
|
+
throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
|
|
1177
|
+
if (sourceSchemaVersion === "2.1") {
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
diagnostics.push({
|
|
1181
|
+
code: "parse.schema21.featureCompatibility",
|
|
1182
|
+
severity: "warning",
|
|
1183
|
+
source: "parse",
|
|
1184
|
+
message: `Feature "${featureName}" requires schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
|
|
1185
|
+
line: location.line,
|
|
1186
|
+
column: location.column,
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
function preprocessAtlasSource(source) {
|
|
1190
|
+
const chars = [...source];
|
|
1191
|
+
const comments = [];
|
|
1192
|
+
let inString = false;
|
|
1193
|
+
let inBlockComment = false;
|
|
1194
|
+
let blockCommentStart = null;
|
|
1195
|
+
let line = 1;
|
|
1196
|
+
let column = 1;
|
|
1197
|
+
for (let index = 0; index < chars.length; index++) {
|
|
1198
|
+
const ch = chars[index];
|
|
1199
|
+
const next = chars[index + 1];
|
|
1200
|
+
if (inBlockComment) {
|
|
1201
|
+
if (ch === "*" && next === "/") {
|
|
1202
|
+
chars[index] = " ";
|
|
1203
|
+
chars[index + 1] = " ";
|
|
1204
|
+
inBlockComment = false;
|
|
1205
|
+
blockCommentStart = null;
|
|
1206
|
+
index++;
|
|
1207
|
+
column += 2;
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
if (ch !== "\n" && ch !== "\r") {
|
|
1211
|
+
chars[index] = " ";
|
|
1212
|
+
}
|
|
1213
|
+
if (ch === "\n") {
|
|
1214
|
+
line++;
|
|
1215
|
+
column = 1;
|
|
1216
|
+
}
|
|
1217
|
+
else {
|
|
1218
|
+
column++;
|
|
1219
|
+
}
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
if (!inString && ch === "/" && next === "*") {
|
|
1223
|
+
comments.push({ kind: "block", line, column });
|
|
1224
|
+
chars[index] = " ";
|
|
1225
|
+
chars[index + 1] = " ";
|
|
1226
|
+
inBlockComment = true;
|
|
1227
|
+
blockCommentStart = { line, column };
|
|
1228
|
+
index++;
|
|
1229
|
+
column += 2;
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (!inString && ch === "#" && !isHexColorLiteral(chars, index)) {
|
|
1233
|
+
comments.push({ kind: "line", line, column });
|
|
1234
|
+
chars[index] = " ";
|
|
1235
|
+
let inner = index + 1;
|
|
1236
|
+
while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
|
|
1237
|
+
chars[inner] = " ";
|
|
1238
|
+
inner++;
|
|
1239
|
+
}
|
|
1240
|
+
column += inner - index;
|
|
1241
|
+
index = inner - 1;
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
if (ch === '"' && chars[index - 1] !== "\\") {
|
|
1245
|
+
inString = !inString;
|
|
1246
|
+
}
|
|
1247
|
+
if (ch === "\n") {
|
|
1248
|
+
line++;
|
|
1249
|
+
column = 1;
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
column++;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
if (inBlockComment) {
|
|
1256
|
+
throw WorldOrbitError.fromLocation("Unclosed block comment", blockCommentStart ?? undefined);
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
source: chars.join(""),
|
|
1260
|
+
comments,
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
function isHexColorLiteral(chars, start) {
|
|
1264
|
+
let index = start + 1;
|
|
1265
|
+
let length = 0;
|
|
1266
|
+
while (index < chars.length && /[0-9a-f]/i.test(chars[index] ?? "")) {
|
|
1267
|
+
index++;
|
|
1268
|
+
length++;
|
|
1269
|
+
}
|
|
1270
|
+
if (![3, 4, 6, 8].includes(length)) {
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
const next = chars[index];
|
|
1274
|
+
return next === undefined || next === " " || next === "\t" || next === "\r" || next === "\n";
|
|
642
1275
|
}
|