@found-in-space/skykit 0.2.0-alpha.1 → 0.2.0-dev.20260527.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -6
- package/examples/custom-object-layer/custom-object-layer.js +1 -24
- package/examples/xr-free-roam/index.html +62 -4
- package/examples/xr-free-roam/xr-free-roam.css +249 -18
- package/examples/xr-free-roam/xr-free-roam.js +644 -217
- package/package.json +31 -5
- package/src/__tests__/skykit-anchored-images.test.js +32 -4
- package/src/__tests__/skykit-browser.test.js +217 -0
- package/src/__tests__/skykit-data.test.js +131 -0
- package/src/__tests__/skykit-parallax.test.js +4 -4
- package/src/__tests__/skykit-touch-os.test.js +71 -0
- package/src/__tests__/skykit-xr.test.js +123 -2
- package/src/__tests__/skykit.test.js +138 -1
- package/src/anchored-images.js +14 -15
- package/src/browser-addons.d.ts +16 -0
- package/src/browser-addons.js +155 -0
- package/src/browser-constellations.d.ts +13 -0
- package/src/browser-constellations.js +387 -0
- package/src/browser-journey.d.ts +8 -0
- package/src/browser-journey.js +240 -0
- package/src/browser.d.ts +98 -0
- package/src/browser.js +215 -13
- package/src/data.d.ts +133 -0
- package/src/data.js +447 -0
- package/src/embed.d.ts +5 -0
- package/src/embed.js +52 -2
- package/src/hr-diagram.js +23 -5
- package/src/index.d.ts +32 -7
- package/src/plugins.js +87 -43
- package/src/story.d.ts +57 -0
- package/src/story.js +396 -0
- package/src/three-shim.d.ts +32 -0
- package/src/touch-os.d.ts +70 -0
- package/src/touch-os.js +275 -0
- package/src/utils.js +96 -6
- package/src/viewer-entry.d.ts +10 -0
- package/src/viewer-entry.js +4 -0
- package/src/viewer.js +110 -12
- package/src/xr/plugins.js +224 -13
- package/src/xr/session.js +60 -14
- package/src/xr.d.ts +22 -0
- package/src/xr.js +1 -0
package/src/touch-os.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
|
+
createAppShell,
|
|
2
3
|
createButton,
|
|
3
4
|
createColumn,
|
|
4
5
|
createDPad,
|
|
5
6
|
createDockLayout,
|
|
6
7
|
createHoldButton,
|
|
8
|
+
createNode,
|
|
9
|
+
createRect,
|
|
7
10
|
createRuntime,
|
|
11
|
+
createTabletHomePresentation,
|
|
8
12
|
createTextLabel,
|
|
13
|
+
createTouchAppRegistry,
|
|
9
14
|
createValueReadout,
|
|
15
|
+
defineTouchApp,
|
|
16
|
+
rectContainsPoint,
|
|
10
17
|
} from '@found-in-space/touch-os';
|
|
11
18
|
import {
|
|
12
19
|
createHudPanelDriver,
|
|
@@ -31,6 +38,185 @@ const DEFAULT_PANEL_DRIVER_OPTIONS = Object.freeze({
|
|
|
31
38
|
pointerClaimPolicy: 'block-on-hit',
|
|
32
39
|
transparent: true,
|
|
33
40
|
});
|
|
41
|
+
const DEFAULT_TABLET_LAUNCHER_LAYOUT = Object.freeze({
|
|
42
|
+
tileWidth: 84,
|
|
43
|
+
tileHeight: 88,
|
|
44
|
+
gap: 10,
|
|
45
|
+
bodyPadding: 10,
|
|
46
|
+
iconMinSize: 38,
|
|
47
|
+
iconMaxSize: 46,
|
|
48
|
+
iconScale: 0.55,
|
|
49
|
+
iconTop: 2,
|
|
50
|
+
labelGap: 5,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const SkykitSurfaceAppFrameComponent = {
|
|
54
|
+
kind: 'skykit-surface-app-frame',
|
|
55
|
+
getChildren(ctx) {
|
|
56
|
+
return ctx.props.child ? [ctx.props.child] : [];
|
|
57
|
+
},
|
|
58
|
+
measure(ctx) {
|
|
59
|
+
const padding = positiveFinite(ctx.props.padding, 0);
|
|
60
|
+
const width = Math.max(0, ctx.constraints.maxWidth - padding * 2);
|
|
61
|
+
const height = Math.max(0, ctx.constraints.maxHeight - padding * 2);
|
|
62
|
+
if (ctx.props.child) {
|
|
63
|
+
ctx.measureChild(ctx.props.child.id, {
|
|
64
|
+
minWidth: 0,
|
|
65
|
+
minHeight: 0,
|
|
66
|
+
maxWidth: width,
|
|
67
|
+
maxHeight: height,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
width: ctx.constraints.maxWidth,
|
|
72
|
+
height: ctx.constraints.maxHeight,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
layout(ctx) {
|
|
76
|
+
const padding = positiveFinite(ctx.props.padding, 0);
|
|
77
|
+
const content = createRect(
|
|
78
|
+
ctx.bounds.x + padding,
|
|
79
|
+
ctx.bounds.y + padding,
|
|
80
|
+
Math.max(0, ctx.bounds.width - padding * 2),
|
|
81
|
+
Math.max(0, ctx.bounds.height - padding * 2),
|
|
82
|
+
);
|
|
83
|
+
if (ctx.props.child) {
|
|
84
|
+
ctx.setChildBounds(ctx.props.child.id, content);
|
|
85
|
+
}
|
|
86
|
+
ctx.setContentBounds(content);
|
|
87
|
+
},
|
|
88
|
+
render(ctx) {
|
|
89
|
+
const theme = ctx.services.theme.getTokens();
|
|
90
|
+
return [{
|
|
91
|
+
type: 'rect',
|
|
92
|
+
componentId: ctx.id,
|
|
93
|
+
role: 'skykit-surface-app-frame',
|
|
94
|
+
rect: ctx.bounds,
|
|
95
|
+
fill: ctx.props.backgroundColor ?? theme.backgroundColor,
|
|
96
|
+
strokeWidth: 0,
|
|
97
|
+
radius: 0,
|
|
98
|
+
}];
|
|
99
|
+
},
|
|
100
|
+
hitTest(ctx) {
|
|
101
|
+
if (ctx.props.pointerOpaque === false || !rectContainsPoint(ctx.bounds, ctx.point)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
targetId: `${ctx.id}:background`,
|
|
106
|
+
role: 'surface-app-background',
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build a SkyKit-flavored touch-os tablet shell from ordinary touch apps.
|
|
113
|
+
*
|
|
114
|
+
* @param {import('./touch-os.d.ts').SkykitTabletRootOptions} [options]
|
|
115
|
+
* @returns {import('@found-in-space/touch-os').DisplayNode}
|
|
116
|
+
*/
|
|
117
|
+
export function createSkykitTabletRoot(options = {}) {
|
|
118
|
+
if (!options || typeof options !== 'object') {
|
|
119
|
+
throw new TypeError('createSkykitTabletRoot requires an options object.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const id = options.id ?? 'skykit-tablet';
|
|
123
|
+
const registry = options.registry ?? createTouchAppRegistry(options.apps ?? []);
|
|
124
|
+
const presentation = options.presentation ?? createTabletHomePresentation({
|
|
125
|
+
homeControl: options.homeControl ?? 'button',
|
|
126
|
+
taskSwitcher: options.taskSwitcher ?? 'cards',
|
|
127
|
+
taskCloseControl: options.taskCloseControl ?? 'button',
|
|
128
|
+
launcherLayout: {
|
|
129
|
+
...DEFAULT_TABLET_LAUNCHER_LAYOUT,
|
|
130
|
+
...(options.launcherLayout ?? {}),
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return createAppShell(id, {
|
|
135
|
+
registry,
|
|
136
|
+
presentation,
|
|
137
|
+
appHostMode: options.appHostMode ?? 'same-runtime',
|
|
138
|
+
homeKey: options.homeKey ?? true,
|
|
139
|
+
keepAlive: options.keepAlive ?? true,
|
|
140
|
+
...(options.initialSessions === undefined ? {} : { initialSessions: options.initialSessions }),
|
|
141
|
+
...(options.appStates === undefined ? {} : { appStates: options.appStates }),
|
|
142
|
+
...(options.getAppState === undefined ? {} : { getAppState: options.getAppState }),
|
|
143
|
+
...(options.forwardAppOutputs === undefined ? {} : { forwardAppOutputs: options.forwardAppOutputs }),
|
|
144
|
+
...(options.storage === undefined ? {} : { storage: options.storage }),
|
|
145
|
+
...(options.surfaces === undefined ? {} : { surfaces: options.surfaces }),
|
|
146
|
+
...(options.onAppEvent === undefined ? {} : { onAppEvent: options.onAppEvent }),
|
|
147
|
+
...(options.onShellChange === undefined ? {} : { onShellChange: options.onShellChange }),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Wrap a display node as a full-screen tablet app. This is useful for surface
|
|
153
|
+
* consumers such as HR diagrams, camera mirrors, or other panel-hosted views.
|
|
154
|
+
*
|
|
155
|
+
* @template TState
|
|
156
|
+
* @param {import('./touch-os.d.ts').SkykitSurfaceAppOptions<TState>} options
|
|
157
|
+
* @returns {import('@found-in-space/touch-os').TouchAppModule<TState>}
|
|
158
|
+
*/
|
|
159
|
+
export function createSkykitSurfaceApp(options) {
|
|
160
|
+
if (!options || typeof options !== 'object') {
|
|
161
|
+
throw new TypeError('createSkykitSurfaceApp requires options.');
|
|
162
|
+
}
|
|
163
|
+
const id = requiredString(options.id, 'createSkykitSurfaceApp id');
|
|
164
|
+
const name = requiredString(options.name, 'createSkykitSurfaceApp name');
|
|
165
|
+
const rootId = options.rootId ?? `${id}:root`;
|
|
166
|
+
|
|
167
|
+
return defineTouchApp({
|
|
168
|
+
manifest: {
|
|
169
|
+
id,
|
|
170
|
+
name,
|
|
171
|
+
version: options.version ?? '1.0.0',
|
|
172
|
+
icon: options.icon ?? createSymbolIcon(name),
|
|
173
|
+
capabilities: options.capabilities ?? ['surfaces'],
|
|
174
|
+
preferredWindow: {
|
|
175
|
+
width: options.preferredWindow?.width ?? 360,
|
|
176
|
+
height: options.preferredWindow?.height ?? 300,
|
|
177
|
+
minWidth: options.preferredWindow?.minWidth ?? 260,
|
|
178
|
+
minHeight: options.preferredWindow?.minHeight ?? 180,
|
|
179
|
+
resizable: options.preferredWindow?.resizable ?? false,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
createApp(ctx) {
|
|
183
|
+
return {
|
|
184
|
+
render(state) {
|
|
185
|
+
const child = resolveSurfaceAppNode(options.node, {
|
|
186
|
+
context: ctx,
|
|
187
|
+
state,
|
|
188
|
+
});
|
|
189
|
+
if (!child) {
|
|
190
|
+
return createTextLabel(`${rootId}:empty`, {
|
|
191
|
+
text: options.emptyLabel ?? `${name} unavailable`,
|
|
192
|
+
tone: 'muted',
|
|
193
|
+
align: 'center',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return createSkykitSurfaceAppFrame(rootId, {
|
|
197
|
+
child,
|
|
198
|
+
padding: options.padding ?? 0,
|
|
199
|
+
pointerOpaque: options.pointerOpaque !== false,
|
|
200
|
+
backgroundColor: options.backgroundColor,
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
handleOutput(output) {
|
|
204
|
+
options.onOutput?.(output, { context: ctx });
|
|
205
|
+
emitSkykitSurfaceAppOutput(ctx, output);
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {string} id
|
|
214
|
+
* @param {{ child: import('@found-in-space/touch-os').DisplayNode; padding?: number; pointerOpaque?: boolean; backgroundColor?: string }} props
|
|
215
|
+
* @returns {import('@found-in-space/touch-os').DisplayNode}
|
|
216
|
+
*/
|
|
217
|
+
function createSkykitSurfaceAppFrame(id, props) {
|
|
218
|
+
return createNode(id, SkykitSurfaceAppFrameComponent, props);
|
|
219
|
+
}
|
|
34
220
|
|
|
35
221
|
/**
|
|
36
222
|
* Create a SkyKit plugin that mounts a touch-os HUD and routes action outputs
|
|
@@ -901,6 +1087,95 @@ function finiteNumber(value, fallback) {
|
|
|
901
1087
|
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
902
1088
|
}
|
|
903
1089
|
|
|
1090
|
+
/**
|
|
1091
|
+
* @param {unknown} value
|
|
1092
|
+
* @param {string} context
|
|
1093
|
+
* @returns {string}
|
|
1094
|
+
*/
|
|
1095
|
+
function requiredString(value, context) {
|
|
1096
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
1097
|
+
throw new TypeError(`${context} must be a non-empty string.`);
|
|
1098
|
+
}
|
|
1099
|
+
return value;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* @param {import('./touch-os.d.ts').SkykitSurfaceAppOptions['node']} node
|
|
1104
|
+
* @param {import('./touch-os.d.ts').SkykitSurfaceAppRenderContext} context
|
|
1105
|
+
* @returns {import('@found-in-space/touch-os').DisplayNode | null}
|
|
1106
|
+
*/
|
|
1107
|
+
function resolveSurfaceAppNode(node, context) {
|
|
1108
|
+
const resolved = typeof node === 'function' ? node(context) : node;
|
|
1109
|
+
if (resolved == null) return null;
|
|
1110
|
+
if (!resolved || typeof resolved !== 'object' || typeof resolved.id !== 'string') {
|
|
1111
|
+
throw new TypeError('SkyKit surface app nodes must be display nodes.');
|
|
1112
|
+
}
|
|
1113
|
+
return resolved;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* @param {import('@found-in-space/touch-os').TouchAppContext} context
|
|
1118
|
+
* @param {unknown} output
|
|
1119
|
+
*/
|
|
1120
|
+
function emitSkykitSurfaceAppOutput(context, output) {
|
|
1121
|
+
if (isTouchOsActionOutput(output)) {
|
|
1122
|
+
context.actions.emit({
|
|
1123
|
+
type: 'app-action',
|
|
1124
|
+
appId: context.appId,
|
|
1125
|
+
instanceId: context.instanceId,
|
|
1126
|
+
windowId: context.windowId,
|
|
1127
|
+
name: output.actionId,
|
|
1128
|
+
...(output.payload === undefined ? {} : { payload: output.payload }),
|
|
1129
|
+
componentId: output.componentId,
|
|
1130
|
+
});
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
if (isTouchOsChangeOutput(output)) {
|
|
1134
|
+
context.actions.emit({
|
|
1135
|
+
type: 'app-change',
|
|
1136
|
+
appId: context.appId,
|
|
1137
|
+
instanceId: context.instanceId,
|
|
1138
|
+
windowId: context.windowId,
|
|
1139
|
+
name: `${output.field}.change`,
|
|
1140
|
+
payload: {
|
|
1141
|
+
field: output.field,
|
|
1142
|
+
value: output.value,
|
|
1143
|
+
},
|
|
1144
|
+
componentId: output.componentId,
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* @param {unknown} output
|
|
1151
|
+
* @returns {output is { type: 'change-request'; componentId: string; field: string; value: unknown }}
|
|
1152
|
+
*/
|
|
1153
|
+
function isTouchOsChangeOutput(output) {
|
|
1154
|
+
return Boolean(output)
|
|
1155
|
+
&& typeof output === 'object'
|
|
1156
|
+
&& output.type === 'change-request'
|
|
1157
|
+
&& typeof output.componentId === 'string'
|
|
1158
|
+
&& typeof output.field === 'string';
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* @param {string} name
|
|
1163
|
+
* @returns {{ kind: 'symbol'; value: string }}
|
|
1164
|
+
*/
|
|
1165
|
+
function createSymbolIcon(name) {
|
|
1166
|
+
const letters = name
|
|
1167
|
+
.split(/\s+/)
|
|
1168
|
+
.filter(Boolean)
|
|
1169
|
+
.map((part) => part[0])
|
|
1170
|
+
.join('')
|
|
1171
|
+
.slice(0, 2)
|
|
1172
|
+
.toUpperCase();
|
|
1173
|
+
return {
|
|
1174
|
+
kind: 'symbol',
|
|
1175
|
+
value: letters || 'SK',
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
|
|
904
1179
|
function now() {
|
|
905
1180
|
return typeof performance !== 'undefined' && typeof performance.now === 'function'
|
|
906
1181
|
? performance.now()
|
package/src/utils.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
+
import {
|
|
3
|
+
resolveSpatialLookAt,
|
|
4
|
+
} from '@found-in-space/spatial';
|
|
2
5
|
|
|
3
6
|
export const DEFAULT_MAG_LIMIT = 6.5;
|
|
4
7
|
export const IDENTITY_QUATERNION = Object.freeze({ x: 0, y: 0, z: 0, w: 1 });
|
|
@@ -86,16 +89,17 @@ export function resolveAnchorRoot(roots, anchorMode, scaleBandId) {
|
|
|
86
89
|
* @param {number} revision
|
|
87
90
|
* @returns {SkykitViewState}
|
|
88
91
|
*/
|
|
89
|
-
export function normalizeViewState(input = {}, revision = 0) {
|
|
92
|
+
export function normalizeViewState(input = {}, revision = 0, options = {}) {
|
|
90
93
|
const observerPc = normalizeVector3(input.observerPc, { x: 0, y: 0, z: 0 });
|
|
91
94
|
const coordinateUnitsPerParsec = positiveFinite(input.coordinateUnitsPerParsec, 1);
|
|
95
|
+
const look = resolveViewLook(input, observerPc, options);
|
|
92
96
|
return {
|
|
93
97
|
revision,
|
|
94
98
|
observerPc,
|
|
95
99
|
renderObserverPosition: normalizeVector3(input.renderObserverPosition, observerPc),
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
orientationIcrs:
|
|
100
|
+
lookAt: look.lookAt,
|
|
101
|
+
targetPc: look.targetPc,
|
|
102
|
+
orientationIcrs: look.orientationIcrs,
|
|
99
103
|
limitingMagnitude: finiteNumber(input.limitingMagnitude, DEFAULT_MAG_LIMIT),
|
|
100
104
|
...(input.verticalFovDeg !== undefined ? { verticalFovDeg: positiveFinite(input.verticalFovDeg, 40) } : {}),
|
|
101
105
|
...(input.aspectRatio !== undefined ? { aspectRatio: positiveFinite(input.aspectRatio, 1) } : {}),
|
|
@@ -110,8 +114,8 @@ export function cloneViewState(view) {
|
|
|
110
114
|
...view,
|
|
111
115
|
observerPc: cloneVector3(view.observerPc),
|
|
112
116
|
renderObserverPosition: cloneVector3(view.renderObserverPosition),
|
|
117
|
+
lookAt: cloneLookAt(view.lookAt),
|
|
113
118
|
targetPc: view.targetPc ? cloneVector3(view.targetPc) : null,
|
|
114
|
-
directionIcrs: view.directionIcrs ? cloneVector3(view.directionIcrs) : null,
|
|
115
119
|
orientationIcrs: view.orientationIcrs ? cloneQuaternion(view.orientationIcrs) : null,
|
|
116
120
|
motion: view.motion ? cloneMotion(view.motion) : null,
|
|
117
121
|
};
|
|
@@ -125,7 +129,6 @@ export function toStarOctreeViewPatch(view) {
|
|
|
125
129
|
limitingMagnitude: view.limitingMagnitude,
|
|
126
130
|
mDesired: view.limitingMagnitude,
|
|
127
131
|
...(view.targetPc ? { targetPc: view.targetPc } : {}),
|
|
128
|
-
...(view.directionIcrs ? { directionIcrs: view.directionIcrs } : {}),
|
|
129
132
|
...(view.orientationIcrs ? { orientationIcrs: view.orientationIcrs } : {}),
|
|
130
133
|
...(view.verticalFovDeg !== undefined ? { verticalFovDeg: view.verticalFovDeg } : {}),
|
|
131
134
|
...(view.aspectRatio !== undefined ? { aspectRatio: view.aspectRatio } : {}),
|
|
@@ -134,6 +137,93 @@ export function toStarOctreeViewPatch(view) {
|
|
|
134
137
|
return patch;
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
/**
|
|
141
|
+
* @param {Partial<SkykitViewState>} input
|
|
142
|
+
* @param {Record<string, unknown>} [options]
|
|
143
|
+
* @returns {Promise<Partial<SkykitViewState>>}
|
|
144
|
+
*/
|
|
145
|
+
export async function resolveViewLookAtInput(input = {}, options = {}) {
|
|
146
|
+
const observerPc = normalizeVector3(
|
|
147
|
+
input.observerPc ?? /** @type {{ observerPc?: unknown }} */ (options).observerPc,
|
|
148
|
+
{ x: 0, y: 0, z: 0 },
|
|
149
|
+
);
|
|
150
|
+
const lookInput = input.lookAt
|
|
151
|
+
?? (input.orientationIcrs ? { orientationIcrs: input.orientationIcrs } : null);
|
|
152
|
+
if (!lookInput) return input;
|
|
153
|
+
const resolved = await resolveSpatialLookAt(lookInput, {
|
|
154
|
+
observerPc,
|
|
155
|
+
resolveStar: typeof options.resolveStar === 'function'
|
|
156
|
+
? /** @type {import('@found-in-space/spatial').ResolveSpatialLookAtOptions['resolveStar']} */ (options.resolveStar)
|
|
157
|
+
: undefined,
|
|
158
|
+
resolveBookmark: typeof options.resolveBookmark === 'function'
|
|
159
|
+
? /** @type {import('@found-in-space/spatial').ResolveSpatialLookAtOptions['resolveBookmark']} */ (options.resolveBookmark)
|
|
160
|
+
: undefined,
|
|
161
|
+
});
|
|
162
|
+
const resolvedLook = /** @type {import('@found-in-space/spatial').SpatialResolvedLookAt} */ (resolved);
|
|
163
|
+
return {
|
|
164
|
+
...input,
|
|
165
|
+
lookAt: /** @type {import('./index.d.ts').SkykitLookAtInput | null} */ (resolvedLook.lookAt),
|
|
166
|
+
targetPc: resolvedLook.targetPc,
|
|
167
|
+
orientationIcrs: resolvedLook.orientationIcrs,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {Partial<SkykitViewState>} input
|
|
173
|
+
* @param {Vector3Like} observerPc
|
|
174
|
+
* @param {Record<string, unknown>} [options]
|
|
175
|
+
* @returns {{ lookAt: import('./index.d.ts').SkykitLookAtInput | null; targetPc: Vector3Like | null; orientationIcrs: QuaternionLike | null }}
|
|
176
|
+
*/
|
|
177
|
+
function resolveViewLook(input, observerPc, options = {}) {
|
|
178
|
+
const lookInput = input.lookAt
|
|
179
|
+
?? (input.orientationIcrs ? { orientationIcrs: input.orientationIcrs } : null);
|
|
180
|
+
if (!lookInput) {
|
|
181
|
+
return {
|
|
182
|
+
lookAt: null,
|
|
183
|
+
targetPc: null,
|
|
184
|
+
orientationIcrs: null,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const resolved = resolveSpatialLookAt(lookInput, {
|
|
188
|
+
observerPc,
|
|
189
|
+
resolveStar: typeof options.resolveStar === 'function'
|
|
190
|
+
? /** @type {import('@found-in-space/spatial').ResolveSpatialLookAtOptions['resolveStar']} */ (options.resolveStar)
|
|
191
|
+
: undefined,
|
|
192
|
+
resolveBookmark: typeof options.resolveBookmark === 'function'
|
|
193
|
+
? /** @type {import('@found-in-space/spatial').ResolveSpatialLookAtOptions['resolveBookmark']} */ (options.resolveBookmark)
|
|
194
|
+
: undefined,
|
|
195
|
+
});
|
|
196
|
+
if (resolved && typeof /** @type {Promise<unknown>} */ (resolved).then === 'function') {
|
|
197
|
+
throw new TypeError('normalizeViewState() received an async lookAt resolver result.');
|
|
198
|
+
}
|
|
199
|
+
const resolvedLook = /** @type {import('@found-in-space/spatial').SpatialResolvedLookAt} */ (resolved);
|
|
200
|
+
return {
|
|
201
|
+
lookAt: /** @type {import('./index.d.ts').SkykitLookAtInput | null} */ (cloneLookAt(resolvedLook.lookAt)),
|
|
202
|
+
targetPc: resolvedLook.targetPc
|
|
203
|
+
? cloneVector3(resolvedLook.targetPc)
|
|
204
|
+
: (input.targetPc == null ? null : normalizeVector3(input.targetPc, { x: 0, y: 0, z: 0 })),
|
|
205
|
+
orientationIcrs: resolvedLook.orientationIcrs ? cloneQuaternion(resolvedLook.orientationIcrs) : null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @param {unknown} lookAt
|
|
211
|
+
* @returns {import('./index.d.ts').SkykitLookAtInput | null}
|
|
212
|
+
*/
|
|
213
|
+
function cloneLookAt(lookAt) {
|
|
214
|
+
if (!lookAt || typeof lookAt !== 'object') return null;
|
|
215
|
+
const source = /** @type {Record<string, unknown>} */ (lookAt);
|
|
216
|
+
return {
|
|
217
|
+
...source,
|
|
218
|
+
...(source.targetPc && typeof source.targetPc === 'object'
|
|
219
|
+
? { targetPc: cloneVector3(/** @type {Vector3Like} */ (source.targetPc)) }
|
|
220
|
+
: {}),
|
|
221
|
+
...(source.orientationIcrs && typeof source.orientationIcrs === 'object'
|
|
222
|
+
? { orientationIcrs: cloneQuaternion(/** @type {QuaternionLike} */ (source.orientationIcrs)) }
|
|
223
|
+
: {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
137
227
|
/**
|
|
138
228
|
* @param {SkykitPluginInput} plugin
|
|
139
229
|
* @param {SkykitThreePluginContext} context
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type * as ThreeNamespace from 'three';
|
|
2
|
+
|
|
3
|
+
export declare const THREE: typeof ThreeNamespace;
|
|
4
|
+
export {
|
|
5
|
+
createSkykitBrowser,
|
|
6
|
+
type SkykitBrowser,
|
|
7
|
+
type SkykitBrowserObjectHandle,
|
|
8
|
+
type SkykitBrowserObjectOptions,
|
|
9
|
+
type SkykitBrowserOptions,
|
|
10
|
+
} from './browser.js';
|
package/src/viewer.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
mountRenderer,
|
|
16
16
|
normalizeViewState,
|
|
17
17
|
positiveFinite,
|
|
18
|
+
resolveViewLookAtInput,
|
|
18
19
|
setupPlugin,
|
|
19
20
|
snapshotPart,
|
|
20
21
|
syncRootsFromView,
|
|
@@ -71,7 +72,7 @@ export async function createSkykitViewer(options = {}) {
|
|
|
71
72
|
let started = false;
|
|
72
73
|
let elapsedSeconds = 0;
|
|
73
74
|
const initialProjectionView = resolveCameraProjectionView(camera);
|
|
74
|
-
|
|
75
|
+
const initialViewInput = await resolveViewLookAtInput({
|
|
75
76
|
...options.view,
|
|
76
77
|
...(options.view?.verticalFovDeg === undefined && initialProjectionView.verticalFovDeg !== undefined
|
|
77
78
|
? { verticalFovDeg: initialProjectionView.verticalFovDeg }
|
|
@@ -83,8 +84,11 @@ export async function createSkykitViewer(options = {}) {
|
|
|
83
84
|
renderObserverPosition: options.view?.renderObserverPosition ?? observerRig.getRenderObserverPosition(),
|
|
84
85
|
orientationIcrs: options.view?.orientationIcrs ?? observerRig.getOrientationIcrs?.() ?? null,
|
|
85
86
|
motion: options.view?.motion ?? observerRig.getMotion?.() ?? null,
|
|
86
|
-
},
|
|
87
|
+
}, viewLookResolverOptions(options.view?.observerPc ?? observerRig.getObserverPc()));
|
|
88
|
+
let view = normalizeViewState(initialViewInput, 0, viewLookResolverOptions(initialViewInput.observerPc));
|
|
87
89
|
const initialView = cloneViewState(view);
|
|
90
|
+
observerRig.setObserverPc?.(view.observerPc);
|
|
91
|
+
if (view.orientationIcrs) observerRig.setOrientationIcrs?.(view.orientationIcrs);
|
|
88
92
|
|
|
89
93
|
addRootToScene(scene, roots.originContentRoot);
|
|
90
94
|
addRootToScene(scene, roots.observerContentRoot);
|
|
@@ -112,6 +116,7 @@ export async function createSkykitViewer(options = {}) {
|
|
|
112
116
|
observerRig,
|
|
113
117
|
actions,
|
|
114
118
|
addPart,
|
|
119
|
+
addPlugin,
|
|
115
120
|
getViewState,
|
|
116
121
|
requestViewState,
|
|
117
122
|
update,
|
|
@@ -139,10 +144,7 @@ export async function createSkykitViewer(options = {}) {
|
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
for (const plugin of pluginInputs) {
|
|
142
|
-
|
|
143
|
-
if (typeof teardown === 'function') {
|
|
144
|
-
disposables.push(teardown);
|
|
145
|
-
}
|
|
147
|
+
await installPlugin(plugin);
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
for (const part of orderedParts()) {
|
|
@@ -176,6 +178,32 @@ export async function createSkykitViewer(options = {}) {
|
|
|
176
178
|
};
|
|
177
179
|
}
|
|
178
180
|
|
|
181
|
+
/**
|
|
182
|
+
* @param {import('./index.d.ts').SkykitPluginInput} plugin
|
|
183
|
+
* @returns {Promise<SkykitPluginTeardown>}
|
|
184
|
+
*/
|
|
185
|
+
async function addPlugin(plugin) {
|
|
186
|
+
assertActive();
|
|
187
|
+
return installPlugin(plugin, true);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {import('./index.d.ts').SkykitPluginInput} plugin
|
|
192
|
+
* @param {boolean} [record]
|
|
193
|
+
* @returns {Promise<SkykitPluginTeardown>}
|
|
194
|
+
*/
|
|
195
|
+
async function installPlugin(plugin, record = false) {
|
|
196
|
+
const teardown = await setupPlugin(plugin, context);
|
|
197
|
+
if (record) pluginInputs.push(plugin);
|
|
198
|
+
if (typeof teardown !== 'function') return () => {};
|
|
199
|
+
disposables.push(teardown);
|
|
200
|
+
return () => {
|
|
201
|
+
const index = disposables.indexOf(teardown);
|
|
202
|
+
if (index >= 0) disposables.splice(index, 1);
|
|
203
|
+
void teardown();
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
179
207
|
/**
|
|
180
208
|
* @param {SkykitThreePart} part
|
|
181
209
|
*/
|
|
@@ -192,9 +220,8 @@ export async function createSkykitViewer(options = {}) {
|
|
|
192
220
|
*/
|
|
193
221
|
async function removePart(part) {
|
|
194
222
|
const index = parts.indexOf(part);
|
|
195
|
-
if (index
|
|
196
|
-
|
|
197
|
-
}
|
|
223
|
+
if (index < 0) return;
|
|
224
|
+
parts.splice(index, 1);
|
|
198
225
|
emit({ type: 'part/remove', part });
|
|
199
226
|
await detachAndDisposePart(part);
|
|
200
227
|
}
|
|
@@ -209,11 +236,32 @@ export async function createSkykitViewer(options = {}) {
|
|
|
209
236
|
*/
|
|
210
237
|
function requestViewState(patch, reason) {
|
|
211
238
|
assertActive();
|
|
239
|
+
const normalizedPatch = normalizeRequestedViewPatch(patch);
|
|
240
|
+
if (lookAtNeedsAsyncResolution(normalizedPatch)) {
|
|
241
|
+
const observerPc = /** @type {Partial<SkykitViewState>} */ (normalizedPatch).observerPc ?? view.observerPc;
|
|
242
|
+
void resolveViewLookAtInput(normalizedPatch, viewLookResolverOptions(observerPc))
|
|
243
|
+
.then((resolvedPatch) => {
|
|
244
|
+
if (disposed) return;
|
|
245
|
+
pendingViewPatch = {
|
|
246
|
+
...(pendingViewPatch ?? {}),
|
|
247
|
+
...resolvedPatch,
|
|
248
|
+
};
|
|
249
|
+
})
|
|
250
|
+
.catch((error) => {
|
|
251
|
+
emit({
|
|
252
|
+
type: 'view/lookAt-error',
|
|
253
|
+
reason,
|
|
254
|
+
error: error instanceof Error ? error.message : String(error),
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
emit({ type: 'view/request', reason, patch: normalizedPatch });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
212
260
|
pendingViewPatch = {
|
|
213
261
|
...(pendingViewPatch ?? {}),
|
|
214
|
-
...
|
|
262
|
+
...normalizedPatch,
|
|
215
263
|
};
|
|
216
|
-
emit({ type: 'view/request', reason, patch });
|
|
264
|
+
emit({ type: 'view/request', reason, patch: normalizedPatch });
|
|
217
265
|
}
|
|
218
266
|
|
|
219
267
|
/**
|
|
@@ -356,7 +404,7 @@ export async function createSkykitViewer(options = {}) {
|
|
|
356
404
|
}
|
|
357
405
|
}
|
|
358
406
|
for (const part of [...orderedParts()].reverse()) {
|
|
359
|
-
await
|
|
407
|
+
await removePart(part);
|
|
360
408
|
}
|
|
361
409
|
for (const disposable of [...disposables].reverse()) {
|
|
362
410
|
await disposable();
|
|
@@ -454,6 +502,56 @@ export async function createSkykitViewer(options = {}) {
|
|
|
454
502
|
return [...parts].sort(compareParts);
|
|
455
503
|
}
|
|
456
504
|
|
|
505
|
+
/** @param {Partial<SkykitViewState>} patch */
|
|
506
|
+
function normalizeRequestedViewPatch(patch) {
|
|
507
|
+
if (!patch || typeof patch !== 'object') return patch;
|
|
508
|
+
if ('lookAt' in patch) return patch;
|
|
509
|
+
if (patch.orientationIcrs) {
|
|
510
|
+
return {
|
|
511
|
+
...patch,
|
|
512
|
+
lookAt: { orientationIcrs: patch.orientationIcrs },
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
if (patch.targetPc) {
|
|
516
|
+
return {
|
|
517
|
+
...patch,
|
|
518
|
+
lookAt: { targetPc: patch.targetPc },
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
return patch;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** @param {Partial<SkykitViewState>} patch */
|
|
525
|
+
function lookAtNeedsAsyncResolution(patch) {
|
|
526
|
+
if (!patch || typeof patch !== 'object' || !('lookAt' in patch)) return false;
|
|
527
|
+
const lookAt = /** @type {Record<string, unknown> | null} */ (patch.lookAt);
|
|
528
|
+
return !!lookAt
|
|
529
|
+
&& typeof lookAt === 'object'
|
|
530
|
+
&& 'star' in lookAt
|
|
531
|
+
&& !('targetPc' in lookAt)
|
|
532
|
+
&& typeof options.resolveLookAtStar === 'function';
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** @param {unknown} observerPc */
|
|
536
|
+
function viewLookResolverOptions(observerPc) {
|
|
537
|
+
return {
|
|
538
|
+
observerPc,
|
|
539
|
+
resolveStar: typeof options.resolveLookAtStar === 'function'
|
|
540
|
+
? /** @type {import('@found-in-space/spatial').ResolveSpatialLookAtOptions['resolveStar']} */ (
|
|
541
|
+
(star, lookAt) => options.resolveLookAtStar?.(
|
|
542
|
+
star,
|
|
543
|
+
/** @type {import('./index.d.ts').SkykitLookAtInput} */ (lookAt),
|
|
544
|
+
)
|
|
545
|
+
)
|
|
546
|
+
: undefined,
|
|
547
|
+
resolveBookmark: typeof options.resolveLookAtBookmark === 'function'
|
|
548
|
+
? /** @type {import('@found-in-space/spatial').ResolveSpatialLookAtOptions['resolveBookmark']} */ (
|
|
549
|
+
(bookmarkId, lookAt) => options.resolveLookAtBookmark?.(bookmarkId, lookAt)
|
|
550
|
+
)
|
|
551
|
+
: undefined,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
457
555
|
/**
|
|
458
556
|
* @param {SkykitViewer} currentViewer
|
|
459
557
|
* @returns {SkykitThreePluginContext}
|