worldorbit 2.5.7 → 2.5.9
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/dist/unpkg/worldorbit-core.min.js +5 -5
- package/dist/unpkg/worldorbit-markdown.min.js +11 -11
- package/dist/unpkg/worldorbit-viewer.min.js +17 -17
- package/dist/unpkg/worldorbit.js +43 -9
- package/dist/unpkg/worldorbit.min.js +19 -19
- package/package.json +2 -2
- package/packages/core/dist/normalize.js +1 -1
- package/packages/core/dist/scene.js +16 -0
- package/packages/core/dist/schema.js +4 -4
- package/packages/core/dist/types.d.ts +1 -1
- package/packages/viewer/dist/render.js +105 -83
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "worldorbit",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.9",
|
|
4
4
|
"description": "A text-based DSL and parser pipeline for orbital worldbuilding",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/unpkg/worldorbit.esm.js",
|
|
@@ -64,4 +64,4 @@
|
|
|
64
64
|
"typescript": "^5.6.0",
|
|
65
65
|
"unified": "^11.0.5"
|
|
66
66
|
}
|
|
67
|
-
}
|
|
67
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WorldOrbitError } from "./errors.js";
|
|
2
2
|
import { getFieldSchema, supportsObjectType, unitFamilyAllowsUnit, } from "./schema.js";
|
|
3
|
-
const UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(au|km|re|sol|
|
|
3
|
+
const UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(kpc|min|mj|rj|ky|my|gy|au|km|me|re|pc|ly|deg|sol|K|m|s|h|d|y)?$/;
|
|
4
4
|
const BOOLEAN_VALUES = new Map([
|
|
5
5
|
["true", true],
|
|
6
6
|
["false", false],
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const AU_IN_KM = 149_597_870.7;
|
|
2
2
|
const EARTH_RADIUS_IN_KM = 6_371;
|
|
3
|
+
const JUPITER_RADIUS_IN_KM = 71_492;
|
|
3
4
|
const SOLAR_RADIUS_IN_KM = 695_700;
|
|
5
|
+
const LY_IN_AU = 63_241.077;
|
|
6
|
+
const PC_IN_AU = 206_264.806;
|
|
7
|
+
const KPC_IN_AU = 206_264_806;
|
|
4
8
|
const ISO_FLATTENING = 0.68;
|
|
5
9
|
const MIN_ISO_MINOR_SCALE = 0.2;
|
|
6
10
|
const ARC_SAMPLE_COUNT = 28;
|
|
@@ -1340,11 +1344,23 @@ function toDistanceMetric(value) {
|
|
|
1340
1344
|
return value.value;
|
|
1341
1345
|
case "km":
|
|
1342
1346
|
return value.value / AU_IN_KM;
|
|
1347
|
+
case "m":
|
|
1348
|
+
return (value.value / 1_000) / AU_IN_KM;
|
|
1349
|
+
case "ly":
|
|
1350
|
+
return value.value * LY_IN_AU;
|
|
1351
|
+
case "pc":
|
|
1352
|
+
return value.value * PC_IN_AU;
|
|
1353
|
+
case "kpc":
|
|
1354
|
+
return value.value * KPC_IN_AU;
|
|
1343
1355
|
case "re":
|
|
1344
1356
|
return (value.value * EARTH_RADIUS_IN_KM) / AU_IN_KM;
|
|
1357
|
+
case "rj":
|
|
1358
|
+
return (value.value * JUPITER_RADIUS_IN_KM) / AU_IN_KM;
|
|
1345
1359
|
case "sol":
|
|
1346
1360
|
return (value.value * SOLAR_RADIUS_IN_KM) / AU_IN_KM;
|
|
1347
1361
|
default:
|
|
1362
|
+
// Unitless or non-distance units (me, mj, s, min, h, d, y, ky, my, gy, K, deg):
|
|
1363
|
+
// return raw value — renderer treats it as an abstract metric
|
|
1348
1364
|
return value.value;
|
|
1349
1365
|
}
|
|
1350
1366
|
}
|
|
@@ -283,13 +283,13 @@ export function supportsObjectType(schema, objectType) {
|
|
|
283
283
|
export function unitFamilyAllowsUnit(family, unit) {
|
|
284
284
|
switch (family) {
|
|
285
285
|
case "distance":
|
|
286
|
-
return unit === null || ["au", "km", "re", "sol"].includes(unit);
|
|
286
|
+
return unit === null || ["au", "km", "m", "ly", "pc", "kpc", "re", "sol"].includes(unit);
|
|
287
287
|
case "radius":
|
|
288
|
-
return unit === null || ["km", "re", "sol"].includes(unit);
|
|
288
|
+
return unit === null || ["km", "m", "re", "rj", "sol"].includes(unit);
|
|
289
289
|
case "mass":
|
|
290
|
-
return unit === null || ["me", "sol"].includes(unit);
|
|
290
|
+
return unit === null || ["me", "mj", "sol"].includes(unit);
|
|
291
291
|
case "duration":
|
|
292
|
-
return unit === null || ["h", "d", "y"].includes(unit);
|
|
292
|
+
return unit === null || ["s", "min", "h", "d", "y", "ky", "my", "gy"].includes(unit);
|
|
293
293
|
case "angle":
|
|
294
294
|
return unit === null || unit === "deg";
|
|
295
295
|
case "generic":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type WorldOrbitObjectType = "system" | "star" | "planet" | "moon" | "belt" | "asteroid" | "comet" | "ring" | "structure" | "phenomenon";
|
|
2
2
|
export type PlacementMode = "orbit" | "at" | "surface" | "free";
|
|
3
|
-
export type Unit = "au" | "km" | "re" | "sol" | "me" | "d" | "y" | "
|
|
3
|
+
export type Unit = "au" | "km" | "m" | "ly" | "pc" | "kpc" | "re" | "rj" | "sol" | "me" | "mj" | "s" | "min" | "h" | "d" | "y" | "ky" | "my" | "gy" | "K" | "deg";
|
|
4
4
|
export type WorldOrbitDocumentVersion = "1.0";
|
|
5
5
|
export type WorldOrbitAtlasDocumentVersion = "2.0";
|
|
6
6
|
export type WorldOrbitDraftDocumentVersion = "2.0-draft";
|
|
@@ -43,80 +43,80 @@ export function renderSceneToSvg(scene, options = {}) {
|
|
|
43
43
|
.join("")
|
|
44
44
|
: "";
|
|
45
45
|
const metadataMarkup = layers.metadata
|
|
46
|
-
? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
|
|
47
|
-
<text class="wo-subtitle" x="56" y="88">${escapeXml(subtitle)}</text>
|
|
46
|
+
? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
|
|
47
|
+
<text class="wo-subtitle" x="56" y="88">${escapeXml(subtitle)}</text>
|
|
48
48
|
<text class="wo-meta" x="56" y="${scene.height - 42}">${escapeXml(renderMetadata(scene))}</text>`
|
|
49
49
|
: "";
|
|
50
50
|
const backgroundMarkup = layers.background
|
|
51
|
-
? `<rect class="wo-bg" x="0" y="0" width="${scene.width}" height="${scene.height}" rx="28" ry="28" />
|
|
52
|
-
<rect class="wo-bg-glow" x="0" y="0" width="${scene.width}" height="${scene.height}" rx="28" ry="28" />
|
|
51
|
+
? `<rect class="wo-bg" x="0" y="0" width="${scene.width}" height="${scene.height}" rx="28" ry="28" />
|
|
52
|
+
<rect class="wo-bg-glow" x="0" y="0" width="${scene.width}" height="${scene.height}" rx="28" ry="28" />
|
|
53
53
|
${layers.guides ? renderBackdrop(scene.width, scene.height) : ""}`
|
|
54
54
|
: "";
|
|
55
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" data-worldorbit-svg="true" width="${scene.width}" height="${scene.height}" viewBox="0 0 ${scene.width} ${scene.height}" preserveAspectRatio="xMidYMid meet" role="img" aria-labelledby="worldorbit-title worldorbit-desc">
|
|
56
|
-
<title id="worldorbit-title">${escapeXml(scene.title)}</title>
|
|
57
|
-
<desc id="worldorbit-desc">A ${escapeXml(scene.viewMode)} WorldOrbit render with ${visibleObjects.length} visible objects.</desc>
|
|
58
|
-
<defs>
|
|
59
|
-
<linearGradient id="wo-bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
60
|
-
<stop offset="0%" stop-color="${theme.backgroundStart}" />
|
|
61
|
-
<stop offset="100%" stop-color="${theme.backgroundEnd}" />
|
|
62
|
-
</linearGradient>
|
|
63
|
-
<radialGradient id="wo-star-glow" cx="50%" cy="50%" r="50%">
|
|
64
|
-
<stop offset="0%" stop-color="${theme.starGlow}" stop-opacity="0.95" />
|
|
65
|
-
<stop offset="100%" stop-color="${theme.starCore}" stop-opacity="0" />
|
|
66
|
-
</radialGradient>
|
|
67
|
-
<radialGradient id="wo-bg-glow" cx="20%" cy="10%" r="90%">
|
|
68
|
-
<stop offset="0%" stop-color="${theme.backgroundGlow}" />
|
|
69
|
-
<stop offset="100%" stop-color="transparent" />
|
|
70
|
-
</radialGradient>
|
|
71
|
-
${imageDefinitions}
|
|
72
|
-
</defs>
|
|
73
|
-
<style>
|
|
74
|
-
.wo-bg { fill: url(#wo-bg); }
|
|
75
|
-
.wo-bg-glow { fill: url(#wo-bg-glow); }
|
|
76
|
-
.wo-grid { fill: none; stroke: ${theme.guide}; stroke-width: 1; }
|
|
77
|
-
.wo-orbit { fill: none; stroke: ${theme.orbit}; stroke-width: 1.5; }
|
|
78
|
-
.wo-orbit-back { opacity: 0.38; stroke-dasharray: 8 6; }
|
|
79
|
-
.wo-orbit-front { opacity: 0.9; }
|
|
80
|
-
.wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
|
|
81
|
-
.wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
|
|
82
|
-
.wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
|
|
83
|
-
.wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
|
|
84
|
-
.wo-title { fill: ${theme.ink}; font: 700 24px ${theme.displayFont}; letter-spacing: 0.06em; text-transform: uppercase; }
|
|
85
|
-
.wo-subtitle { fill: ${theme.muted}; font: 500 12px ${theme.fontFamily}; letter-spacing: 0.14em; text-transform: uppercase; }
|
|
86
|
-
.wo-meta { fill: ${theme.muted}; font: 500 11px ${theme.fontFamily}; letter-spacing: 0.06em; }
|
|
87
|
-
.wo-object { cursor: pointer; outline: none; }
|
|
88
|
-
.wo-object:focus-visible .wo-selection-ring,
|
|
89
|
-
.wo-object:hover .wo-selection-ring,
|
|
90
|
-
.wo-object-selected .wo-selection-ring { display: block; }
|
|
91
|
-
.wo-object-selected .wo-selection-ring { stroke: ${theme.selected}; }
|
|
92
|
-
.wo-object-label-selected .wo-label { fill: ${theme.accent}; }
|
|
93
|
-
.wo-object-label-selected .wo-label-secondary { fill: ${theme.selected}; }
|
|
94
|
-
.wo-chain-selected .wo-selection-ring,
|
|
95
|
-
.wo-chain-hover .wo-selection-ring { display: block; }
|
|
96
|
-
.wo-ancestor-selected .wo-selection-ring,
|
|
97
|
-
.wo-ancestor-hover .wo-selection-ring { display: block; opacity: 0.66; }
|
|
98
|
-
.wo-chain-selected .wo-label,
|
|
99
|
-
.wo-chain-hover .wo-label { fill: ${theme.accent}; }
|
|
100
|
-
.wo-ancestor-selected .wo-label,
|
|
101
|
-
.wo-ancestor-hover .wo-label { fill: ${theme.ink}; opacity: 0.82; }
|
|
102
|
-
.wo-orbit-related-selected,
|
|
103
|
-
.wo-orbit-related-hover { stroke: ${theme.accentStrong}; opacity: 1; }
|
|
104
|
-
.wo-selection-ring { display: none; fill: none; stroke-width: 2; stroke-dasharray: 6 5; }
|
|
105
|
-
.wo-object-image { pointer-events: none; }
|
|
106
|
-
</style>
|
|
107
|
-
${backgroundMarkup}
|
|
108
|
-
${metadataMarkup}
|
|
109
|
-
<g data-worldorbit-world="true">
|
|
110
|
-
<g data-worldorbit-camera-root="${WORLD_LAYER_ID}" id="${WORLD_LAYER_ID}">
|
|
111
|
-
<g data-worldorbit-world-content="true">
|
|
112
|
-
${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
|
|
113
|
-
${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
|
|
114
|
-
${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
|
|
115
|
-
${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
|
|
116
|
-
${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
|
|
117
|
-
</g>
|
|
118
|
-
</g>
|
|
119
|
-
</g>
|
|
55
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" data-worldorbit-svg="true" width="${scene.width}" height="${scene.height}" viewBox="0 0 ${scene.width} ${scene.height}" preserveAspectRatio="xMidYMid meet" role="img" aria-labelledby="worldorbit-title worldorbit-desc">
|
|
56
|
+
<title id="worldorbit-title">${escapeXml(scene.title)}</title>
|
|
57
|
+
<desc id="worldorbit-desc">A ${escapeXml(scene.viewMode)} WorldOrbit render with ${visibleObjects.length} visible objects.</desc>
|
|
58
|
+
<defs>
|
|
59
|
+
<linearGradient id="wo-bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
60
|
+
<stop offset="0%" stop-color="${theme.backgroundStart}" />
|
|
61
|
+
<stop offset="100%" stop-color="${theme.backgroundEnd}" />
|
|
62
|
+
</linearGradient>
|
|
63
|
+
<radialGradient id="wo-star-glow" cx="50%" cy="50%" r="50%">
|
|
64
|
+
<stop offset="0%" stop-color="${theme.starGlow}" stop-opacity="0.95" />
|
|
65
|
+
<stop offset="100%" stop-color="${theme.starCore}" stop-opacity="0" />
|
|
66
|
+
</radialGradient>
|
|
67
|
+
<radialGradient id="wo-bg-glow" cx="20%" cy="10%" r="90%">
|
|
68
|
+
<stop offset="0%" stop-color="${theme.backgroundGlow}" />
|
|
69
|
+
<stop offset="100%" stop-color="transparent" />
|
|
70
|
+
</radialGradient>
|
|
71
|
+
${imageDefinitions}
|
|
72
|
+
</defs>
|
|
73
|
+
<style>
|
|
74
|
+
.wo-bg { fill: url(#wo-bg); }
|
|
75
|
+
.wo-bg-glow { fill: url(#wo-bg-glow); }
|
|
76
|
+
.wo-grid { fill: none; stroke: ${theme.guide}; stroke-width: 1; }
|
|
77
|
+
.wo-orbit { fill: none; stroke: ${theme.orbit}; stroke-width: 1.5; }
|
|
78
|
+
.wo-orbit-back { opacity: 0.38; stroke-dasharray: 8 6; }
|
|
79
|
+
.wo-orbit-front { opacity: 0.9; }
|
|
80
|
+
.wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
|
|
81
|
+
.wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
|
|
82
|
+
.wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
|
|
83
|
+
.wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
|
|
84
|
+
.wo-title { fill: ${theme.ink}; font: 700 24px ${theme.displayFont}; letter-spacing: 0.06em; text-transform: uppercase; }
|
|
85
|
+
.wo-subtitle { fill: ${theme.muted}; font: 500 12px ${theme.fontFamily}; letter-spacing: 0.14em; text-transform: uppercase; }
|
|
86
|
+
.wo-meta { fill: ${theme.muted}; font: 500 11px ${theme.fontFamily}; letter-spacing: 0.06em; }
|
|
87
|
+
.wo-object { cursor: pointer; outline: none; }
|
|
88
|
+
.wo-object:focus-visible .wo-selection-ring,
|
|
89
|
+
.wo-object:hover .wo-selection-ring,
|
|
90
|
+
.wo-object-selected .wo-selection-ring { display: block; }
|
|
91
|
+
.wo-object-selected .wo-selection-ring { stroke: ${theme.selected}; }
|
|
92
|
+
.wo-object-label-selected .wo-label { fill: ${theme.accent}; }
|
|
93
|
+
.wo-object-label-selected .wo-label-secondary { fill: ${theme.selected}; }
|
|
94
|
+
.wo-chain-selected .wo-selection-ring,
|
|
95
|
+
.wo-chain-hover .wo-selection-ring { display: block; }
|
|
96
|
+
.wo-ancestor-selected .wo-selection-ring,
|
|
97
|
+
.wo-ancestor-hover .wo-selection-ring { display: block; opacity: 0.66; }
|
|
98
|
+
.wo-chain-selected .wo-label,
|
|
99
|
+
.wo-chain-hover .wo-label { fill: ${theme.accent}; }
|
|
100
|
+
.wo-ancestor-selected .wo-label,
|
|
101
|
+
.wo-ancestor-hover .wo-label { fill: ${theme.ink}; opacity: 0.82; }
|
|
102
|
+
.wo-orbit-related-selected,
|
|
103
|
+
.wo-orbit-related-hover { stroke: ${theme.accentStrong}; opacity: 1; }
|
|
104
|
+
.wo-selection-ring { display: none; fill: none; stroke-width: 2; stroke-dasharray: 6 5; }
|
|
105
|
+
.wo-object-image { pointer-events: none; }
|
|
106
|
+
</style>
|
|
107
|
+
${backgroundMarkup}
|
|
108
|
+
${metadataMarkup}
|
|
109
|
+
<g data-worldorbit-world="true">
|
|
110
|
+
<g data-worldorbit-camera-root="${WORLD_LAYER_ID}" id="${WORLD_LAYER_ID}">
|
|
111
|
+
<g data-worldorbit-world-content="true">
|
|
112
|
+
${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
|
|
113
|
+
${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
|
|
114
|
+
${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
|
|
115
|
+
${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
|
|
116
|
+
${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
|
|
117
|
+
</g>
|
|
118
|
+
</g>
|
|
119
|
+
</g>
|
|
120
120
|
</svg>`;
|
|
121
121
|
}
|
|
122
122
|
export function renderDocumentToSvg(document, options = {}) {
|
|
@@ -176,25 +176,28 @@ function buildImageDefinitions(objects) {
|
|
|
176
176
|
function renderSceneObject(sceneObject, selectedObjectId, theme) {
|
|
177
177
|
const { object, x, y, radius, visualRadius } = sceneObject;
|
|
178
178
|
const selectionClass = selectedObjectId === sceneObject.objectId ? " wo-object-selected" : "";
|
|
179
|
+
const kindClass = object.properties.kind
|
|
180
|
+
? ` wo-kind-${String(object.properties.kind).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`
|
|
181
|
+
: "";
|
|
179
182
|
const palette = resolveObjectPalette(sceneObject, theme);
|
|
180
183
|
const imageMarkup = renderObjectImage(sceneObject);
|
|
181
184
|
const outlineMarkup = imageMarkup
|
|
182
185
|
? renderObjectBody(object, x, y, radius, palette, { outlineOnly: true })
|
|
183
186
|
: "";
|
|
184
|
-
return `<g class="wo-object wo-object-${object.type}${selectionClass}" data-object-id="${escapeXml(sceneObject.objectId)}" data-parent-id="${escapeAttribute(sceneObject.parentId ?? "")}" data-group-id="${escapeAttribute(sceneObject.groupId ?? "")}" data-render-id="${escapeXml(sceneObject.renderId)}" tabindex="0" role="button" aria-label="${escapeXml(`${object.type} ${sceneObject.objectId}`)}">
|
|
185
|
-
<circle class="wo-selection-ring" cx="${x}" cy="${y}" r="${visualRadius + 8}" />
|
|
186
|
-
${renderAtmosphere(sceneObject, palette)}
|
|
187
|
-
${renderObjectBody(object, x, y, radius, palette)}
|
|
188
|
-
${imageMarkup}
|
|
189
|
-
${outlineMarkup}
|
|
187
|
+
return `<g class="wo-object wo-object-${object.type}${kindClass}${selectionClass}" data-object-id="${escapeXml(sceneObject.objectId)}" data-parent-id="${escapeAttribute(sceneObject.parentId ?? "")}" data-group-id="${escapeAttribute(sceneObject.groupId ?? "")}" data-render-id="${escapeXml(sceneObject.renderId)}" tabindex="0" role="button" aria-label="${escapeXml(`${object.type} ${sceneObject.objectId}`)}">
|
|
188
|
+
<circle class="wo-selection-ring" cx="${x}" cy="${y}" r="${visualRadius + 8}" />
|
|
189
|
+
${renderAtmosphere(sceneObject, palette)}
|
|
190
|
+
${renderObjectBody(object, x, y, radius, palette)}
|
|
191
|
+
${imageMarkup}
|
|
192
|
+
${outlineMarkup}
|
|
190
193
|
</g>`;
|
|
191
194
|
}
|
|
192
195
|
function renderSceneLabel(scene, label, selectedObjectId) {
|
|
193
196
|
const selectionClass = selectedObjectId === label.objectId ? " wo-object-label-selected" : "";
|
|
194
197
|
const labelScale = scene.scaleModel.labelMultiplier;
|
|
195
|
-
return `<g class="wo-object-label${selectionClass}" data-object-id="${escapeXml(label.objectId)}" data-group-id="${escapeAttribute(label.groupId ?? "")}" data-render-id="${escapeXml(label.renderId)}">
|
|
196
|
-
<text class="wo-label" x="${label.x}" y="${label.y}" text-anchor="${label.textAnchor}" font-size="${14 * labelScale}">${escapeXml(label.label)}</text>
|
|
197
|
-
<text class="wo-label-secondary" x="${label.x}" y="${label.secondaryY}" text-anchor="${label.textAnchor}" font-size="${11 * labelScale}">${escapeXml(label.secondaryLabel)}</text>
|
|
198
|
+
return `<g class="wo-object-label${selectionClass}" data-object-id="${escapeXml(label.objectId)}" data-group-id="${escapeAttribute(label.groupId ?? "")}" data-render-id="${escapeXml(label.renderId)}">
|
|
199
|
+
<text class="wo-label" x="${label.x}" y="${label.y}" text-anchor="${label.textAnchor}" font-size="${14 * labelScale}">${escapeXml(label.label)}</text>
|
|
200
|
+
<text class="wo-label-secondary" x="${label.x}" y="${label.secondaryY}" text-anchor="${label.textAnchor}" font-size="${11 * labelScale}">${escapeXml(label.secondaryLabel)}</text>
|
|
198
201
|
</g>`;
|
|
199
202
|
}
|
|
200
203
|
function renderObjectBody(object, x, y, radius, palette, options = {}) {
|
|
@@ -204,7 +207,7 @@ function renderObjectBody(object, x, y, radius, palette, options = {}) {
|
|
|
204
207
|
case "star":
|
|
205
208
|
return options.outlineOnly
|
|
206
209
|
? `<circle cx="${x}" cy="${y}" r="${radius}" fill="transparent" stroke="${palette.stroke}" stroke-width="2" />`
|
|
207
|
-
: `<circle cx="${x}" cy="${y}" r="${radius * 2.4}" fill="${palette.glow ?? "url(#wo-star-glow)"}" opacity="0.84" />
|
|
210
|
+
: `<circle cx="${x}" cy="${y}" r="${radius * 2.4}" fill="${palette.glow ?? "url(#wo-star-glow)"}" opacity="0.84" />
|
|
208
211
|
<circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="2" />`;
|
|
209
212
|
case "planet":
|
|
210
213
|
case "moon":
|
|
@@ -215,7 +218,7 @@ function renderObjectBody(object, x, y, radius, palette, options = {}) {
|
|
|
215
218
|
case "comet":
|
|
216
219
|
return options.outlineOnly
|
|
217
220
|
? `<circle cx="${x}" cy="${y}" r="${radius}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`
|
|
218
|
-
: `<path d="M ${x - radius * 2} ${y + radius * 1.3} Q ${x - radius * 0.7} ${y + radius * 0.3} ${x - radius * 0.45} ${y}" fill="none" stroke="${tail}" stroke-width="${Math.max(2, radius * 0.8)}" stroke-linecap="round" opacity="0.85" />
|
|
221
|
+
: `<path d="M ${x - radius * 2} ${y + radius * 1.3} Q ${x - radius * 0.7} ${y + radius * 0.3} ${x - radius * 0.45} ${y}" fill="none" stroke="${tail}" stroke-width="${Math.max(2, radius * 0.8)}" stroke-linecap="round" opacity="0.85" />
|
|
219
222
|
<circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
|
|
220
223
|
case "structure":
|
|
221
224
|
return `<polygon points="${diamondPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
|
|
@@ -334,7 +337,8 @@ function rectsOverlap(left, right) {
|
|
|
334
337
|
right.bottom < left.top);
|
|
335
338
|
}
|
|
336
339
|
function resolveObjectPalette(sceneObject, theme) {
|
|
337
|
-
const
|
|
340
|
+
const kind = String(sceneObject.object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
|
|
341
|
+
const base = basePaletteForType(sceneObject.object.type, kind, theme);
|
|
338
342
|
const customFill = sceneObject.fillColor && isColorLike(sceneObject.fillColor)
|
|
339
343
|
? sceneObject.fillColor
|
|
340
344
|
: base.fill;
|
|
@@ -356,7 +360,7 @@ function resolveObjectPalette(sceneObject, theme) {
|
|
|
356
360
|
: undefined,
|
|
357
361
|
};
|
|
358
362
|
}
|
|
359
|
-
function basePaletteForType(type, theme) {
|
|
363
|
+
function basePaletteForType(type, kind, theme) {
|
|
360
364
|
switch (type) {
|
|
361
365
|
case "star":
|
|
362
366
|
return {
|
|
@@ -378,8 +382,26 @@ function basePaletteForType(type, theme) {
|
|
|
378
382
|
case "structure":
|
|
379
383
|
return { fill: theme.accentStrong, stroke: "#fff2ea" };
|
|
380
384
|
case "phenomenon":
|
|
381
|
-
return
|
|
385
|
+
return kindPhenomenonPalette(kind);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function kindPhenomenonPalette(kind) {
|
|
389
|
+
if (kind === "galaxy") {
|
|
390
|
+
return { fill: "rgba(165,125,255,0.55)", stroke: "rgba(210,185,255,0.75)", halo: "rgba(160,120,255,0.10)", core: "#ede0ff" };
|
|
391
|
+
}
|
|
392
|
+
if (kind === "dwarf-galaxy") {
|
|
393
|
+
return { fill: "rgba(190,165,255,0.45)", stroke: "rgba(220,205,255,0.75)", core: "#ddd0ff" };
|
|
394
|
+
}
|
|
395
|
+
if (kind === "black-hole") {
|
|
396
|
+
return { fill: "#040408", stroke: "#ff6a00", accentRing: "rgba(255,140,20,0.72)" };
|
|
397
|
+
}
|
|
398
|
+
if (kind === "nebula") {
|
|
399
|
+
return { fill: "rgba(105,205,255,0.45)", stroke: "rgba(180,235,255,0.72)", halo: "rgba(100,200,255,0.08)" };
|
|
400
|
+
}
|
|
401
|
+
if (kind === "void") {
|
|
402
|
+
return { fill: "#05080f", stroke: "rgba(130,160,255,0.4)" };
|
|
382
403
|
}
|
|
404
|
+
return { fill: "#78ffd7", stroke: "#e9fff7" };
|
|
383
405
|
}
|
|
384
406
|
function applyTemperatureAndAlbedo(baseColor, temperature, albedo, type) {
|
|
385
407
|
let nextColor = baseColor;
|