worldorbit 2.5.2
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/LICENSE.md +5 -0
- package/README.md +250 -0
- package/dist/browser/core/dist/index.js +4009 -0
- package/dist/browser/markdown/dist/index.js +3951 -0
- package/dist/browser/viewer/dist/index.js +5981 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.js +84 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +16 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +25 -0
- package/dist/normalize.d.ts +2 -0
- package/dist/normalize.js +243 -0
- package/dist/parse.d.ts +2 -0
- package/dist/parse.js +126 -0
- package/dist/render.d.ts +6 -0
- package/dist/render.js +683 -0
- package/dist/tokenize.d.ts +4 -0
- package/dist/tokenize.js +68 -0
- package/dist/types.d.ts +208 -0
- package/dist/types.js +1 -0
- package/dist/unpkg/core/dist/index.js +4081 -0
- package/dist/unpkg/markdown/dist/index.js +3979 -0
- package/dist/unpkg/test.html +1 -0
- package/dist/unpkg/viewer/dist/index.js +6038 -0
- package/dist/unpkg/worldorbit-core.min.js +5 -0
- package/dist/unpkg/worldorbit-markdown.min.js +81 -0
- package/dist/unpkg/worldorbit-viewer.min.js +232 -0
- package/dist/unpkg/worldorbit.d.ts +2 -0
- package/dist/unpkg/worldorbit.js +2 -0
- package/dist/unpkg/worldorbit.min.js +236 -0
- package/dist/validate.d.ts +2 -0
- package/dist/validate.js +31 -0
- package/dist/viewer-state.d.ts +16 -0
- package/dist/viewer-state.js +130 -0
- package/dist/viewer.d.ts +2 -0
- package/dist/viewer.js +434 -0
- package/package.json +64 -0
- package/packages/core/README.md +13 -0
- package/packages/core/dist/atlas-edit.d.ts +11 -0
- package/packages/core/dist/atlas-edit.js +210 -0
- package/packages/core/dist/diagnostics.d.ts +10 -0
- package/packages/core/dist/diagnostics.js +109 -0
- package/packages/core/dist/draft-parse.d.ts +3 -0
- package/packages/core/dist/draft-parse.js +642 -0
- package/packages/core/dist/draft.d.ts +15 -0
- package/packages/core/dist/draft.js +343 -0
- package/packages/core/dist/errors.d.ts +7 -0
- package/packages/core/dist/errors.js +16 -0
- package/packages/core/dist/format.d.ts +4 -0
- package/packages/core/dist/format.js +364 -0
- package/packages/core/dist/index.d.ts +28 -0
- package/packages/core/dist/index.js +44 -0
- package/packages/core/dist/load.d.ts +4 -0
- package/packages/core/dist/load.js +130 -0
- package/packages/core/dist/markdown.d.ts +2 -0
- package/packages/core/dist/markdown.js +37 -0
- package/packages/core/dist/normalize.d.ts +2 -0
- package/packages/core/dist/normalize.js +304 -0
- package/packages/core/dist/parse.d.ts +2 -0
- package/packages/core/dist/parse.js +133 -0
- package/packages/core/dist/scene.d.ts +3 -0
- package/packages/core/dist/scene.js +1484 -0
- package/packages/core/dist/schema.d.ts +8 -0
- package/packages/core/dist/schema.js +298 -0
- package/packages/core/dist/tokenize.d.ts +4 -0
- package/packages/core/dist/tokenize.js +68 -0
- package/packages/core/dist/types.d.ts +382 -0
- package/packages/core/dist/types.js +1 -0
- package/packages/core/dist/validate.d.ts +2 -0
- package/packages/core/dist/validate.js +56 -0
- package/packages/editor/dist/editor.d.ts +2 -0
- package/packages/editor/dist/editor.js +2620 -0
- package/packages/editor/dist/index.d.ts +2 -0
- package/packages/editor/dist/index.js +1 -0
- package/packages/editor/dist/types.d.ts +53 -0
- package/packages/editor/dist/types.js +1 -0
- package/packages/markdown/README.md +9 -0
- package/packages/markdown/dist/html.d.ts +3 -0
- package/packages/markdown/dist/html.js +57 -0
- package/packages/markdown/dist/index.d.ts +4 -0
- package/packages/markdown/dist/index.js +3 -0
- package/packages/markdown/dist/rehype.d.ts +10 -0
- package/packages/markdown/dist/rehype.js +49 -0
- package/packages/markdown/dist/remark.d.ts +9 -0
- package/packages/markdown/dist/remark.js +28 -0
- package/packages/markdown/dist/types.d.ts +11 -0
- package/packages/markdown/dist/types.js +1 -0
- package/packages/viewer/README.md +12 -0
- package/packages/viewer/dist/atlas-state.d.ts +12 -0
- package/packages/viewer/dist/atlas-state.js +251 -0
- package/packages/viewer/dist/atlas-viewer.d.ts +2 -0
- package/packages/viewer/dist/atlas-viewer.js +448 -0
- package/packages/viewer/dist/custom-element.d.ts +1 -0
- package/packages/viewer/dist/custom-element.js +64 -0
- package/packages/viewer/dist/embed.d.ts +20 -0
- package/packages/viewer/dist/embed.js +138 -0
- package/packages/viewer/dist/index.d.ts +9 -0
- package/packages/viewer/dist/index.js +8 -0
- package/packages/viewer/dist/minimap.d.ts +3 -0
- package/packages/viewer/dist/minimap.js +63 -0
- package/packages/viewer/dist/render.d.ts +6 -0
- package/packages/viewer/dist/render.js +585 -0
- package/packages/viewer/dist/theme.d.ts +4 -0
- package/packages/viewer/dist/theme.js +98 -0
- package/packages/viewer/dist/tooltip.d.ts +3 -0
- package/packages/viewer/dist/tooltip.js +154 -0
- package/packages/viewer/dist/types.d.ts +256 -0
- package/packages/viewer/dist/types.js +1 -0
- package/packages/viewer/dist/viewer-state.d.ts +19 -0
- package/packages/viewer/dist/viewer-state.js +162 -0
- package/packages/viewer/dist/viewer.d.ts +2 -0
- package/packages/viewer/dist/viewer.js +1156 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { getThemePreset, resolveLayers, resolveTheme } from "./theme.js";
|
|
2
|
+
export { deserializeViewerAtlasState, normalizeViewerFilter, sceneViewpointToLayerOptions, searchSceneObjects, serializeViewerAtlasState, viewpointToViewerFilter, } from "./atlas-state.js";
|
|
3
|
+
export { DEFAULT_VIEWER_STATE, clampScale, composeViewerTransform, fitViewerState, focusViewerState, getViewerVisibleBounds, invertViewerPoint, getSceneCenter, normalizeRotation, panViewerState, rotateViewerState, zoomViewerStateAt, } from "./viewer-state.js";
|
|
4
|
+
export { WORLD_LAYER_ID, renderDocumentToSvg, renderSceneToSvg, renderSourceToSvg, } from "./render.js";
|
|
5
|
+
export { createEmbedPayload, createWorldOrbitEmbedMarkup, deserializeWorldOrbitEmbedPayload, mountWorldOrbitEmbeds, serializeWorldOrbitEmbedPayload, } from "./embed.js";
|
|
6
|
+
export { defineWorldOrbitViewerElement } from "./custom-element.js";
|
|
7
|
+
export { createAtlasViewer } from "./atlas-viewer.js";
|
|
8
|
+
export { createInteractiveViewer } from "./viewer.js";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getViewerVisibleBounds } from "./viewer-state.js";
|
|
2
|
+
const MINIMAP_WIDTH = 180;
|
|
3
|
+
const MINIMAP_HEIGHT = 120;
|
|
4
|
+
const MINIMAP_PADDING = 10;
|
|
5
|
+
export function renderViewerMinimap(scene, state, visibleObjects) {
|
|
6
|
+
const bounds = scene.contentBounds.width > 0 && scene.contentBounds.height > 0
|
|
7
|
+
? scene.contentBounds
|
|
8
|
+
: {
|
|
9
|
+
minX: 0,
|
|
10
|
+
minY: 0,
|
|
11
|
+
maxX: scene.width,
|
|
12
|
+
maxY: scene.height,
|
|
13
|
+
width: scene.width,
|
|
14
|
+
height: scene.height,
|
|
15
|
+
centerX: scene.width / 2,
|
|
16
|
+
centerY: scene.height / 2,
|
|
17
|
+
};
|
|
18
|
+
const scale = Math.min((MINIMAP_WIDTH - MINIMAP_PADDING * 2) / Math.max(bounds.width, 1), (MINIMAP_HEIGHT - MINIMAP_PADDING * 2) / Math.max(bounds.height, 1));
|
|
19
|
+
const translateX = (MINIMAP_WIDTH - bounds.width * scale) / 2 - bounds.minX * scale;
|
|
20
|
+
const translateY = (MINIMAP_HEIGHT - bounds.height * scale) / 2 - bounds.minY * scale;
|
|
21
|
+
const viewport = getViewerVisibleBounds(scene, state);
|
|
22
|
+
const objectsMarkup = visibleObjects
|
|
23
|
+
.map((object) => {
|
|
24
|
+
const x = object.x * scale + translateX;
|
|
25
|
+
const y = object.y * scale + translateY;
|
|
26
|
+
const radius = Math.max(1.4, Math.min(object.visualRadius * scale, 5.2));
|
|
27
|
+
const fill = object.fillColor ?? minimapColorForObject(object.object.type);
|
|
28
|
+
return `<circle cx="${formatNumber(x)}" cy="${formatNumber(y)}" r="${formatNumber(radius)}" fill="${fill}" fill-opacity="0.92" />`;
|
|
29
|
+
})
|
|
30
|
+
.join("");
|
|
31
|
+
return `<div data-worldorbit-minimap="true" style="position:absolute;right:16px;bottom:16px;width:${MINIMAP_WIDTH}px;height:${MINIMAP_HEIGHT}px;padding:8px;border-radius:16px;background:rgba(5, 14, 22, 0.78);border:1px solid rgba(179, 216, 255, 0.16);box-shadow:0 14px 28px rgba(0, 0, 0, 0.24);backdrop-filter:blur(8px);pointer-events:none;">
|
|
32
|
+
<svg width="${MINIMAP_WIDTH}" height="${MINIMAP_HEIGHT}" viewBox="0 0 ${MINIMAP_WIDTH} ${MINIMAP_HEIGHT}" role="presentation" aria-hidden="true">
|
|
33
|
+
<rect x="0.5" y="0.5" width="${MINIMAP_WIDTH - 1}" height="${MINIMAP_HEIGHT - 1}" rx="12" ry="12" fill="rgba(7, 17, 27, 0.85)" stroke="rgba(179, 216, 255, 0.18)" />
|
|
34
|
+
<rect x="${formatNumber(bounds.minX * scale + translateX)}" y="${formatNumber(bounds.minY * scale + translateY)}" width="${formatNumber(bounds.width * scale)}" height="${formatNumber(bounds.height * scale)}" rx="10" ry="10" fill="rgba(163, 209, 255, 0.04)" stroke="rgba(163, 209, 255, 0.16)" />
|
|
35
|
+
${objectsMarkup}
|
|
36
|
+
<rect x="${formatNumber(viewport.minX * scale + translateX)}" y="${formatNumber(viewport.minY * scale + translateY)}" width="${formatNumber(viewport.width * scale)}" height="${formatNumber(viewport.height * scale)}" rx="8" ry="8" fill="rgba(255, 180, 100, 0.09)" stroke="rgba(255, 180, 100, 0.88)" stroke-width="1.4" />
|
|
37
|
+
</svg>
|
|
38
|
+
</div>`;
|
|
39
|
+
}
|
|
40
|
+
function minimapColorForObject(type) {
|
|
41
|
+
switch (type) {
|
|
42
|
+
case "star":
|
|
43
|
+
return "#ffcc67";
|
|
44
|
+
case "planet":
|
|
45
|
+
return "#72b7ff";
|
|
46
|
+
case "moon":
|
|
47
|
+
return "#c7d7ea";
|
|
48
|
+
case "belt":
|
|
49
|
+
case "ring":
|
|
50
|
+
return "#d9aa74";
|
|
51
|
+
case "asteroid":
|
|
52
|
+
return "#a7a5b8";
|
|
53
|
+
case "comet":
|
|
54
|
+
return "#9ce7ff";
|
|
55
|
+
case "structure":
|
|
56
|
+
return "#ff7f5f";
|
|
57
|
+
case "phenomenon":
|
|
58
|
+
return "#78ffd7";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function formatNumber(value) {
|
|
62
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
|
63
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type RenderScene, type WorldOrbitDocument } from "@worldorbit/core";
|
|
2
|
+
import type { SvgRenderOptions } from "./types.js";
|
|
3
|
+
export declare const WORLD_LAYER_ID = "worldorbit-camera-root";
|
|
4
|
+
export declare function renderSceneToSvg(scene: RenderScene, options?: SvgRenderOptions): string;
|
|
5
|
+
export declare function renderDocumentToSvg(document: WorldOrbitDocument, options?: SvgRenderOptions): string;
|
|
6
|
+
export declare function renderSourceToSvg(source: string, options?: SvgRenderOptions): string;
|
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { loadWorldOrbitSource, renderDocumentToScene, } from "@worldorbit/core";
|
|
2
|
+
import { computeVisibleObjectIds } from "./atlas-state.js";
|
|
3
|
+
import { resolveLayers, resolveTheme } from "./theme.js";
|
|
4
|
+
export const WORLD_LAYER_ID = "worldorbit-camera-root";
|
|
5
|
+
export function renderSceneToSvg(scene, options = {}) {
|
|
6
|
+
const theme = resolveTheme(options.theme);
|
|
7
|
+
const presetDefaults = resolveRenderPreset(options.preset ?? scene.renderPreset ?? undefined);
|
|
8
|
+
const layers = resolveLayers({
|
|
9
|
+
...presetDefaults.layers,
|
|
10
|
+
...options.layers,
|
|
11
|
+
});
|
|
12
|
+
const subtitle = options.subtitle ?? scene.subtitle;
|
|
13
|
+
const visibleObjectIds = computeVisibleObjectIds(scene, options.filter ?? null);
|
|
14
|
+
const visibleObjects = scene.objects
|
|
15
|
+
.filter((object) => !object.hidden)
|
|
16
|
+
.filter((object) => visibleObjectIds.has(object.objectId))
|
|
17
|
+
.filter((object) => layers.structures || !isStructureLike(object.object))
|
|
18
|
+
.sort((left, right) => left.sortKey - right.sortKey);
|
|
19
|
+
const visibleLabels = scene.labels
|
|
20
|
+
.filter((label) => !label.hidden)
|
|
21
|
+
.filter((label) => visibleObjectIds.has(label.objectId))
|
|
22
|
+
.filter((label) => layers.structures || !isStructureLike(label.object));
|
|
23
|
+
const imageDefinitions = buildImageDefinitions(visibleObjects);
|
|
24
|
+
const orbitMarkup = layers.orbits
|
|
25
|
+
? renderOrbitLayer(scene, visibleObjectIds, layers.structures)
|
|
26
|
+
: { back: "", front: "" };
|
|
27
|
+
const leaderMarkup = layers.guides
|
|
28
|
+
? scene.leaders
|
|
29
|
+
.filter((leader) => !leader.hidden)
|
|
30
|
+
.filter((leader) => visibleObjectIds.has(leader.objectId))
|
|
31
|
+
.filter((leader) => layers.structures || !isStructureLike(leader.object))
|
|
32
|
+
.map((leader) => `<line class="wo-leader wo-leader-${leader.mode}" x1="${leader.x1}" y1="${leader.y1}" x2="${leader.x2}" y2="${leader.y2}" data-render-id="${escapeXml(leader.renderId)}" data-group-id="${escapeAttribute(leader.groupId ?? "")}" />`)
|
|
33
|
+
.join("")
|
|
34
|
+
: "";
|
|
35
|
+
const objectMarkup = layers.objects
|
|
36
|
+
? visibleObjects
|
|
37
|
+
.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme))
|
|
38
|
+
.join("")
|
|
39
|
+
: "";
|
|
40
|
+
const labelMarkup = layers.labels
|
|
41
|
+
? visibleLabels
|
|
42
|
+
.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null))
|
|
43
|
+
.join("")
|
|
44
|
+
: "";
|
|
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>
|
|
48
|
+
<text class="wo-meta" x="56" y="${scene.height - 42}">${escapeXml(renderMetadata(scene))}</text>`
|
|
49
|
+
: "";
|
|
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" />
|
|
53
|
+
${layers.guides ? renderBackdrop(scene.width, scene.height) : ""}`
|
|
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>
|
|
120
|
+
</svg>`;
|
|
121
|
+
}
|
|
122
|
+
export function renderDocumentToSvg(document, options = {}) {
|
|
123
|
+
return renderSceneToSvg(renderDocumentToScene(document, options), options);
|
|
124
|
+
}
|
|
125
|
+
export function renderSourceToSvg(source, options = {}) {
|
|
126
|
+
const loaded = loadWorldOrbitSource(source);
|
|
127
|
+
return renderDocumentToSvg(loaded.document, resolveSourceRenderOptions(loaded, options));
|
|
128
|
+
}
|
|
129
|
+
function resolveSourceRenderOptions(loaded, options) {
|
|
130
|
+
const atlasDocument = loaded.atlasDocument ?? loaded.draftDocument;
|
|
131
|
+
if (options.preset || !atlasDocument?.system?.defaults.preset) {
|
|
132
|
+
return options;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
...options,
|
|
136
|
+
preset: atlasDocument.system.defaults.preset,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function renderOrbitLayer(scene, visibleObjectIds, includeStructures) {
|
|
140
|
+
const backParts = [];
|
|
141
|
+
const frontParts = [];
|
|
142
|
+
for (const visual of scene.orbitVisuals.filter((entry) => !entry.hidden &&
|
|
143
|
+
visibleObjectIds.has(entry.objectId) &&
|
|
144
|
+
(includeStructures || !isStructureLike(entry.object)))) {
|
|
145
|
+
const strokeWidth = visual.bandThickness ?? (visual.band ? 10 : 1.5);
|
|
146
|
+
const orbitClass = visual.band ? "wo-orbit wo-orbit-band wo-orbit-node" : "wo-orbit wo-orbit-node";
|
|
147
|
+
const dataAttributes = `data-render-id="${escapeXml(visual.renderId)}" data-orbit-object-id="${escapeAttribute(visual.objectId)}" data-parent-id="${escapeAttribute(visual.parentId)}" data-group-id="${escapeAttribute(visual.groupId ?? "")}"`;
|
|
148
|
+
if (visual.backArcPath || visual.frontArcPath) {
|
|
149
|
+
if (visual.backArcPath) {
|
|
150
|
+
backParts.push(`<path class="${orbitClass} wo-orbit-back" d="${visual.backArcPath}" stroke-width="${strokeWidth}" ${dataAttributes} />`);
|
|
151
|
+
}
|
|
152
|
+
if (visual.frontArcPath) {
|
|
153
|
+
frontParts.push(`<path class="${orbitClass} wo-orbit-front" d="${visual.frontArcPath}" stroke-width="${strokeWidth}" ${dataAttributes} />`);
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (visual.kind === "ellipse") {
|
|
158
|
+
const rx = visual.rx ?? visual.radius ?? 0;
|
|
159
|
+
const ry = visual.ry ?? visual.radius ?? 0;
|
|
160
|
+
frontParts.push(`<ellipse class="${orbitClass} wo-orbit-front" cx="${visual.cx}" cy="${visual.cy}" rx="${rx}" ry="${ry}" transform="rotate(${visual.rotationDeg} ${visual.cx} ${visual.cy})" stroke-width="${strokeWidth}" ${dataAttributes} />`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
frontParts.push(`<circle class="${orbitClass} wo-orbit-front" cx="${visual.cx}" cy="${visual.cy}" r="${visual.radius ?? 0}" stroke-width="${strokeWidth}" ${dataAttributes} />`);
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
back: backParts.join(""),
|
|
167
|
+
front: frontParts.join(""),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function buildImageDefinitions(objects) {
|
|
171
|
+
return objects
|
|
172
|
+
.filter((object) => Boolean(object.imageHref))
|
|
173
|
+
.map((object) => renderImageClipPath(object))
|
|
174
|
+
.join("");
|
|
175
|
+
}
|
|
176
|
+
function renderSceneObject(sceneObject, selectedObjectId, theme) {
|
|
177
|
+
const { object, x, y, radius, visualRadius } = sceneObject;
|
|
178
|
+
const selectionClass = selectedObjectId === sceneObject.objectId ? " wo-object-selected" : "";
|
|
179
|
+
const palette = resolveObjectPalette(sceneObject, theme);
|
|
180
|
+
const imageMarkup = renderObjectImage(sceneObject);
|
|
181
|
+
const outlineMarkup = imageMarkup
|
|
182
|
+
? renderObjectBody(object, x, y, radius, palette, { outlineOnly: true })
|
|
183
|
+
: "";
|
|
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}
|
|
190
|
+
</g>`;
|
|
191
|
+
}
|
|
192
|
+
function renderSceneLabel(scene, label, selectedObjectId) {
|
|
193
|
+
const selectionClass = selectedObjectId === label.objectId ? " wo-object-label-selected" : "";
|
|
194
|
+
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
|
+
</g>`;
|
|
199
|
+
}
|
|
200
|
+
function renderObjectBody(object, x, y, radius, palette, options = {}) {
|
|
201
|
+
const fill = options.outlineOnly ? "transparent" : palette.fill;
|
|
202
|
+
const tail = palette.tail ?? palette.fill;
|
|
203
|
+
switch (object.type) {
|
|
204
|
+
case "star":
|
|
205
|
+
return options.outlineOnly
|
|
206
|
+
? `<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" />
|
|
208
|
+
<circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="2" />`;
|
|
209
|
+
case "planet":
|
|
210
|
+
case "moon":
|
|
211
|
+
case "belt":
|
|
212
|
+
case "asteroid":
|
|
213
|
+
case "ring":
|
|
214
|
+
return `<circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="${options.outlineOnly ? 1.5 : 1.4}" />`;
|
|
215
|
+
case "comet":
|
|
216
|
+
return options.outlineOnly
|
|
217
|
+
? `<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" />
|
|
219
|
+
<circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
|
|
220
|
+
case "structure":
|
|
221
|
+
return `<polygon points="${diamondPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
|
|
222
|
+
case "phenomenon":
|
|
223
|
+
return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function renderAtmosphere(sceneObject, palette) {
|
|
227
|
+
if (!palette.atmosphere) {
|
|
228
|
+
return "";
|
|
229
|
+
}
|
|
230
|
+
const { object, x, y, radius } = sceneObject;
|
|
231
|
+
if (object.type !== "planet" && object.type !== "moon" && object.type !== "asteroid") {
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
return `<circle cx="${x}" cy="${y}" r="${radius + 4}" fill="none" stroke="${palette.atmosphere}" stroke-width="4" opacity="0.45" />`;
|
|
235
|
+
}
|
|
236
|
+
function renderObjectImage(sceneObject) {
|
|
237
|
+
if (!sceneObject.imageHref) {
|
|
238
|
+
return "";
|
|
239
|
+
}
|
|
240
|
+
const bounds = imageBoundsForObject(sceneObject);
|
|
241
|
+
return `<image class="wo-object-image" x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" href="${escapeAttribute(sceneObject.imageHref)}" preserveAspectRatio="xMidYMid slice" clip-path="url(#${escapeAttribute(getObjectClipPathId(sceneObject))})" />`;
|
|
242
|
+
}
|
|
243
|
+
function renderImageClipPath(sceneObject) {
|
|
244
|
+
const { x, y, radius } = sceneObject;
|
|
245
|
+
let content = "";
|
|
246
|
+
switch (sceneObject.object.type) {
|
|
247
|
+
case "star":
|
|
248
|
+
case "planet":
|
|
249
|
+
case "moon":
|
|
250
|
+
case "asteroid":
|
|
251
|
+
case "comet":
|
|
252
|
+
content = `<circle cx="${x}" cy="${y}" r="${radius}" />`;
|
|
253
|
+
break;
|
|
254
|
+
case "structure":
|
|
255
|
+
content = `<polygon points="${diamondPoints(x, y, radius)}" />`;
|
|
256
|
+
break;
|
|
257
|
+
case "phenomenon":
|
|
258
|
+
content = `<polygon points="${phenomenonPoints(x, y, radius)}" />`;
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
return `<clipPath id="${escapeAttribute(getObjectClipPathId(sceneObject))}" clipPathUnits="userSpaceOnUse">${content}</clipPath>`;
|
|
264
|
+
}
|
|
265
|
+
function imageBoundsForObject(sceneObject) {
|
|
266
|
+
const { object, x, y, radius } = sceneObject;
|
|
267
|
+
switch (object.type) {
|
|
268
|
+
case "structure":
|
|
269
|
+
return {
|
|
270
|
+
x: x - radius,
|
|
271
|
+
y: y - radius,
|
|
272
|
+
width: radius * 2,
|
|
273
|
+
height: radius * 2,
|
|
274
|
+
};
|
|
275
|
+
case "phenomenon":
|
|
276
|
+
return {
|
|
277
|
+
x: x - radius * 1.2,
|
|
278
|
+
y: y - radius * 1.2,
|
|
279
|
+
width: radius * 2.4,
|
|
280
|
+
height: radius * 2.4,
|
|
281
|
+
};
|
|
282
|
+
default:
|
|
283
|
+
return {
|
|
284
|
+
x: x - radius,
|
|
285
|
+
y: y - radius,
|
|
286
|
+
width: radius * 2,
|
|
287
|
+
height: radius * 2,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function computeLabelPlacements(scene, objects) {
|
|
292
|
+
const placements = new Map();
|
|
293
|
+
const occupied = [];
|
|
294
|
+
const labelScale = scene.scaleModel.labelMultiplier;
|
|
295
|
+
for (const object of objects) {
|
|
296
|
+
const direction = object.y > scene.height * 0.62 ? -1 : 1;
|
|
297
|
+
const labelHalfWidth = estimateLabelHalfWidth(object, labelScale);
|
|
298
|
+
let labelY = object.y + direction * (object.radius + 18 * labelScale);
|
|
299
|
+
let secondaryY = labelY + direction * (16 * labelScale);
|
|
300
|
+
let rect = labelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
|
|
301
|
+
let attempts = 0;
|
|
302
|
+
while (occupied.some((entry) => rectsOverlap(entry, rect)) && attempts < 10) {
|
|
303
|
+
labelY += direction * 14 * labelScale;
|
|
304
|
+
secondaryY += direction * 14 * labelScale;
|
|
305
|
+
rect = labelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
|
|
306
|
+
attempts += 1;
|
|
307
|
+
}
|
|
308
|
+
occupied.push(rect);
|
|
309
|
+
placements.set(object.objectId, { labelY, secondaryY });
|
|
310
|
+
}
|
|
311
|
+
return placements;
|
|
312
|
+
}
|
|
313
|
+
function defaultLabelPlacement(scene, object) {
|
|
314
|
+
const direction = object.y > scene.height * 0.62 ? -1 : 1;
|
|
315
|
+
const labelScale = scene.scaleModel.labelMultiplier;
|
|
316
|
+
const labelY = object.y + direction * (object.radius + 18 * labelScale);
|
|
317
|
+
return {
|
|
318
|
+
labelY,
|
|
319
|
+
secondaryY: labelY + direction * (16 * labelScale),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function labelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
|
|
323
|
+
return {
|
|
324
|
+
left: x - labelHalfWidth,
|
|
325
|
+
right: x + labelHalfWidth,
|
|
326
|
+
top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
|
|
327
|
+
bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function rectsOverlap(left, right) {
|
|
331
|
+
return !(left.right < right.left ||
|
|
332
|
+
right.right < left.left ||
|
|
333
|
+
left.bottom < right.top ||
|
|
334
|
+
right.bottom < left.top);
|
|
335
|
+
}
|
|
336
|
+
function resolveObjectPalette(sceneObject, theme) {
|
|
337
|
+
const base = basePaletteForType(sceneObject.object.type, theme);
|
|
338
|
+
const customFill = sceneObject.fillColor && isColorLike(sceneObject.fillColor)
|
|
339
|
+
? sceneObject.fillColor
|
|
340
|
+
: base.fill;
|
|
341
|
+
const albedo = numericValue(sceneObject.object.properties.albedo);
|
|
342
|
+
const temperature = numericValue(sceneObject.object.properties.temperature);
|
|
343
|
+
const fill = applyTemperatureAndAlbedo(customFill, temperature, albedo, sceneObject.object.type);
|
|
344
|
+
const stroke = mixColors(fill, "#ffffff", 0.4) ?? base.stroke;
|
|
345
|
+
const atmosphere = atmosphereColor(sceneObject.object.properties.atmosphere);
|
|
346
|
+
const glow = sceneObject.object.type === "star"
|
|
347
|
+
? rgbaString(mixColors(fill, "#fff2cb", 0.4) ?? fill, 0.82)
|
|
348
|
+
: undefined;
|
|
349
|
+
return {
|
|
350
|
+
fill,
|
|
351
|
+
stroke,
|
|
352
|
+
glow,
|
|
353
|
+
atmosphere,
|
|
354
|
+
tail: sceneObject.object.type === "comet"
|
|
355
|
+
? rgbaString(mixColors(fill, "#ffffff", 0.5) ?? fill, 0.72)
|
|
356
|
+
: undefined,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function basePaletteForType(type, theme) {
|
|
360
|
+
switch (type) {
|
|
361
|
+
case "star":
|
|
362
|
+
return {
|
|
363
|
+
fill: theme.starCore,
|
|
364
|
+
stroke: theme.starStroke,
|
|
365
|
+
};
|
|
366
|
+
case "planet":
|
|
367
|
+
return { fill: "#72b7ff", stroke: "#d8efff" };
|
|
368
|
+
case "moon":
|
|
369
|
+
return { fill: "#c7d7ea", stroke: "#f2f8ff" };
|
|
370
|
+
case "belt":
|
|
371
|
+
return { fill: "#d9aa74", stroke: "#f7d5aa" };
|
|
372
|
+
case "asteroid":
|
|
373
|
+
return { fill: "#a7a5b8", stroke: "#ebe5ff" };
|
|
374
|
+
case "comet":
|
|
375
|
+
return { fill: "#9ce7ff", stroke: "#e7fbff" };
|
|
376
|
+
case "ring":
|
|
377
|
+
return { fill: "#e59f7d", stroke: "#fff0d3" };
|
|
378
|
+
case "structure":
|
|
379
|
+
return { fill: theme.accentStrong, stroke: "#fff2ea" };
|
|
380
|
+
case "phenomenon":
|
|
381
|
+
return { fill: "#78ffd7", stroke: "#e9fff7" };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function applyTemperatureAndAlbedo(baseColor, temperature, albedo, type) {
|
|
385
|
+
let nextColor = baseColor;
|
|
386
|
+
if (temperature !== null) {
|
|
387
|
+
const tint = temperatureTint(temperature, type);
|
|
388
|
+
nextColor = mixColors(nextColor, tint, type === "star" ? 0.42 : 0.2) ?? nextColor;
|
|
389
|
+
}
|
|
390
|
+
if (albedo !== null) {
|
|
391
|
+
const clamped = Math.min(Math.max(albedo, 0), 1);
|
|
392
|
+
nextColor =
|
|
393
|
+
mixColors(nextColor, clamped >= 0.5 ? "#ffffff" : "#0f1520", Math.abs(clamped - 0.5) * 0.7) ??
|
|
394
|
+
nextColor;
|
|
395
|
+
}
|
|
396
|
+
return nextColor;
|
|
397
|
+
}
|
|
398
|
+
function temperatureTint(value, type) {
|
|
399
|
+
if (type === "star") {
|
|
400
|
+
if (value >= 8_000)
|
|
401
|
+
return "#d7ebff";
|
|
402
|
+
if (value >= 6_000)
|
|
403
|
+
return "#fff3d8";
|
|
404
|
+
if (value >= 4_000)
|
|
405
|
+
return "#ffd39a";
|
|
406
|
+
return "#ff9d76";
|
|
407
|
+
}
|
|
408
|
+
if (value <= 180)
|
|
409
|
+
return "#8fd8ff";
|
|
410
|
+
if (value >= 600)
|
|
411
|
+
return "#ffb56e";
|
|
412
|
+
return "#fff1c4";
|
|
413
|
+
}
|
|
414
|
+
function atmosphereColor(value) {
|
|
415
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
const normalized = value.trim().toLowerCase();
|
|
419
|
+
if (normalized.includes("methane"))
|
|
420
|
+
return "rgba(134, 236, 255, 0.75)";
|
|
421
|
+
if (normalized.includes("nitrogen") || normalized.includes("oxygen") || normalized.includes("air")) {
|
|
422
|
+
return "rgba(122, 194, 255, 0.75)";
|
|
423
|
+
}
|
|
424
|
+
if (normalized.includes("carbon") || normalized.includes("co2")) {
|
|
425
|
+
return "rgba(255, 198, 138, 0.75)";
|
|
426
|
+
}
|
|
427
|
+
if (normalized.includes("sulfur")) {
|
|
428
|
+
return "rgba(255, 233, 138, 0.75)";
|
|
429
|
+
}
|
|
430
|
+
return "rgba(180, 222, 255, 0.68)";
|
|
431
|
+
}
|
|
432
|
+
function resolveRenderPreset(preset) {
|
|
433
|
+
switch (preset) {
|
|
434
|
+
case "presentation":
|
|
435
|
+
return {
|
|
436
|
+
layers: {
|
|
437
|
+
background: true,
|
|
438
|
+
guides: true,
|
|
439
|
+
orbits: true,
|
|
440
|
+
objects: true,
|
|
441
|
+
labels: true,
|
|
442
|
+
structures: true,
|
|
443
|
+
metadata: true,
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
case "atlas-card":
|
|
447
|
+
return {
|
|
448
|
+
layers: {
|
|
449
|
+
background: true,
|
|
450
|
+
guides: false,
|
|
451
|
+
orbits: true,
|
|
452
|
+
objects: true,
|
|
453
|
+
labels: true,
|
|
454
|
+
structures: true,
|
|
455
|
+
metadata: true,
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
case "markdown":
|
|
459
|
+
return {
|
|
460
|
+
layers: {
|
|
461
|
+
background: true,
|
|
462
|
+
guides: false,
|
|
463
|
+
orbits: true,
|
|
464
|
+
objects: true,
|
|
465
|
+
labels: true,
|
|
466
|
+
structures: true,
|
|
467
|
+
metadata: false,
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
case "diagram":
|
|
471
|
+
default:
|
|
472
|
+
return {
|
|
473
|
+
layers: {
|
|
474
|
+
background: true,
|
|
475
|
+
guides: true,
|
|
476
|
+
orbits: true,
|
|
477
|
+
objects: true,
|
|
478
|
+
labels: true,
|
|
479
|
+
structures: true,
|
|
480
|
+
metadata: true,
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function numericValue(value) {
|
|
486
|
+
if (typeof value === "number") {
|
|
487
|
+
return value;
|
|
488
|
+
}
|
|
489
|
+
if (value && typeof value === "object" && "value" in value) {
|
|
490
|
+
return value.value;
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
function estimateLabelHalfWidth(object, labelScale) {
|
|
495
|
+
const primaryWidth = object.label.length * 4.8 * labelScale + 18;
|
|
496
|
+
const secondaryWidth = object.secondaryLabel.length * 4.1 * labelScale + 18;
|
|
497
|
+
return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
|
|
498
|
+
}
|
|
499
|
+
function getObjectClipPathId(sceneObject) {
|
|
500
|
+
return `${sceneObject.renderId}-clip`;
|
|
501
|
+
}
|
|
502
|
+
function diamondPoints(x, y, radius) {
|
|
503
|
+
return `${x},${y - radius} ${x + radius},${y} ${x},${y + radius} ${x - radius},${y}`;
|
|
504
|
+
}
|
|
505
|
+
function phenomenonPoints(x, y, radius) {
|
|
506
|
+
return `${x},${y - radius * 1.2} ${x + radius * 0.9},${y - radius * 0.3} ${x + radius * 1.2},${y + radius * 0.8} ${x},${y + radius * 1.2} ${x - radius * 1.1},${y + radius * 0.3} ${x - radius * 0.8},${y - radius * 0.8}`;
|
|
507
|
+
}
|
|
508
|
+
function renderBackdrop(width, height) {
|
|
509
|
+
const verticalLines = Array.from({ length: 10 }, (_, index) => {
|
|
510
|
+
const x = 90 + index * ((width - 180) / 9);
|
|
511
|
+
return `<line class="wo-grid" x1="${x}" y1="120" x2="${x}" y2="${height - 70}" />`;
|
|
512
|
+
}).join("");
|
|
513
|
+
const horizontalLines = Array.from({ length: 6 }, (_, index) => {
|
|
514
|
+
const y = 150 + index * ((height - 240) / 5);
|
|
515
|
+
return `<line class="wo-grid" x1="56" y1="${y}" x2="${width - 56}" y2="${y}" />`;
|
|
516
|
+
}).join("");
|
|
517
|
+
return `${verticalLines}${horizontalLines}`;
|
|
518
|
+
}
|
|
519
|
+
function renderMetadata(scene) {
|
|
520
|
+
return [
|
|
521
|
+
`${scene.layoutPreset} layout`,
|
|
522
|
+
`${scene.viewMode} view`,
|
|
523
|
+
`${scene.renderPreset ?? "custom"} preset`,
|
|
524
|
+
`schema ${scene.metadata.version ?? "1.0"}`,
|
|
525
|
+
].join(" - ");
|
|
526
|
+
}
|
|
527
|
+
function isStructureLike(object) {
|
|
528
|
+
return object.type === "structure" || object.type === "phenomenon";
|
|
529
|
+
}
|
|
530
|
+
function mixColors(left, right, amount) {
|
|
531
|
+
const leftRgb = parseColor(left);
|
|
532
|
+
const rightRgb = parseColor(right);
|
|
533
|
+
if (!leftRgb || !rightRgb) {
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
const clamped = Math.min(Math.max(amount, 0), 1);
|
|
537
|
+
return rgbToHex({
|
|
538
|
+
r: Math.round(leftRgb.r + (rightRgb.r - leftRgb.r) * clamped),
|
|
539
|
+
g: Math.round(leftRgb.g + (rightRgb.g - leftRgb.g) * clamped),
|
|
540
|
+
b: Math.round(leftRgb.b + (rightRgb.b - leftRgb.b) * clamped),
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function rgbaString(color, alpha) {
|
|
544
|
+
const rgb = parseColor(color);
|
|
545
|
+
if (!rgb) {
|
|
546
|
+
return color;
|
|
547
|
+
}
|
|
548
|
+
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
|
|
549
|
+
}
|
|
550
|
+
function parseColor(value) {
|
|
551
|
+
const hex = value.trim();
|
|
552
|
+
if (/^#[0-9a-f]{6}$/i.test(hex)) {
|
|
553
|
+
return {
|
|
554
|
+
r: Number.parseInt(hex.slice(1, 3), 16),
|
|
555
|
+
g: Number.parseInt(hex.slice(3, 5), 16),
|
|
556
|
+
b: Number.parseInt(hex.slice(5, 7), 16),
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
if (/^#[0-9a-f]{3}$/i.test(hex)) {
|
|
560
|
+
return {
|
|
561
|
+
r: Number.parseInt(hex[1] + hex[1], 16),
|
|
562
|
+
g: Number.parseInt(hex[2] + hex[2], 16),
|
|
563
|
+
b: Number.parseInt(hex[3] + hex[3], 16),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
function rgbToHex(color) {
|
|
569
|
+
const part = (value) => value.toString(16).padStart(2, "0");
|
|
570
|
+
return `#${part(color.r)}${part(color.g)}${part(color.b)}`;
|
|
571
|
+
}
|
|
572
|
+
function isColorLike(value) {
|
|
573
|
+
return Boolean(value.trim());
|
|
574
|
+
}
|
|
575
|
+
function escapeXml(value) {
|
|
576
|
+
return value
|
|
577
|
+
.replaceAll("&", "&")
|
|
578
|
+
.replaceAll("<", "<")
|
|
579
|
+
.replaceAll(">", ">")
|
|
580
|
+
.replaceAll("\"", """)
|
|
581
|
+
.replaceAll("'", "'");
|
|
582
|
+
}
|
|
583
|
+
function escapeAttribute(value) {
|
|
584
|
+
return escapeXml(value);
|
|
585
|
+
}
|