worldorbit 2.5.7 → 2.5.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worldorbit",
3
- "version": "2.5.7",
3
+ "version": "2.5.8",
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
+ }
@@ -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 base = basePaletteForType(sceneObject.object.type, theme);
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 { fill: "#78ffd7", stroke: "#e9fff7" };
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;