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,130 @@
|
|
|
1
|
+
export const DEFAULT_VIEWER_STATE = {
|
|
2
|
+
scale: 1,
|
|
3
|
+
rotationDeg: 0,
|
|
4
|
+
translateX: 0,
|
|
5
|
+
translateY: 0,
|
|
6
|
+
selectedObjectId: null,
|
|
7
|
+
};
|
|
8
|
+
export function normalizeRotation(rotationDeg) {
|
|
9
|
+
let normalized = rotationDeg % 360;
|
|
10
|
+
if (normalized > 180) {
|
|
11
|
+
normalized -= 360;
|
|
12
|
+
}
|
|
13
|
+
if (normalized <= -180) {
|
|
14
|
+
normalized += 360;
|
|
15
|
+
}
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
export function clampScale(scale, constraints) {
|
|
19
|
+
return Math.min(Math.max(scale, constraints.minScale), constraints.maxScale);
|
|
20
|
+
}
|
|
21
|
+
export function panViewerState(state, dx, dy) {
|
|
22
|
+
return {
|
|
23
|
+
...state,
|
|
24
|
+
translateX: state.translateX + dx,
|
|
25
|
+
translateY: state.translateY + dy,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function rotateViewerState(state, deltaDeg) {
|
|
29
|
+
return {
|
|
30
|
+
...state,
|
|
31
|
+
rotationDeg: normalizeRotation(state.rotationDeg + deltaDeg),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function zoomViewerStateAt(scene, state, factor, anchor, constraints) {
|
|
35
|
+
if (!Number.isFinite(factor) || factor <= 0) {
|
|
36
|
+
return state;
|
|
37
|
+
}
|
|
38
|
+
const center = getSceneCenter(scene);
|
|
39
|
+
const nextScale = clampScale(state.scale * factor, constraints);
|
|
40
|
+
if (nextScale === state.scale) {
|
|
41
|
+
return state;
|
|
42
|
+
}
|
|
43
|
+
const zoomRatio = nextScale / state.scale;
|
|
44
|
+
const anchorDx = anchor.x - center.x;
|
|
45
|
+
const anchorDy = anchor.y - center.y;
|
|
46
|
+
return {
|
|
47
|
+
...state,
|
|
48
|
+
scale: nextScale,
|
|
49
|
+
translateX: (1 - zoomRatio) * anchorDx + zoomRatio * state.translateX,
|
|
50
|
+
translateY: (1 - zoomRatio) * anchorDy + zoomRatio * state.translateY,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function fitViewerState(scene, state, constraints) {
|
|
54
|
+
const center = getSceneCenter(scene);
|
|
55
|
+
const rotatedBounds = rotateBounds(scene.contentBounds, center, state.rotationDeg);
|
|
56
|
+
const availableWidth = Math.max(scene.width - constraints.fitPadding * 2, 1);
|
|
57
|
+
const availableHeight = Math.max(scene.height - constraints.fitPadding * 2, 1);
|
|
58
|
+
const safeWidth = Math.max(rotatedBounds.width, 1);
|
|
59
|
+
const safeHeight = Math.max(rotatedBounds.height, 1);
|
|
60
|
+
const nextScale = clampScale(Math.min(availableWidth / safeWidth, availableHeight / safeHeight), constraints);
|
|
61
|
+
const rotatedCenter = rotatePoint({
|
|
62
|
+
x: scene.contentBounds.centerX,
|
|
63
|
+
y: scene.contentBounds.centerY,
|
|
64
|
+
}, center, state.rotationDeg);
|
|
65
|
+
return {
|
|
66
|
+
...state,
|
|
67
|
+
scale: nextScale,
|
|
68
|
+
translateX: center.x - (center.x + (rotatedCenter.x - center.x) * nextScale),
|
|
69
|
+
translateY: center.y - (center.y + (rotatedCenter.y - center.y) * nextScale),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function focusViewerState(scene, state, objectId, constraints) {
|
|
73
|
+
const target = scene.objects.find((object) => object.objectId === objectId && !object.hidden);
|
|
74
|
+
if (!target) {
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
77
|
+
const center = getSceneCenter(scene);
|
|
78
|
+
const nextScale = clampScale(Math.max(state.scale, 1.8), constraints);
|
|
79
|
+
const rotatedPoint = rotatePoint({ x: target.x, y: target.y }, center, state.rotationDeg);
|
|
80
|
+
return {
|
|
81
|
+
...state,
|
|
82
|
+
scale: nextScale,
|
|
83
|
+
translateX: center.x - (center.x + (rotatedPoint.x - center.x) * nextScale),
|
|
84
|
+
translateY: center.y - (center.y + (rotatedPoint.y - center.y) * nextScale),
|
|
85
|
+
selectedObjectId: objectId,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function composeViewerTransform(scene, state) {
|
|
89
|
+
const center = getSceneCenter(scene);
|
|
90
|
+
return `translate(${state.translateX} ${state.translateY}) translate(${center.x} ${center.y}) rotate(${state.rotationDeg}) scale(${state.scale}) translate(${-center.x} ${-center.y})`;
|
|
91
|
+
}
|
|
92
|
+
export function getSceneCenter(scene) {
|
|
93
|
+
return {
|
|
94
|
+
x: scene.width / 2,
|
|
95
|
+
y: scene.height / 2,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function rotateBounds(bounds, center, rotationDeg) {
|
|
99
|
+
const corners = [
|
|
100
|
+
{ x: bounds.minX, y: bounds.minY },
|
|
101
|
+
{ x: bounds.maxX, y: bounds.minY },
|
|
102
|
+
{ x: bounds.maxX, y: bounds.maxY },
|
|
103
|
+
{ x: bounds.minX, y: bounds.maxY },
|
|
104
|
+
].map((corner) => rotatePoint(corner, center, rotationDeg));
|
|
105
|
+
const minX = Math.min(...corners.map((corner) => corner.x));
|
|
106
|
+
const minY = Math.min(...corners.map((corner) => corner.y));
|
|
107
|
+
const maxX = Math.max(...corners.map((corner) => corner.x));
|
|
108
|
+
const maxY = Math.max(...corners.map((corner) => corner.y));
|
|
109
|
+
return {
|
|
110
|
+
minX,
|
|
111
|
+
minY,
|
|
112
|
+
maxX,
|
|
113
|
+
maxY,
|
|
114
|
+
width: maxX - minX,
|
|
115
|
+
height: maxY - minY,
|
|
116
|
+
centerX: minX + (maxX - minX) / 2,
|
|
117
|
+
centerY: minY + (maxY - minY) / 2,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function rotatePoint(point, center, rotationDeg) {
|
|
121
|
+
const radians = (rotationDeg * Math.PI) / 180;
|
|
122
|
+
const cos = Math.cos(radians);
|
|
123
|
+
const sin = Math.sin(radians);
|
|
124
|
+
const dx = point.x - center.x;
|
|
125
|
+
const dy = point.y - center.y;
|
|
126
|
+
return {
|
|
127
|
+
x: center.x + dx * cos - dy * sin,
|
|
128
|
+
y: center.y + dx * sin + dy * cos,
|
|
129
|
+
};
|
|
130
|
+
}
|
package/dist/viewer.d.ts
ADDED
package/dist/viewer.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { normalizeDocument } from "./normalize.js";
|
|
2
|
+
import { parseWorldOrbit } from "./parse.js";
|
|
3
|
+
import { renderDocumentToScene, renderSceneToSvg } from "./render.js";
|
|
4
|
+
import { validateDocument } from "./validate.js";
|
|
5
|
+
import { DEFAULT_VIEWER_STATE, composeViewerTransform, fitViewerState, focusViewerState, panViewerState, rotateViewerState, zoomViewerStateAt, } from "./viewer-state.js";
|
|
6
|
+
const DEFAULT_VIEWER_LIMITS = {
|
|
7
|
+
minScale: 0.2,
|
|
8
|
+
maxScale: 8,
|
|
9
|
+
fitPadding: 48,
|
|
10
|
+
panStep: 40,
|
|
11
|
+
zoomStep: 1.2,
|
|
12
|
+
rotationStep: 15,
|
|
13
|
+
};
|
|
14
|
+
export function createInteractiveViewer(container, options) {
|
|
15
|
+
ensureBrowserEnvironment(container);
|
|
16
|
+
const inputCount = Number(Boolean(options.source)) + Number(Boolean(options.document)) + Number(Boolean(options.scene));
|
|
17
|
+
if (inputCount !== 1) {
|
|
18
|
+
throw new Error('Interactive viewer requires exactly one of "source", "document", or "scene".');
|
|
19
|
+
}
|
|
20
|
+
const constraints = {
|
|
21
|
+
minScale: options.minScale ?? DEFAULT_VIEWER_LIMITS.minScale,
|
|
22
|
+
maxScale: options.maxScale ?? DEFAULT_VIEWER_LIMITS.maxScale,
|
|
23
|
+
fitPadding: options.fitPadding ?? DEFAULT_VIEWER_LIMITS.fitPadding,
|
|
24
|
+
};
|
|
25
|
+
const behavior = {
|
|
26
|
+
keyboard: options.keyboard ?? true,
|
|
27
|
+
pointer: options.pointer ?? true,
|
|
28
|
+
touch: options.touch ?? true,
|
|
29
|
+
selection: options.selection ?? true,
|
|
30
|
+
panStep: options.panStep ?? DEFAULT_VIEWER_LIMITS.panStep,
|
|
31
|
+
zoomStep: options.zoomStep ?? DEFAULT_VIEWER_LIMITS.zoomStep,
|
|
32
|
+
rotationStep: options.rotationStep ?? DEFAULT_VIEWER_LIMITS.rotationStep,
|
|
33
|
+
};
|
|
34
|
+
const renderOptions = {
|
|
35
|
+
width: options.width,
|
|
36
|
+
height: options.height,
|
|
37
|
+
padding: options.padding,
|
|
38
|
+
};
|
|
39
|
+
const previousTabIndex = container.getAttribute("tabindex");
|
|
40
|
+
const previousTouchAction = container.style.touchAction;
|
|
41
|
+
let scene = resolveInitialScene(options, renderOptions);
|
|
42
|
+
let state = { ...DEFAULT_VIEWER_STATE };
|
|
43
|
+
let svgElement = null;
|
|
44
|
+
let cameraRoot = null;
|
|
45
|
+
let suppressClick = false;
|
|
46
|
+
let activePointerId = null;
|
|
47
|
+
let lastPointerPoint = null;
|
|
48
|
+
let dragDistance = 0;
|
|
49
|
+
let destroyed = false;
|
|
50
|
+
let touchPoints = new Map();
|
|
51
|
+
let touchGesture = null;
|
|
52
|
+
if (previousTabIndex === null) {
|
|
53
|
+
container.tabIndex = 0;
|
|
54
|
+
}
|
|
55
|
+
container.classList.add("wo-viewer-container");
|
|
56
|
+
container.style.touchAction = behavior.touch ? "none" : previousTouchAction;
|
|
57
|
+
const handleWheel = (event) => {
|
|
58
|
+
if (!behavior.pointer || destroyed) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
container.focus();
|
|
63
|
+
const anchor = getScenePointFromClient(event.clientX, event.clientY);
|
|
64
|
+
const factor = clampValue(Math.exp(-event.deltaY * 0.002), 0.6, 1.6);
|
|
65
|
+
updateState(zoomViewerStateAt(scene, state, factor, anchor, constraints));
|
|
66
|
+
};
|
|
67
|
+
const handlePointerDown = (event) => {
|
|
68
|
+
if (destroyed) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const isTouch = event.pointerType === "touch";
|
|
72
|
+
if ((isTouch && !behavior.touch) || (!isTouch && !behavior.pointer)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!isTouch && event.button !== 0) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
container.focus();
|
|
79
|
+
container.setPointerCapture?.(event.pointerId);
|
|
80
|
+
const point = getScenePointFromClient(event.clientX, event.clientY);
|
|
81
|
+
if (isTouch) {
|
|
82
|
+
touchPoints.set(event.pointerId, point);
|
|
83
|
+
if (touchPoints.size === 2) {
|
|
84
|
+
touchGesture = createTouchGestureState(state, touchPoints);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
activePointerId = event.pointerId;
|
|
89
|
+
lastPointerPoint = point;
|
|
90
|
+
dragDistance = 0;
|
|
91
|
+
suppressClick = false;
|
|
92
|
+
};
|
|
93
|
+
const handlePointerMove = (event) => {
|
|
94
|
+
if (destroyed) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const isTouch = event.pointerType === "touch";
|
|
98
|
+
if (isTouch) {
|
|
99
|
+
if (!behavior.touch || !touchPoints.has(event.pointerId)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
touchPoints.set(event.pointerId, getScenePointFromClient(event.clientX, event.clientY));
|
|
103
|
+
if (touchPoints.size === 2) {
|
|
104
|
+
if (!touchGesture) {
|
|
105
|
+
touchGesture = createTouchGestureState(state, touchPoints);
|
|
106
|
+
}
|
|
107
|
+
const current = getTouchCenterAndDistance(touchPoints);
|
|
108
|
+
const factor = current.distance / Math.max(touchGesture.startDistance, 1);
|
|
109
|
+
const zoomedState = zoomViewerStateAt(scene, touchGesture.startState, factor, touchGesture.startCenter, constraints);
|
|
110
|
+
const deltaX = current.center.x - touchGesture.startCenter.x;
|
|
111
|
+
const deltaY = current.center.y - touchGesture.startCenter.y;
|
|
112
|
+
updateState(panViewerState(zoomedState, deltaX, deltaY));
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!behavior.pointer || activePointerId !== event.pointerId || !lastPointerPoint) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const nextPoint = getScenePointFromClient(event.clientX, event.clientY);
|
|
120
|
+
const deltaX = nextPoint.x - lastPointerPoint.x;
|
|
121
|
+
const deltaY = nextPoint.y - lastPointerPoint.y;
|
|
122
|
+
dragDistance += Math.abs(deltaX) + Math.abs(deltaY);
|
|
123
|
+
lastPointerPoint = nextPoint;
|
|
124
|
+
if (dragDistance > 2) {
|
|
125
|
+
suppressClick = true;
|
|
126
|
+
}
|
|
127
|
+
updateState(panViewerState(state, deltaX, deltaY));
|
|
128
|
+
};
|
|
129
|
+
const handlePointerEnd = (event) => {
|
|
130
|
+
if (event.pointerType === "touch") {
|
|
131
|
+
touchPoints.delete(event.pointerId);
|
|
132
|
+
if (touchPoints.size < 2) {
|
|
133
|
+
touchGesture = null;
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (activePointerId === event.pointerId) {
|
|
138
|
+
activePointerId = null;
|
|
139
|
+
lastPointerPoint = null;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const handleClick = (event) => {
|
|
143
|
+
if (!behavior.selection || destroyed) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (suppressClick) {
|
|
147
|
+
suppressClick = false;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const target = event.target;
|
|
151
|
+
if (!(target instanceof Element)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const objectElement = target.closest("[data-object-id]");
|
|
155
|
+
if (!objectElement) {
|
|
156
|
+
applySelection(null);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
applySelection(objectElement.dataset.objectId ?? null);
|
|
160
|
+
};
|
|
161
|
+
const handleKeyDown = (event) => {
|
|
162
|
+
if (!behavior.keyboard || destroyed) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
switch (event.key) {
|
|
166
|
+
case "+":
|
|
167
|
+
case "=":
|
|
168
|
+
event.preventDefault();
|
|
169
|
+
api.zoomBy(behavior.zoomStep);
|
|
170
|
+
return;
|
|
171
|
+
case "-":
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
api.zoomBy(1 / behavior.zoomStep);
|
|
174
|
+
return;
|
|
175
|
+
case "ArrowLeft":
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
api.panBy(-behavior.panStep, 0);
|
|
178
|
+
return;
|
|
179
|
+
case "ArrowRight":
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
api.panBy(behavior.panStep, 0);
|
|
182
|
+
return;
|
|
183
|
+
case "ArrowUp":
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
api.panBy(0, -behavior.panStep);
|
|
186
|
+
return;
|
|
187
|
+
case "ArrowDown":
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
api.panBy(0, behavior.panStep);
|
|
190
|
+
return;
|
|
191
|
+
case "[":
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
api.rotateBy(-behavior.rotationStep);
|
|
194
|
+
return;
|
|
195
|
+
case "]":
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
api.rotateBy(behavior.rotationStep);
|
|
198
|
+
return;
|
|
199
|
+
case "f":
|
|
200
|
+
case "F":
|
|
201
|
+
event.preventDefault();
|
|
202
|
+
api.fitToSystem();
|
|
203
|
+
return;
|
|
204
|
+
case "0":
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
api.resetView();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
container.addEventListener("wheel", handleWheel, { passive: false });
|
|
211
|
+
container.addEventListener("pointerdown", handlePointerDown);
|
|
212
|
+
container.addEventListener("pointermove", handlePointerMove);
|
|
213
|
+
container.addEventListener("pointerup", handlePointerEnd);
|
|
214
|
+
container.addEventListener("pointercancel", handlePointerEnd);
|
|
215
|
+
container.addEventListener("click", handleClick);
|
|
216
|
+
container.addEventListener("keydown", handleKeyDown);
|
|
217
|
+
const api = {
|
|
218
|
+
setSource(source) {
|
|
219
|
+
scene = renderDocumentToScene(parseSource(source), renderOptions);
|
|
220
|
+
rerenderScene(true);
|
|
221
|
+
},
|
|
222
|
+
setDocument(document) {
|
|
223
|
+
scene = renderDocumentToScene(document, renderOptions);
|
|
224
|
+
rerenderScene(true);
|
|
225
|
+
},
|
|
226
|
+
setScene(nextScene) {
|
|
227
|
+
scene = nextScene;
|
|
228
|
+
rerenderScene(true);
|
|
229
|
+
},
|
|
230
|
+
getState() {
|
|
231
|
+
return { ...state };
|
|
232
|
+
},
|
|
233
|
+
setState(nextState) {
|
|
234
|
+
updateState(sanitizeState({ ...state, ...nextState }));
|
|
235
|
+
},
|
|
236
|
+
zoomBy(factor, anchor) {
|
|
237
|
+
updateState(zoomViewerStateAt(scene, state, factor, anchor ?? { x: scene.width / 2, y: scene.height / 2 }, constraints));
|
|
238
|
+
},
|
|
239
|
+
panBy(dx, dy) {
|
|
240
|
+
updateState(panViewerState(state, dx, dy));
|
|
241
|
+
},
|
|
242
|
+
rotateBy(deg) {
|
|
243
|
+
updateState(rotateViewerState(state, deg));
|
|
244
|
+
},
|
|
245
|
+
fitToSystem() {
|
|
246
|
+
updateState(fitViewerState(scene, state, constraints));
|
|
247
|
+
},
|
|
248
|
+
focusObject(id) {
|
|
249
|
+
updateState(focusViewerState(scene, state, id, constraints));
|
|
250
|
+
applySelection(id);
|
|
251
|
+
},
|
|
252
|
+
resetView() {
|
|
253
|
+
const resetState = fitViewerState(scene, { ...DEFAULT_VIEWER_STATE }, constraints);
|
|
254
|
+
updateState(resetState);
|
|
255
|
+
applySelection(null);
|
|
256
|
+
},
|
|
257
|
+
destroy() {
|
|
258
|
+
if (destroyed) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
destroyed = true;
|
|
262
|
+
container.removeEventListener("wheel", handleWheel);
|
|
263
|
+
container.removeEventListener("pointerdown", handlePointerDown);
|
|
264
|
+
container.removeEventListener("pointermove", handlePointerMove);
|
|
265
|
+
container.removeEventListener("pointerup", handlePointerEnd);
|
|
266
|
+
container.removeEventListener("pointercancel", handlePointerEnd);
|
|
267
|
+
container.removeEventListener("click", handleClick);
|
|
268
|
+
container.removeEventListener("keydown", handleKeyDown);
|
|
269
|
+
container.classList.remove("wo-viewer-container");
|
|
270
|
+
container.style.touchAction = previousTouchAction;
|
|
271
|
+
if (previousTabIndex === null) {
|
|
272
|
+
container.removeAttribute("tabindex");
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
container.setAttribute("tabindex", previousTabIndex);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
rerenderScene(true);
|
|
280
|
+
return api;
|
|
281
|
+
function rerenderScene(resetView) {
|
|
282
|
+
container.innerHTML = renderSceneToSvg(scene);
|
|
283
|
+
svgElement = container.querySelector("svg");
|
|
284
|
+
cameraRoot = container.querySelector(`#worldorbit-camera-root`);
|
|
285
|
+
if (!svgElement || !cameraRoot) {
|
|
286
|
+
throw new Error("Interactive viewer could not locate the rendered SVG camera root.");
|
|
287
|
+
}
|
|
288
|
+
state = resetView
|
|
289
|
+
? fitViewerState(scene, { ...DEFAULT_VIEWER_STATE }, constraints)
|
|
290
|
+
: sanitizeState(state);
|
|
291
|
+
applySelection(state.selectedObjectId &&
|
|
292
|
+
scene.objects.some((object) => object.objectId === state.selectedObjectId && !object.hidden)
|
|
293
|
+
? state.selectedObjectId
|
|
294
|
+
: null, false);
|
|
295
|
+
updateCameraTransform();
|
|
296
|
+
options.onViewChange?.({ ...state });
|
|
297
|
+
}
|
|
298
|
+
function updateState(nextState) {
|
|
299
|
+
state = sanitizeState(nextState);
|
|
300
|
+
updateCameraTransform();
|
|
301
|
+
options.onViewChange?.({ ...state });
|
|
302
|
+
}
|
|
303
|
+
function sanitizeState(nextState) {
|
|
304
|
+
return {
|
|
305
|
+
scale: clampValue(nextState.scale, constraints.minScale, constraints.maxScale),
|
|
306
|
+
rotationDeg: normalizeRotation(nextState.rotationDeg),
|
|
307
|
+
translateX: Number.isFinite(nextState.translateX) ? nextState.translateX : state.translateX,
|
|
308
|
+
translateY: Number.isFinite(nextState.translateY) ? nextState.translateY : state.translateY,
|
|
309
|
+
selectedObjectId: nextState.selectedObjectId &&
|
|
310
|
+
scene.objects.some((object) => object.objectId === nextState.selectedObjectId && !object.hidden)
|
|
311
|
+
? nextState.selectedObjectId
|
|
312
|
+
: null,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function updateCameraTransform() {
|
|
316
|
+
if (!cameraRoot) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
cameraRoot.setAttribute("transform", composeViewerTransform(scene, state));
|
|
320
|
+
}
|
|
321
|
+
function applySelection(objectId, emitCallback = true) {
|
|
322
|
+
if (state.selectedObjectId) {
|
|
323
|
+
const previous = container.querySelector(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`);
|
|
324
|
+
previous?.classList.remove("wo-object-selected");
|
|
325
|
+
}
|
|
326
|
+
state = {
|
|
327
|
+
...state,
|
|
328
|
+
selectedObjectId: objectId &&
|
|
329
|
+
scene.objects.some((object) => object.objectId === objectId && !object.hidden)
|
|
330
|
+
? objectId
|
|
331
|
+
: null,
|
|
332
|
+
};
|
|
333
|
+
if (state.selectedObjectId) {
|
|
334
|
+
const next = container.querySelector(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`);
|
|
335
|
+
next?.classList.add("wo-object-selected");
|
|
336
|
+
}
|
|
337
|
+
if (emitCallback) {
|
|
338
|
+
options.onSelectionChange?.(getSelectedObject());
|
|
339
|
+
options.onViewChange?.({ ...state });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function getSelectedObject() {
|
|
343
|
+
return (scene.objects.find((object) => object.objectId === state.selectedObjectId && !object.hidden) ?? null);
|
|
344
|
+
}
|
|
345
|
+
function getScenePointFromClient(clientX, clientY) {
|
|
346
|
+
if (!svgElement) {
|
|
347
|
+
return {
|
|
348
|
+
x: scene.width / 2,
|
|
349
|
+
y: scene.height / 2,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const rect = svgElement.getBoundingClientRect();
|
|
353
|
+
if (!rect.width || !rect.height) {
|
|
354
|
+
return {
|
|
355
|
+
x: scene.width / 2,
|
|
356
|
+
y: scene.height / 2,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
x: ((clientX - rect.left) / rect.width) * scene.width,
|
|
361
|
+
y: ((clientY - rect.top) / rect.height) * scene.height,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function resolveInitialScene(options, renderOptions) {
|
|
366
|
+
if (options.scene) {
|
|
367
|
+
return options.scene;
|
|
368
|
+
}
|
|
369
|
+
if (options.document) {
|
|
370
|
+
return renderDocumentToScene(options.document, renderOptions);
|
|
371
|
+
}
|
|
372
|
+
if (options.source) {
|
|
373
|
+
return renderDocumentToScene(parseSource(options.source), renderOptions);
|
|
374
|
+
}
|
|
375
|
+
throw new Error("Interactive viewer requires an initial render input.");
|
|
376
|
+
}
|
|
377
|
+
function parseSource(source) {
|
|
378
|
+
const ast = parseWorldOrbit(source);
|
|
379
|
+
const document = normalizeDocument(ast);
|
|
380
|
+
validateDocument(document);
|
|
381
|
+
return document;
|
|
382
|
+
}
|
|
383
|
+
function createTouchGestureState(state, touchPoints) {
|
|
384
|
+
const { center, distance } = getTouchCenterAndDistance(touchPoints);
|
|
385
|
+
return {
|
|
386
|
+
startState: { ...state },
|
|
387
|
+
startCenter: center,
|
|
388
|
+
startDistance: distance,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function getTouchCenterAndDistance(touchPoints) {
|
|
392
|
+
const points = [...touchPoints.values()];
|
|
393
|
+
if (points.length < 2) {
|
|
394
|
+
return {
|
|
395
|
+
center: points[0] ?? { x: 0, y: 0 },
|
|
396
|
+
distance: 1,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
const [first, second] = points;
|
|
400
|
+
return {
|
|
401
|
+
center: {
|
|
402
|
+
x: (first.x + second.x) / 2,
|
|
403
|
+
y: (first.y + second.y) / 2,
|
|
404
|
+
},
|
|
405
|
+
distance: Math.hypot(second.x - first.x, second.y - first.y),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function ensureBrowserEnvironment(container) {
|
|
409
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
410
|
+
throw new Error("createInteractiveViewer can only run in a browser environment.");
|
|
411
|
+
}
|
|
412
|
+
if (!(container instanceof HTMLElement)) {
|
|
413
|
+
throw new Error("Interactive viewer requires an HTMLElement container.");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function clampValue(value, min, max) {
|
|
417
|
+
return Math.min(Math.max(value, min), max);
|
|
418
|
+
}
|
|
419
|
+
function normalizeRotation(rotationDeg) {
|
|
420
|
+
let normalized = rotationDeg % 360;
|
|
421
|
+
if (normalized > 180) {
|
|
422
|
+
normalized -= 360;
|
|
423
|
+
}
|
|
424
|
+
if (normalized <= -180) {
|
|
425
|
+
normalized += 360;
|
|
426
|
+
}
|
|
427
|
+
return normalized;
|
|
428
|
+
}
|
|
429
|
+
function cssEscape(value) {
|
|
430
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
|
|
431
|
+
return CSS.escape(value);
|
|
432
|
+
}
|
|
433
|
+
return value.replace(/["\\]/g, "\\$&");
|
|
434
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "worldorbit",
|
|
3
|
+
"version": "2.5.2",
|
|
4
|
+
"description": "A text-based DSL and parser pipeline for orbital worldbuilding",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/unpkg/worldorbit.js",
|
|
7
|
+
"unpkg": "./dist/unpkg/worldorbit.min.js",
|
|
8
|
+
"jsdelivr": "./dist/unpkg/worldorbit.min.js",
|
|
9
|
+
"types": "./dist/unpkg/worldorbit.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/unpkg/worldorbit.d.ts",
|
|
13
|
+
"import": "./dist/unpkg/worldorbit.js"
|
|
14
|
+
},
|
|
15
|
+
"./min": "./dist/unpkg/worldorbit.min.js",
|
|
16
|
+
"./core": {
|
|
17
|
+
"types": "./packages/core/dist/index.d.ts",
|
|
18
|
+
"import": "./packages/core/dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./viewer": {
|
|
21
|
+
"types": "./packages/viewer/dist/index.d.ts",
|
|
22
|
+
"import": "./packages/viewer/dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./markdown": {
|
|
25
|
+
"types": "./packages/markdown/dist/index.d.ts",
|
|
26
|
+
"import": "./packages/markdown/dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./editor": {
|
|
29
|
+
"types": "./packages/editor/dist/index.d.ts",
|
|
30
|
+
"import": "./packages/editor/dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"packages/core/dist",
|
|
36
|
+
"packages/viewer/dist",
|
|
37
|
+
"packages/markdown/dist",
|
|
38
|
+
"packages/editor/dist",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"keywords": [
|
|
43
|
+
"worldbuilding",
|
|
44
|
+
"orbit",
|
|
45
|
+
"dsl",
|
|
46
|
+
"parser"
|
|
47
|
+
],
|
|
48
|
+
"author": "Hanjo Winter",
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "node ./scripts/build.mjs",
|
|
52
|
+
"test": "npm run build && node --test test/*.test.js",
|
|
53
|
+
"check": "npm run test"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"esbuild-wasm": "^0.27.4",
|
|
57
|
+
"jsdom": "^26.0.0",
|
|
58
|
+
"rehype-stringify": "^10.0.1",
|
|
59
|
+
"remark-parse": "^11.0.0",
|
|
60
|
+
"remark-rehype": "^11.1.2",
|
|
61
|
+
"typescript": "^5.6.0",
|
|
62
|
+
"unified": "^11.0.5"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @worldorbit/core
|
|
2
|
+
|
|
3
|
+
WorldOrbit core contains the parser, schema, normalization, validation, formatting, and scene generation APIs.
|
|
4
|
+
|
|
5
|
+
Main exports:
|
|
6
|
+
|
|
7
|
+
- `parse(source)`
|
|
8
|
+
- `parseWorldOrbit(source)`
|
|
9
|
+
- `normalizeDocument(ast)`
|
|
10
|
+
- `validateDocument(document)`
|
|
11
|
+
- `renderDocumentToScene(document, options?)`
|
|
12
|
+
- `formatDocument(document)`
|
|
13
|
+
- `extractWorldOrbitBlocks(markdown)`
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AtlasDocumentPath, AtlasResolvedDiagnostic, WorldOrbitAtlasDocument, WorldOrbitDiagnostic } from "./types.js";
|
|
2
|
+
export declare function createEmptyAtlasDocument(systemId?: string): WorldOrbitAtlasDocument;
|
|
3
|
+
export declare function cloneAtlasDocument(document: WorldOrbitAtlasDocument): WorldOrbitAtlasDocument;
|
|
4
|
+
export declare function listAtlasDocumentPaths(document: WorldOrbitAtlasDocument): AtlasDocumentPath[];
|
|
5
|
+
export declare function getAtlasDocumentNode(document: WorldOrbitAtlasDocument, path: AtlasDocumentPath): unknown;
|
|
6
|
+
export declare function upsertAtlasDocumentNode(document: WorldOrbitAtlasDocument, path: AtlasDocumentPath, value: unknown): WorldOrbitAtlasDocument;
|
|
7
|
+
export declare function updateAtlasDocumentNode(document: WorldOrbitAtlasDocument, path: AtlasDocumentPath, updater: (value: unknown) => unknown): WorldOrbitAtlasDocument;
|
|
8
|
+
export declare function removeAtlasDocumentNode(document: WorldOrbitAtlasDocument, path: AtlasDocumentPath): WorldOrbitAtlasDocument;
|
|
9
|
+
export declare function resolveAtlasDiagnostics(document: WorldOrbitAtlasDocument, diagnostics: WorldOrbitDiagnostic[]): AtlasResolvedDiagnostic[];
|
|
10
|
+
export declare function resolveAtlasDiagnosticPath(document: WorldOrbitAtlasDocument, diagnostic: WorldOrbitDiagnostic): AtlasDocumentPath | null;
|
|
11
|
+
export declare function validateAtlasDocumentWithDiagnostics(document: WorldOrbitAtlasDocument): AtlasResolvedDiagnostic[];
|