worldorbit 3.2.2 → 5.0.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 +543 -543
- package/dist/browser/core/dist/atlas-edit.js +146 -1
- package/dist/browser/core/dist/atlas-validate.js +105 -10
- package/dist/browser/core/dist/draft-parse.js +461 -17
- package/dist/browser/core/dist/draft.d.ts +2 -1
- package/dist/browser/core/dist/draft.js +26 -4
- package/dist/browser/core/dist/format.js +126 -5
- package/dist/browser/core/dist/index.d.ts +1 -0
- package/dist/browser/core/dist/index.js +1 -0
- package/dist/browser/core/dist/load.js +12 -2
- package/dist/browser/core/dist/normalize.js +1 -0
- package/dist/browser/core/dist/scene.js +226 -5
- package/dist/browser/core/dist/schema.js +11 -1
- package/dist/browser/core/dist/solver.d.ts +33 -0
- package/dist/browser/core/dist/solver.js +99 -0
- package/dist/browser/core/dist/spatial-scene.js +56 -0
- package/dist/browser/core/dist/types.d.ts +130 -4
- package/dist/browser/editor/dist/editor.js +844 -719
- package/dist/browser/editor/dist/types.d.ts +2 -1
- package/dist/browser/viewer/dist/minimap.js +9 -7
- package/dist/browser/viewer/dist/render.js +78 -18
- package/dist/browser/viewer/dist/runtime-3d.js +2 -0
- package/dist/browser/viewer/dist/theme.js +1 -0
- package/dist/browser/viewer/dist/types.d.ts +7 -0
- package/dist/browser/viewer/dist/viewer.js +34 -3
- package/dist/obsidian-plugin/README.md +141 -124
- package/dist/obsidian-plugin/main.js +82 -68
- package/dist/unpkg/core/dist/atlas-edit.js +146 -1
- package/dist/unpkg/core/dist/atlas-validate.js +105 -10
- package/dist/unpkg/core/dist/draft-parse.js +461 -17
- package/dist/unpkg/core/dist/draft.d.ts +2 -1
- package/dist/unpkg/core/dist/draft.js +26 -4
- package/dist/unpkg/core/dist/format.js +126 -5
- package/dist/unpkg/core/dist/index.d.ts +1 -0
- package/dist/unpkg/core/dist/index.js +1 -0
- package/dist/unpkg/core/dist/load.js +12 -2
- package/dist/unpkg/core/dist/normalize.js +1 -0
- package/dist/unpkg/core/dist/scene.js +226 -5
- package/dist/unpkg/core/dist/schema.js +11 -1
- package/dist/unpkg/core/dist/solver.d.ts +33 -0
- package/dist/unpkg/core/dist/solver.js +99 -0
- package/dist/unpkg/core/dist/spatial-scene.js +56 -0
- package/dist/unpkg/core/dist/types.d.ts +130 -4
- package/dist/unpkg/editor/dist/editor.js +844 -719
- package/dist/unpkg/editor/dist/types.d.ts +2 -1
- package/dist/unpkg/viewer/dist/minimap.js +9 -7
- package/dist/unpkg/viewer/dist/render.js +78 -18
- package/dist/unpkg/viewer/dist/runtime-3d.js +2 -0
- package/dist/unpkg/viewer/dist/theme.js +1 -0
- package/dist/unpkg/viewer/dist/types.d.ts +7 -0
- package/dist/unpkg/viewer/dist/viewer.js +34 -3
- package/dist/unpkg/worldorbit-core.min.js +12 -12
- package/dist/unpkg/worldorbit-editor.min.js +381 -340
- package/dist/unpkg/worldorbit-markdown.min.js +47 -33
- package/dist/unpkg/worldorbit-viewer.min.js +238 -224
- package/dist/unpkg/worldorbit.js +1218 -46
- package/dist/unpkg/worldorbit.min.js +242 -228
- package/package.json +5 -1
- package/packages/core/dist/atlas-edit.js +146 -1
- package/packages/core/dist/atlas-validate.js +105 -10
- package/packages/core/dist/draft-parse.js +461 -17
- package/packages/core/dist/draft.d.ts +2 -1
- package/packages/core/dist/draft.js +26 -4
- package/packages/core/dist/format.js +126 -5
- package/packages/core/dist/index.d.ts +1 -0
- package/packages/core/dist/index.js +1 -0
- package/packages/core/dist/load.js +12 -2
- package/packages/core/dist/normalize.js +1 -0
- package/packages/core/dist/scene.js +226 -5
- package/packages/core/dist/schema.js +11 -1
- package/packages/core/dist/solver.d.ts +33 -0
- package/packages/core/dist/solver.js +99 -0
- package/packages/core/dist/spatial-scene.js +56 -0
- package/packages/core/dist/types.d.ts +130 -4
- package/packages/editor/dist/editor.js +844 -719
- package/packages/editor/dist/types.d.ts +2 -1
- package/packages/viewer/dist/minimap.js +9 -7
- package/packages/viewer/dist/render.js +78 -18
- package/packages/viewer/dist/runtime-3d.js +2 -0
- package/packages/viewer/dist/theme.js +1 -0
- package/packages/viewer/dist/types.d.ts +7 -0
- package/packages/viewer/dist/viewer.js +34 -3
|
@@ -18,15 +18,16 @@ export function upgradeDocumentToV2(document, options = {}) {
|
|
|
18
18
|
}
|
|
19
19
|
return {
|
|
20
20
|
format: "worldorbit",
|
|
21
|
-
version: "
|
|
22
|
-
schemaVersion: "
|
|
21
|
+
version: "3.1",
|
|
22
|
+
schemaVersion: "3.1",
|
|
23
23
|
sourceVersion: document.version,
|
|
24
24
|
theme: document.theme ?? null,
|
|
25
25
|
system,
|
|
26
26
|
groups: structuredClone(document.groups ?? []),
|
|
27
27
|
relations: structuredClone(document.relations ?? []),
|
|
28
28
|
events: structuredClone(document.events ?? []),
|
|
29
|
-
|
|
29
|
+
trajectories: structuredClone(document.trajectories ?? []),
|
|
30
|
+
objects: document.objects.map(cloneWorldOrbitObject).map(normalizeLegacyCraftObject),
|
|
30
31
|
diagnostics,
|
|
31
32
|
};
|
|
32
33
|
}
|
|
@@ -57,6 +58,7 @@ export function materializeAtlasDocument(document, options = {}) {
|
|
|
57
58
|
groups: structuredClone(document.groups ?? []),
|
|
58
59
|
relations: structuredClone(document.relations ?? []),
|
|
59
60
|
events: document.events.map(cloneWorldOrbitEvent),
|
|
61
|
+
trajectories: document.trajectories.map(cloneWorldOrbitTrajectory),
|
|
60
62
|
objects,
|
|
61
63
|
};
|
|
62
64
|
}
|
|
@@ -226,6 +228,7 @@ function mapSceneViewpointToDraftViewpoint(viewpoint) {
|
|
|
226
228
|
function cloneWorldOrbitObject(object) {
|
|
227
229
|
return {
|
|
228
230
|
...object,
|
|
231
|
+
trajectoryId: object.trajectoryId ?? null,
|
|
229
232
|
groups: object.groups ? [...object.groups] : undefined,
|
|
230
233
|
resonance: object.resonance ? { ...object.resonance } : object.resonance,
|
|
231
234
|
renderHints: object.renderHints ? { ...object.renderHints } : object.renderHints,
|
|
@@ -255,6 +258,7 @@ function cloneWorldOrbitObject(object) {
|
|
|
255
258
|
function cloneWorldOrbitEvent(event) {
|
|
256
259
|
return {
|
|
257
260
|
...event,
|
|
261
|
+
trajectoryId: event.trajectoryId ?? null,
|
|
258
262
|
participantObjectIds: [...event.participantObjectIds],
|
|
259
263
|
tags: [...event.tags],
|
|
260
264
|
positions: event.positions.map(cloneWorldOrbitEventPose),
|
|
@@ -264,12 +268,30 @@ function cloneWorldOrbitEventPose(pose) {
|
|
|
264
268
|
return {
|
|
265
269
|
objectId: pose.objectId,
|
|
266
270
|
placement: clonePlacement(pose.placement),
|
|
271
|
+
trajectorySegmentId: pose.trajectorySegmentId ?? null,
|
|
272
|
+
trajectoryManeuverId: pose.trajectoryManeuverId ?? null,
|
|
267
273
|
inner: pose.inner ? { ...pose.inner } : undefined,
|
|
268
274
|
outer: pose.outer ? { ...pose.outer } : undefined,
|
|
269
275
|
epoch: pose.epoch ?? null,
|
|
270
276
|
referencePlane: pose.referencePlane ?? null,
|
|
271
277
|
};
|
|
272
278
|
}
|
|
279
|
+
function cloneWorldOrbitTrajectory(trajectory) {
|
|
280
|
+
return structuredClone(trajectory);
|
|
281
|
+
}
|
|
282
|
+
function normalizeLegacyCraftObject(object) {
|
|
283
|
+
if (object.type !== "structure") {
|
|
284
|
+
return object;
|
|
285
|
+
}
|
|
286
|
+
const kind = typeof object.properties.kind === "string" ? object.properties.kind.toLowerCase() : "";
|
|
287
|
+
if (!["ship", "probe", "station"].includes(kind)) {
|
|
288
|
+
return object;
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
...object,
|
|
292
|
+
type: "craft",
|
|
293
|
+
};
|
|
294
|
+
}
|
|
273
295
|
function clonePlacement(placement) {
|
|
274
296
|
return placement ? structuredClone(placement) : null;
|
|
275
297
|
}
|
|
@@ -468,7 +490,7 @@ function serializeViewpointLayers(layers) {
|
|
|
468
490
|
if (orbitFront !== undefined || orbitBack !== undefined) {
|
|
469
491
|
tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
|
|
470
492
|
}
|
|
471
|
-
for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
|
|
493
|
+
for (const key of ["background", "guides", "relations", "events", "objects", "trajectories", "labels", "metadata"]) {
|
|
472
494
|
if (layers[key] !== undefined) {
|
|
473
495
|
tokens.push(layers[key] ? key : `-${key}`);
|
|
474
496
|
}
|
|
@@ -40,10 +40,14 @@ export function formatDocument(document, options = {}) {
|
|
|
40
40
|
schema === "2.1" ||
|
|
41
41
|
schema === "2.5" ||
|
|
42
42
|
schema === "2.6" ||
|
|
43
|
+
schema === "3.0" ||
|
|
44
|
+
schema === "3.1" ||
|
|
43
45
|
schema === "2.0-draft" ||
|
|
44
46
|
document.version === "2.0" ||
|
|
45
47
|
document.version === "2.1" ||
|
|
46
48
|
document.version === "2.5" ||
|
|
49
|
+
document.version === "3.0" ||
|
|
50
|
+
document.version === "3.1" ||
|
|
47
51
|
document.version === "2.6" ||
|
|
48
52
|
document.version === "2.0-draft";
|
|
49
53
|
if (useDraft) {
|
|
@@ -59,7 +63,12 @@ export function formatDocument(document, options = {}) {
|
|
|
59
63
|
: upgradeDocumentToDraftV2(document);
|
|
60
64
|
return formatDraftDocument(legacyDraftDocument);
|
|
61
65
|
}
|
|
62
|
-
const atlasDocument = document.version === "2.0" ||
|
|
66
|
+
const atlasDocument = document.version === "2.0" ||
|
|
67
|
+
document.version === "2.1" ||
|
|
68
|
+
document.version === "2.5" ||
|
|
69
|
+
document.version === "2.6" ||
|
|
70
|
+
document.version === "3.0" ||
|
|
71
|
+
document.version === "3.1"
|
|
63
72
|
? document
|
|
64
73
|
: document.version === "2.0-draft"
|
|
65
74
|
? {
|
|
@@ -68,7 +77,13 @@ export function formatDocument(document, options = {}) {
|
|
|
68
77
|
schemaVersion: "2.0",
|
|
69
78
|
}
|
|
70
79
|
: upgradeDocumentToV2(document);
|
|
71
|
-
if ((schema === "2.0" ||
|
|
80
|
+
if ((schema === "2.0" ||
|
|
81
|
+
schema === "2.1" ||
|
|
82
|
+
schema === "2.5" ||
|
|
83
|
+
schema === "2.6" ||
|
|
84
|
+
schema === "3.0" ||
|
|
85
|
+
schema === "3.1") &&
|
|
86
|
+
atlasDocument.version !== schema) {
|
|
72
87
|
return formatAtlasDocument({
|
|
73
88
|
...atlasDocument,
|
|
74
89
|
version: schema,
|
|
@@ -108,6 +123,10 @@ export function formatAtlasDocument(document) {
|
|
|
108
123
|
lines.push("");
|
|
109
124
|
lines.push(...formatAtlasEvent(event));
|
|
110
125
|
}
|
|
126
|
+
for (const trajectory of [...document.trajectories].sort(compareIdLike)) {
|
|
127
|
+
lines.push("");
|
|
128
|
+
lines.push(...formatAtlasTrajectory(trajectory));
|
|
129
|
+
}
|
|
111
130
|
const sortedObjects = [...document.objects].sort(compareObjects);
|
|
112
131
|
if (sortedObjects.length > 0 && lines.at(-1) !== "") {
|
|
113
132
|
lines.push("");
|
|
@@ -295,6 +314,9 @@ function formatObjectMetadata(object) {
|
|
|
295
314
|
if (object.groups?.length) {
|
|
296
315
|
lines.push(`groups ${object.groups.join(" ")}`);
|
|
297
316
|
}
|
|
317
|
+
if (object.trajectoryId) {
|
|
318
|
+
lines.push(`trajectory ${object.trajectoryId}`);
|
|
319
|
+
}
|
|
298
320
|
if (object.epoch) {
|
|
299
321
|
lines.push(`epoch ${quoteIfNeeded(object.epoch)}`);
|
|
300
322
|
}
|
|
@@ -455,6 +477,9 @@ function formatAtlasEvent(event) {
|
|
|
455
477
|
if (event.summary) {
|
|
456
478
|
lines.push(` summary ${quoteIfNeeded(event.summary)}`);
|
|
457
479
|
}
|
|
480
|
+
if (event.trajectoryId) {
|
|
481
|
+
lines.push(` trajectory ${event.trajectoryId}`);
|
|
482
|
+
}
|
|
458
483
|
if (event.targetObjectId) {
|
|
459
484
|
lines.push(` target ${event.targetObjectId}`);
|
|
460
485
|
}
|
|
@@ -497,12 +522,106 @@ function formatAtlasEvent(event) {
|
|
|
497
522
|
function formatEventPoseFields(pose) {
|
|
498
523
|
return [
|
|
499
524
|
...formatPlacement(pose.placement),
|
|
525
|
+
...(pose.trajectorySegmentId ? [`segment ${pose.trajectorySegmentId}`] : []),
|
|
526
|
+
...(pose.trajectoryManeuverId ? [`maneuver ${pose.trajectoryManeuverId}`] : []),
|
|
500
527
|
...(pose.epoch ? [`epoch ${quoteIfNeeded(pose.epoch)}`] : []),
|
|
501
528
|
...(pose.referencePlane ? [`referencePlane ${quoteIfNeeded(pose.referencePlane)}`] : []),
|
|
502
529
|
...formatOptionalUnit("inner", pose.inner),
|
|
503
530
|
...formatOptionalUnit("outer", pose.outer),
|
|
504
531
|
];
|
|
505
532
|
}
|
|
533
|
+
function formatAtlasTrajectory(trajectory) {
|
|
534
|
+
const lines = [`trajectory ${trajectory.id}`];
|
|
535
|
+
if (trajectory.label) {
|
|
536
|
+
lines.push(` label ${quoteIfNeeded(trajectory.label)}`);
|
|
537
|
+
}
|
|
538
|
+
if (trajectory.summary) {
|
|
539
|
+
lines.push(` summary ${quoteIfNeeded(trajectory.summary)}`);
|
|
540
|
+
}
|
|
541
|
+
if (trajectory.craftObjectId) {
|
|
542
|
+
lines.push(` craft ${trajectory.craftObjectId}`);
|
|
543
|
+
}
|
|
544
|
+
if (trajectory.tags.length > 0) {
|
|
545
|
+
lines.push(` tags ${trajectory.tags.map(quoteIfNeeded).join(" ")}`);
|
|
546
|
+
}
|
|
547
|
+
if (trajectory.color) {
|
|
548
|
+
lines.push(` color ${quoteIfNeeded(trajectory.color)}`);
|
|
549
|
+
}
|
|
550
|
+
if (trajectory.renderMode) {
|
|
551
|
+
lines.push(` renderMode ${trajectory.renderMode}`);
|
|
552
|
+
}
|
|
553
|
+
if (trajectory.stroke) {
|
|
554
|
+
lines.push(` stroke ${quoteIfNeeded(trajectory.stroke)}`);
|
|
555
|
+
}
|
|
556
|
+
if (trajectory.strokeWidth !== null && trajectory.strokeWidth !== undefined) {
|
|
557
|
+
lines.push(` strokeWidth ${trajectory.strokeWidth}`);
|
|
558
|
+
}
|
|
559
|
+
if (trajectory.marker) {
|
|
560
|
+
lines.push(` marker ${quoteIfNeeded(trajectory.marker)}`);
|
|
561
|
+
}
|
|
562
|
+
if (trajectory.labelMode) {
|
|
563
|
+
lines.push(` labelMode ${quoteIfNeeded(trajectory.labelMode)}`);
|
|
564
|
+
}
|
|
565
|
+
if (trajectory.showWaypoints !== null && trajectory.showWaypoints !== undefined) {
|
|
566
|
+
lines.push(` showWaypoints ${trajectory.showWaypoints ? "true" : "false"}`);
|
|
567
|
+
}
|
|
568
|
+
if (trajectory.hidden) {
|
|
569
|
+
lines.push(" hidden true");
|
|
570
|
+
}
|
|
571
|
+
for (const segment of [...trajectory.segments].sort(compareIdLike)) {
|
|
572
|
+
lines.push("");
|
|
573
|
+
lines.push(` segment ${segment.id}`);
|
|
574
|
+
for (const field of formatTrajectorySegmentFields(segment)) {
|
|
575
|
+
lines.push(` ${field}`);
|
|
576
|
+
}
|
|
577
|
+
for (const maneuver of [...segment.maneuvers].sort(compareIdLike)) {
|
|
578
|
+
lines.push(` maneuver ${maneuver.id}`);
|
|
579
|
+
for (const field of formatTrajectoryManeuverFields(maneuver)) {
|
|
580
|
+
lines.push(` ${field}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return lines;
|
|
585
|
+
}
|
|
586
|
+
function formatTrajectorySegmentFields(segment) {
|
|
587
|
+
return [
|
|
588
|
+
`kind ${segment.kind}`,
|
|
589
|
+
...(segment.label ? [`label ${quoteIfNeeded(segment.label)}`] : []),
|
|
590
|
+
...(segment.summary ? [`summary ${quoteIfNeeded(segment.summary)}`] : []),
|
|
591
|
+
...(segment.fromObjectId ? [`from ${segment.fromObjectId}`] : []),
|
|
592
|
+
...(segment.toObjectId ? [`to ${segment.toObjectId}`] : []),
|
|
593
|
+
...(segment.aroundObjectId ? [`around ${segment.aroundObjectId}`] : []),
|
|
594
|
+
...(segment.assist?.objectId ? [`assist ${segment.assist.objectId}`] : []),
|
|
595
|
+
...(segment.epoch ? [`epoch ${quoteIfNeeded(segment.epoch)}`] : []),
|
|
596
|
+
...formatOptionalUnit("periapsis", segment.periapsis),
|
|
597
|
+
...formatOptionalUnit("apoapsis", segment.apoapsis),
|
|
598
|
+
...formatOptionalUnit("inclination", segment.inclination),
|
|
599
|
+
...formatOptionalUnit("duration", segment.duration),
|
|
600
|
+
...formatOptionalUnit("deltaV", segment.deltaV),
|
|
601
|
+
...formatOptionalUnit("phaseAngle", segment.phaseAngle),
|
|
602
|
+
...formatOptionalUnit("turnAngle", segment.turnAngle),
|
|
603
|
+
...formatOptionalUnit("energy", segment.energy),
|
|
604
|
+
...(segment.waypointLabel ? [`waypointLabel ${quoteIfNeeded(segment.waypointLabel)}`] : []),
|
|
605
|
+
...(segment.waypointDate ? [`waypointDate ${quoteIfNeeded(segment.waypointDate)}`] : []),
|
|
606
|
+
...(segment.renderHidden !== null && segment.renderHidden !== undefined
|
|
607
|
+
? [`renderHidden ${segment.renderHidden ? "true" : "false"}`]
|
|
608
|
+
: []),
|
|
609
|
+
...(segment.sampleDensity !== null && segment.sampleDensity !== undefined
|
|
610
|
+
? [`sampleDensity ${segment.sampleDensity}`]
|
|
611
|
+
: []),
|
|
612
|
+
...(segment.notes.length > 0 ? [`notes ${segment.notes.map(quoteIfNeeded).join(" ")}`] : []),
|
|
613
|
+
];
|
|
614
|
+
}
|
|
615
|
+
function formatTrajectoryManeuverFields(maneuver) {
|
|
616
|
+
return [
|
|
617
|
+
`kind ${quoteIfNeeded(maneuver.kind)}`,
|
|
618
|
+
...(maneuver.label ? [`label ${quoteIfNeeded(maneuver.label)}`] : []),
|
|
619
|
+
...(maneuver.epoch ? [`epoch ${quoteIfNeeded(maneuver.epoch)}`] : []),
|
|
620
|
+
...formatOptionalUnit("deltaV", maneuver.deltaV),
|
|
621
|
+
...formatOptionalUnit("duration", maneuver.duration),
|
|
622
|
+
...(maneuver.notes.length > 0 ? [`notes ${maneuver.notes.map(quoteIfNeeded).join(" ")}`] : []),
|
|
623
|
+
];
|
|
624
|
+
}
|
|
506
625
|
function hasCameraValues(camera) {
|
|
507
626
|
return (camera.azimuth !== null ||
|
|
508
627
|
camera.elevation !== null ||
|
|
@@ -554,7 +673,7 @@ function formatDraftLayers(layers) {
|
|
|
554
673
|
? "orbits"
|
|
555
674
|
: "-orbits");
|
|
556
675
|
}
|
|
557
|
-
for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
|
|
676
|
+
for (const key of ["background", "guides", "relations", "events", "objects", "trajectories", "labels", "metadata"]) {
|
|
558
677
|
if (layers[key] !== undefined) {
|
|
559
678
|
tokens.push(layers[key] ? key : `-${key}`);
|
|
560
679
|
}
|
|
@@ -601,10 +720,12 @@ function objectTypeIndex(objectType) {
|
|
|
601
720
|
return 5;
|
|
602
721
|
case "ring":
|
|
603
722
|
return 6;
|
|
604
|
-
case "
|
|
723
|
+
case "craft":
|
|
605
724
|
return 7;
|
|
606
|
-
case "
|
|
725
|
+
case "structure":
|
|
607
726
|
return 8;
|
|
727
|
+
case "phenomenon":
|
|
728
|
+
return 9;
|
|
608
729
|
}
|
|
609
730
|
}
|
|
610
731
|
function quoteIfNeeded(value) {
|
|
@@ -8,6 +8,7 @@ export { validateDocument } from "./validate.js";
|
|
|
8
8
|
export { createDiagnostic, diagnosticFromError, normalizeWithDiagnostics, parseWithDiagnostics, validateDocumentWithDiagnostics, } from "./diagnostics.js";
|
|
9
9
|
export { renderDocumentToScene, rotatePoint } from "./scene.js";
|
|
10
10
|
export { evaluateSpatialSceneAtTime, renderDocumentToSpatialScene, } from "./spatial-scene.js";
|
|
11
|
+
export { createTrajectorySolverSnapshot } from "./solver.js";
|
|
11
12
|
export { formatAtlasDocument, formatDocument, formatDraftDocument } from "./format.js";
|
|
12
13
|
export { materializeAtlasDocument, materializeDraftDocument, upgradeDocumentToDraftV2, upgradeDocumentToV2, } from "./draft.js";
|
|
13
14
|
export { parseWorldOrbitAtlas, parseWorldOrbitDraft } from "./draft-parse.js";
|
|
@@ -8,6 +8,7 @@ export { validateDocument } from "./validate.js";
|
|
|
8
8
|
export { createDiagnostic, diagnosticFromError, normalizeWithDiagnostics, parseWithDiagnostics, validateDocumentWithDiagnostics, } from "./diagnostics.js";
|
|
9
9
|
export { renderDocumentToScene, rotatePoint } from "./scene.js";
|
|
10
10
|
export { evaluateSpatialSceneAtTime, renderDocumentToSpatialScene, } from "./spatial-scene.js";
|
|
11
|
+
export { createTrajectorySolverSnapshot } from "./solver.js";
|
|
11
12
|
export { formatAtlasDocument, formatDocument, formatDraftDocument } from "./format.js";
|
|
12
13
|
export { materializeAtlasDocument, materializeDraftDocument, upgradeDocumentToDraftV2, upgradeDocumentToV2, } from "./draft.js";
|
|
13
14
|
export { parseWorldOrbitAtlas, parseWorldOrbitDraft } from "./draft-parse.js";
|
|
@@ -5,10 +5,12 @@ import { WorldOrbitError } from "./errors.js";
|
|
|
5
5
|
import { normalizeDocument } from "./normalize.js";
|
|
6
6
|
import { parseWorldOrbit } from "./parse.js";
|
|
7
7
|
import { validateDocument } from "./validate.js";
|
|
8
|
-
const ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1|\.5|\.6)
|
|
8
|
+
const ATLAS_SCHEMA_PATTERN = /^schema\s+(?:2(?:\.0|\.1|\.5|\.6)?|3(?:\.0|\.1)?)$/i;
|
|
9
9
|
const ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
|
|
10
10
|
const ATLAS_SCHEMA_25_PATTERN = /^schema\s+2\.5$/i;
|
|
11
11
|
const ATLAS_SCHEMA_26_PATTERN = /^schema\s+2\.6$/i;
|
|
12
|
+
const ATLAS_SCHEMA_30_PATTERN = /^schema\s+3(?:\.0)?$/i;
|
|
13
|
+
const ATLAS_SCHEMA_31_PATTERN = /^schema\s+3\.1$/i;
|
|
12
14
|
const LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
|
|
13
15
|
export function detectWorldOrbitSchemaVersion(source) {
|
|
14
16
|
for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
|
|
@@ -28,6 +30,12 @@ export function detectWorldOrbitSchemaVersion(source) {
|
|
|
28
30
|
if (ATLAS_SCHEMA_26_PATTERN.test(trimmed)) {
|
|
29
31
|
return "2.6";
|
|
30
32
|
}
|
|
33
|
+
if (ATLAS_SCHEMA_30_PATTERN.test(trimmed)) {
|
|
34
|
+
return "3.0";
|
|
35
|
+
}
|
|
36
|
+
if (ATLAS_SCHEMA_31_PATTERN.test(trimmed)) {
|
|
37
|
+
return "3.1";
|
|
38
|
+
}
|
|
31
39
|
if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
|
|
32
40
|
return "2.0";
|
|
33
41
|
}
|
|
@@ -92,7 +100,9 @@ export function loadWorldOrbitSourceWithDiagnostics(source) {
|
|
|
92
100
|
schemaVersion === "2.0-draft" ||
|
|
93
101
|
schemaVersion === "2.1" ||
|
|
94
102
|
schemaVersion === "2.5" ||
|
|
95
|
-
schemaVersion === "2.6"
|
|
103
|
+
schemaVersion === "2.6" ||
|
|
104
|
+
schemaVersion === "3.0" ||
|
|
105
|
+
schemaVersion === "3.1") {
|
|
96
106
|
return loadAtlasSourceWithDiagnostics(source, schemaVersion);
|
|
97
107
|
}
|
|
98
108
|
let ast;
|
|
@@ -138,7 +138,8 @@ export function renderDocumentToScene(document, options = {}) {
|
|
|
138
138
|
const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
|
|
139
139
|
const relations = createSceneRelations(document, objects);
|
|
140
140
|
const events = createSceneEvents(document.events ?? [], objects, activeEventId);
|
|
141
|
-
const
|
|
141
|
+
const trajectories = createSceneTrajectories(document, objects, events, options);
|
|
142
|
+
const layers = createSceneLayers(orbitVisuals, trajectories, relations, events, leaders, objects, labels);
|
|
142
143
|
const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
|
|
143
144
|
const semanticGroups = createSceneSemanticGroups(document, objects);
|
|
144
145
|
const viewpoints = createSceneViewpoints(document, schemaProjection, frame.preset, relationships, objectMap);
|
|
@@ -179,6 +180,7 @@ export function renderDocumentToScene(document, options = {}) {
|
|
|
179
180
|
viewpoints,
|
|
180
181
|
events,
|
|
181
182
|
activeEventId,
|
|
183
|
+
trajectories,
|
|
182
184
|
objects,
|
|
183
185
|
orbitVisuals,
|
|
184
186
|
relations,
|
|
@@ -467,7 +469,9 @@ function createSceneObject(position, scaleModel, relationships) {
|
|
|
467
469
|
anchorX,
|
|
468
470
|
anchorY,
|
|
469
471
|
label: object.id,
|
|
470
|
-
secondaryLabel: object.type === "structure"
|
|
472
|
+
secondaryLabel: object.type === "structure" || object.type === "craft"
|
|
473
|
+
? String(object.properties.kind ?? object.type)
|
|
474
|
+
: object.type,
|
|
471
475
|
fillColor: customColorFor(object.properties.color),
|
|
472
476
|
imageHref: typeof object.properties.image === "string" && object.properties.image.trim()
|
|
473
477
|
? object.properties.image
|
|
@@ -566,6 +570,7 @@ function labelPlacementPriority(object) {
|
|
|
566
570
|
case "asteroid":
|
|
567
571
|
case "comet":
|
|
568
572
|
return 4;
|
|
573
|
+
case "craft":
|
|
569
574
|
case "structure":
|
|
570
575
|
case "phenomenon":
|
|
571
576
|
return 5;
|
|
@@ -590,7 +595,8 @@ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
|
|
|
590
595
|
const oppositeVertical = vertical === "below" ? "above" : "below";
|
|
591
596
|
const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
|
|
592
597
|
const oppositeHorizontal = horizontal === "right" ? "left" : "right";
|
|
593
|
-
const preferHorizontal = object.object.type === "
|
|
598
|
+
const preferHorizontal = object.object.type === "craft" ||
|
|
599
|
+
object.object.type === "structure" ||
|
|
594
600
|
object.object.type === "phenomenon" ||
|
|
595
601
|
object.object.placement?.mode === "at" ||
|
|
596
602
|
object.object.placement?.mode === "surface" ||
|
|
@@ -658,7 +664,7 @@ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
|
|
|
658
664
|
}
|
|
659
665
|
}
|
|
660
666
|
}
|
|
661
|
-
function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
|
|
667
|
+
function createSceneLayers(orbitVisuals, trajectories, relations, events, leaders, objects, labels) {
|
|
662
668
|
const backOrbitIds = orbitVisuals
|
|
663
669
|
.filter((visual) => !visual.hidden && Boolean(visual.backArcPath))
|
|
664
670
|
.map((visual) => visual.renderId);
|
|
@@ -673,6 +679,15 @@ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, la
|
|
|
673
679
|
},
|
|
674
680
|
{ id: "orbits-back", renderIds: backOrbitIds },
|
|
675
681
|
{ id: "orbits-front", renderIds: frontOrbitIds },
|
|
682
|
+
{
|
|
683
|
+
id: "trajectories",
|
|
684
|
+
renderIds: trajectories
|
|
685
|
+
.filter((trajectory) => !trajectory.hidden)
|
|
686
|
+
.flatMap((trajectory) => [
|
|
687
|
+
trajectory.renderId,
|
|
688
|
+
...trajectory.waypoints.filter((waypoint) => !waypoint.hidden).map((waypoint) => waypoint.renderId),
|
|
689
|
+
]),
|
|
690
|
+
},
|
|
676
691
|
{
|
|
677
692
|
id: "relations",
|
|
678
693
|
renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId),
|
|
@@ -815,6 +830,206 @@ function createSceneEvents(events, objects, activeEventId) {
|
|
|
815
830
|
})
|
|
816
831
|
.sort((left, right) => left.event.id.localeCompare(right.event.id));
|
|
817
832
|
}
|
|
833
|
+
function createSceneTrajectories(document, objects, events, options) {
|
|
834
|
+
const objectMap = new Map(objects.map((object) => [object.objectId, object]));
|
|
835
|
+
return document.trajectories
|
|
836
|
+
.map((trajectory) => createSceneTrajectory(trajectory, objectMap, events, options))
|
|
837
|
+
.sort((left, right) => left.trajectoryId.localeCompare(right.trajectoryId));
|
|
838
|
+
}
|
|
839
|
+
function createSceneTrajectory(trajectory, objectMap, events, options) {
|
|
840
|
+
const craftObject = trajectory.craftObjectId ? objectMap.get(trajectory.craftObjectId) ?? null : null;
|
|
841
|
+
const mode = resolveTrajectoryMode(trajectory, options);
|
|
842
|
+
const stroke = trajectory.stroke ?? trajectory.color ?? craftObject?.fillColor ?? null;
|
|
843
|
+
const strokeWidth = trajectory.strokeWidth ?? 2.4;
|
|
844
|
+
const showWaypoints = options.showTrajectoryWaypoints ?? trajectory.showWaypoints ?? true;
|
|
845
|
+
const labelMode = trajectory.labelMode ?? (options.showTrajectoryLabels === false ? "hidden" : "waypoint");
|
|
846
|
+
const pathParts = [];
|
|
847
|
+
const waypoints = [];
|
|
848
|
+
const objectIds = new Set();
|
|
849
|
+
let lastAnchor = craftObject;
|
|
850
|
+
if (craftObject) {
|
|
851
|
+
objectIds.add(craftObject.objectId);
|
|
852
|
+
}
|
|
853
|
+
trajectory.segments.forEach((segment, index) => {
|
|
854
|
+
const geometry = buildTrajectorySegmentGeometry(trajectory, segment, index, mode, objectMap, lastAnchor, showWaypoints);
|
|
855
|
+
if (geometry.path) {
|
|
856
|
+
pathParts.push(geometry.path);
|
|
857
|
+
}
|
|
858
|
+
geometry.objectIds.forEach((objectId) => objectIds.add(objectId));
|
|
859
|
+
waypoints.push(...geometry.waypoints);
|
|
860
|
+
lastAnchor = geometry.lastAnchor ?? lastAnchor;
|
|
861
|
+
});
|
|
862
|
+
for (const event of events.filter((entry) => entry.event.trajectoryId === trajectory.id)) {
|
|
863
|
+
const eventObject = event.targetObjectId
|
|
864
|
+
? objectMap.get(event.targetObjectId) ?? null
|
|
865
|
+
: event.objectIds.map((objectId) => objectMap.get(objectId) ?? null).find(Boolean) ?? null;
|
|
866
|
+
if (!eventObject) {
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
waypoints.push({
|
|
870
|
+
renderId: `${createRenderId(`${trajectory.id}-${event.eventId}`)}-waypoint`,
|
|
871
|
+
trajectoryId: trajectory.id,
|
|
872
|
+
segmentId: null,
|
|
873
|
+
maneuverId: null,
|
|
874
|
+
objectId: eventObject.objectId,
|
|
875
|
+
x: eventObject.x,
|
|
876
|
+
y: eventObject.y,
|
|
877
|
+
label: event.event.label ?? event.event.id,
|
|
878
|
+
dateLabel: event.event.epoch ?? null,
|
|
879
|
+
hidden: trajectory.hidden || event.hidden || !showWaypoints,
|
|
880
|
+
});
|
|
881
|
+
objectIds.add(eventObject.objectId);
|
|
882
|
+
}
|
|
883
|
+
return {
|
|
884
|
+
renderId: `${createRenderId(trajectory.id)}-trajectory`,
|
|
885
|
+
trajectoryId: trajectory.id,
|
|
886
|
+
trajectory,
|
|
887
|
+
craftObjectId: trajectory.craftObjectId,
|
|
888
|
+
mode,
|
|
889
|
+
path: pathParts.join(" "),
|
|
890
|
+
stroke,
|
|
891
|
+
strokeWidth,
|
|
892
|
+
marker: trajectory.marker ?? "arrow",
|
|
893
|
+
labelMode,
|
|
894
|
+
showWaypoints,
|
|
895
|
+
objectIds: [...objectIds],
|
|
896
|
+
waypoints,
|
|
897
|
+
hidden: trajectory.hidden || pathParts.length === 0,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
function buildTrajectorySegmentGeometry(trajectory, segment, segmentIndex, mode, objectMap, fallbackStart, showWaypoints) {
|
|
901
|
+
const start = resolveTrajectoryObject(segment.fromObjectId, objectMap) ??
|
|
902
|
+
fallbackStart;
|
|
903
|
+
const assist = resolveTrajectoryObject(segment.assist?.objectId ?? segment.aroundObjectId ?? null, objectMap);
|
|
904
|
+
const end = resolveTrajectoryObject(segment.toObjectId, objectMap) ??
|
|
905
|
+
resolveTrajectoryObject(segment.aroundObjectId, objectMap) ??
|
|
906
|
+
assist ??
|
|
907
|
+
start;
|
|
908
|
+
if (!start || !end) {
|
|
909
|
+
return {
|
|
910
|
+
path: "",
|
|
911
|
+
objectIds: [],
|
|
912
|
+
waypoints: [],
|
|
913
|
+
lastAnchor: fallbackStart,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
const objectIds = [start.objectId, end.objectId];
|
|
917
|
+
if (assist) {
|
|
918
|
+
objectIds.push(assist.objectId);
|
|
919
|
+
}
|
|
920
|
+
const hidden = trajectory.hidden || segment.renderHidden === true || start.hidden || end.hidden;
|
|
921
|
+
const waypoints = [];
|
|
922
|
+
const label = segment.waypointLabel ?? segment.label ?? humanizeIdentifier(segment.id);
|
|
923
|
+
const dateLabel = segment.waypointDate ?? segment.epoch ?? null;
|
|
924
|
+
if (showWaypoints) {
|
|
925
|
+
if (segmentIndex === 0) {
|
|
926
|
+
waypoints.push(createTrajectoryWaypoint(trajectory.id, segment.id, null, start, start.label, null, hidden));
|
|
927
|
+
}
|
|
928
|
+
if (assist && assist.objectId !== start.objectId && assist.objectId !== end.objectId) {
|
|
929
|
+
waypoints.push(createTrajectoryWaypoint(trajectory.id, segment.id, null, assist, label, dateLabel, hidden));
|
|
930
|
+
}
|
|
931
|
+
waypoints.push(createTrajectoryWaypoint(trajectory.id, segment.id, null, end, label, dateLabel, hidden));
|
|
932
|
+
}
|
|
933
|
+
const samples = mode === "solver"
|
|
934
|
+
? sampleQuadraticPoints(start, resolveTrajectoryControlPoint(start, end, assist, segmentIndex), end, Math.max(10, Math.round((segment.sampleDensity ?? 1) * 14)))
|
|
935
|
+
: null;
|
|
936
|
+
const control = resolveTrajectoryControlPoint(start, end, assist, segmentIndex);
|
|
937
|
+
const path = mode === "solver"
|
|
938
|
+
? pointsToPath(samples ?? [start, end])
|
|
939
|
+
: `M ${formatPathNumber(start.x)} ${formatPathNumber(start.y)} Q ${formatPathNumber(control.x)} ${formatPathNumber(control.y)} ${formatPathNumber(end.x)} ${formatPathNumber(end.y)}`;
|
|
940
|
+
segment.maneuvers.forEach((maneuver, maneuverIndex) => {
|
|
941
|
+
if (!showWaypoints) {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const point = samplePointOnQuadratic(start, control, end, (maneuverIndex + 1) / (segment.maneuvers.length + 1));
|
|
945
|
+
waypoints.push({
|
|
946
|
+
renderId: `${createRenderId(`${trajectory.id}-${segment.id}-${maneuver.id}`)}-waypoint`,
|
|
947
|
+
trajectoryId: trajectory.id,
|
|
948
|
+
segmentId: segment.id,
|
|
949
|
+
maneuverId: maneuver.id,
|
|
950
|
+
objectId: null,
|
|
951
|
+
x: point.x,
|
|
952
|
+
y: point.y,
|
|
953
|
+
label: maneuver.label ?? maneuver.kind,
|
|
954
|
+
dateLabel: maneuver.epoch ?? null,
|
|
955
|
+
hidden,
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
return {
|
|
959
|
+
path,
|
|
960
|
+
objectIds: [...new Set(objectIds)],
|
|
961
|
+
waypoints,
|
|
962
|
+
lastAnchor: end,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
function createTrajectoryWaypoint(trajectoryId, segmentId, maneuverId, object, label, dateLabel, hidden) {
|
|
966
|
+
return {
|
|
967
|
+
renderId: `${createRenderId(`${trajectoryId}-${segmentId ?? object.objectId}-${maneuverId ?? object.objectId}`)}-waypoint`,
|
|
968
|
+
trajectoryId,
|
|
969
|
+
segmentId,
|
|
970
|
+
maneuverId,
|
|
971
|
+
objectId: object.objectId,
|
|
972
|
+
x: object.x,
|
|
973
|
+
y: object.y,
|
|
974
|
+
label,
|
|
975
|
+
dateLabel,
|
|
976
|
+
hidden,
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
function resolveTrajectoryMode(trajectory, options) {
|
|
980
|
+
const requested = options.trajectoryMode ?? trajectory.renderMode ?? "auto";
|
|
981
|
+
if (requested !== "auto") {
|
|
982
|
+
return requested;
|
|
983
|
+
}
|
|
984
|
+
const segmentCount = trajectory.segments.filter((segment) => segment.fromObjectId || segment.toObjectId || segment.assist?.objectId || segment.aroundObjectId).length;
|
|
985
|
+
return segmentCount > 0 ? "solver" : "illustrative";
|
|
986
|
+
}
|
|
987
|
+
function resolveTrajectoryObject(objectId, objectMap) {
|
|
988
|
+
return objectId ? objectMap.get(objectId) ?? null : null;
|
|
989
|
+
}
|
|
990
|
+
function resolveTrajectoryControlPoint(start, end, assist, segmentIndex) {
|
|
991
|
+
if (assist) {
|
|
992
|
+
return {
|
|
993
|
+
x: assist.x,
|
|
994
|
+
y: assist.y,
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
const dx = end.x - start.x;
|
|
998
|
+
const dy = end.y - start.y;
|
|
999
|
+
const length = Math.max(Math.hypot(dx, dy), 1);
|
|
1000
|
+
const offset = Math.min(Math.max(length * 0.18, 26), 120) * (segmentIndex % 2 === 0 ? 1 : -1);
|
|
1001
|
+
const normalX = -dy / length;
|
|
1002
|
+
const normalY = dx / length;
|
|
1003
|
+
return {
|
|
1004
|
+
x: (start.x + end.x) / 2 + normalX * offset,
|
|
1005
|
+
y: (start.y + end.y) / 2 + normalY * offset,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function sampleQuadraticPoints(start, control, end, count) {
|
|
1009
|
+
const samples = [];
|
|
1010
|
+
for (let index = 0; index <= count; index += 1) {
|
|
1011
|
+
samples.push(samplePointOnQuadratic(start, control, end, index / count));
|
|
1012
|
+
}
|
|
1013
|
+
return samples;
|
|
1014
|
+
}
|
|
1015
|
+
function samplePointOnQuadratic(start, control, end, t) {
|
|
1016
|
+
const inverse = 1 - t;
|
|
1017
|
+
return {
|
|
1018
|
+
x: inverse * inverse * start.x + 2 * inverse * t * control.x + t * t * end.x,
|
|
1019
|
+
y: inverse * inverse * start.y + 2 * inverse * t * control.y + t * t * end.y,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function pointsToPath(points) {
|
|
1023
|
+
if (points.length === 0) {
|
|
1024
|
+
return "";
|
|
1025
|
+
}
|
|
1026
|
+
return points
|
|
1027
|
+
.map((point, index) => `${index === 0 ? "M" : "L"} ${formatPathNumber(point.x)} ${formatPathNumber(point.y)}`)
|
|
1028
|
+
.join(" ");
|
|
1029
|
+
}
|
|
1030
|
+
function formatPathNumber(value) {
|
|
1031
|
+
return Number.isFinite(value) ? value.toFixed(2) : "0";
|
|
1032
|
+
}
|
|
818
1033
|
function createSceneViewpoints(document, projection, preset, relationships, objectMap) {
|
|
819
1034
|
const generatedOverview = createGeneratedOverviewViewpoint(document, projection, preset);
|
|
820
1035
|
const drafts = new Map();
|
|
@@ -1095,7 +1310,8 @@ function parseViewpointLayers(value) {
|
|
|
1095
1310
|
rawLayer === "events" ||
|
|
1096
1311
|
rawLayer === "objects" ||
|
|
1097
1312
|
rawLayer === "labels" ||
|
|
1098
|
-
rawLayer === "metadata"
|
|
1313
|
+
rawLayer === "metadata" ||
|
|
1314
|
+
rawLayer === "trajectories") {
|
|
1099
1315
|
next[rawLayer] = enabled;
|
|
1100
1316
|
}
|
|
1101
1317
|
}
|
|
@@ -1109,6 +1325,7 @@ function parseViewpointObjectTypes(value) {
|
|
|
1109
1325
|
entry === "asteroid" ||
|
|
1110
1326
|
entry === "comet" ||
|
|
1111
1327
|
entry === "ring" ||
|
|
1328
|
+
entry === "craft" ||
|
|
1112
1329
|
entry === "structure" ||
|
|
1113
1330
|
entry === "phenomenon");
|
|
1114
1331
|
}
|
|
@@ -1785,6 +2002,8 @@ function visualRadiusFor(object, depth, scaleModel, sceneMetricScale) {
|
|
|
1785
2002
|
return clampNumber(6 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
|
|
1786
2003
|
case "ring":
|
|
1787
2004
|
return clampNumber(5 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
|
|
2005
|
+
case "craft":
|
|
2006
|
+
return clampNumber(5 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
|
|
1788
2007
|
case "structure":
|
|
1789
2008
|
return clampNumber(6 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
|
|
1790
2009
|
case "phenomenon":
|
|
@@ -1798,6 +2017,8 @@ function visualExtentForObject(object, radius, scaleModel) {
|
|
|
1798
2017
|
return radius * 2.4;
|
|
1799
2018
|
case "phenomenon":
|
|
1800
2019
|
return radius * 1.25;
|
|
2020
|
+
case "craft":
|
|
2021
|
+
return radius + 1.5;
|
|
1801
2022
|
case "structure":
|
|
1802
2023
|
return radius + 2;
|
|
1803
2024
|
default:
|