@found-in-space/skykit 0.2.0-alpha.1 → 0.2.0-alpha.20260528

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.
Files changed (40) hide show
  1. package/README.md +142 -11
  2. package/examples/custom-object-layer/custom-object-layer.js +1 -24
  3. package/examples/xr-free-roam/index.html +62 -4
  4. package/examples/xr-free-roam/xr-free-roam.css +249 -18
  5. package/examples/xr-free-roam/xr-free-roam.js +675 -274
  6. package/package.json +22 -6
  7. package/src/__tests__/skykit-anchored-images.test.js +32 -4
  8. package/src/__tests__/skykit-browser.test.js +267 -0
  9. package/src/__tests__/skykit-data.test.js +131 -0
  10. package/src/__tests__/skykit-parallax.test.js +4 -4
  11. package/src/__tests__/skykit-touch-os.test.js +71 -0
  12. package/src/__tests__/skykit-xr.test.js +179 -2
  13. package/src/__tests__/skykit.test.js +142 -506
  14. package/src/actions.js +0 -8
  15. package/src/anchored-images.js +14 -15
  16. package/src/browser-addons.d.ts +16 -0
  17. package/src/browser-addons.js +155 -0
  18. package/src/browser-constellations.d.ts +13 -0
  19. package/src/browser-constellations.js +387 -0
  20. package/src/browser.d.ts +81 -0
  21. package/src/browser.js +192 -13
  22. package/src/data.d.ts +133 -0
  23. package/src/data.js +447 -0
  24. package/src/embed.d.ts +5 -0
  25. package/src/embed.js +53 -2
  26. package/src/hr-diagram.js +23 -5
  27. package/src/index.d.ts +21 -73
  28. package/src/index.js +0 -1
  29. package/src/plugins.js +22 -708
  30. package/src/three-shim.d.ts +32 -0
  31. package/src/touch-os.d.ts +70 -0
  32. package/src/touch-os.js +275 -0
  33. package/src/utils.js +96 -6
  34. package/src/viewer-entry.d.ts +10 -0
  35. package/src/viewer-entry.js +4 -0
  36. package/src/viewer.js +110 -12
  37. package/src/xr/plugins.js +298 -13
  38. package/src/xr/session.js +60 -14
  39. package/src/xr.d.ts +40 -0
  40. package/src/xr.js +2 -0
@@ -17,6 +17,8 @@ declare module 'three' {
17
17
  updateMatrixWorld(force?: boolean): void;
18
18
  }
19
19
 
20
+ export type ColorRepresentation = number | string;
21
+
20
22
  export class Group extends Object3D {}
21
23
  export class Scene extends Object3D {}
22
24
  export class Camera extends Object3D {
@@ -105,10 +107,40 @@ declare module 'three' {
105
107
 
106
108
  export class ShaderMaterial extends Material {}
107
109
 
110
+ export class LineBasicMaterial extends Material {
111
+ constructor(parameters?: {
112
+ color?: ColorRepresentation;
113
+ transparent?: boolean;
114
+ opacity?: number;
115
+ depthTest?: boolean;
116
+ depthWrite?: boolean;
117
+ });
118
+ }
119
+
120
+ export class BufferAttribute {
121
+ array: ArrayLike<number>;
122
+ itemSize: number;
123
+ needsUpdate: boolean;
124
+ count: number;
125
+ constructor(array: ArrayLike<number>, itemSize: number, normalized?: boolean);
126
+ }
127
+
108
128
  export class BufferGeometry {
129
+ attributes: Record<string, BufferAttribute>;
130
+ setAttribute(name: string, attribute: BufferAttribute): this;
131
+ getAttribute(name: string): BufferAttribute | undefined;
132
+ computeBoundingSphere(): void;
109
133
  dispose(): void;
110
134
  }
111
135
 
136
+ export class Line extends Object3D {
137
+ geometry: BufferGeometry;
138
+ material: Material | Material[];
139
+ frustumCulled: boolean;
140
+ renderOrder: number;
141
+ constructor(geometry?: BufferGeometry, material?: Material | Material[]);
142
+ }
143
+
112
144
  export class Mesh extends Object3D {
113
145
  readonly isMesh: boolean;
114
146
  geometry: BufferGeometry;
package/src/touch-os.d.ts CHANGED
@@ -1,9 +1,24 @@
1
1
  import type {
2
+ AppShellChange,
3
+ AppShellPresentation,
4
+ AppShellSession,
5
+ AppShellSessionSeed,
2
6
  DisplayNode,
3
7
  DisplayRuntime,
8
+ EmbeddedSurfaceService,
4
9
  RuntimeOutput,
5
10
  RuntimeOptions,
6
11
  SurfaceMetrics,
12
+ TabletHomeLauncherLayoutOptions,
13
+ TouchAppCapability,
14
+ TouchAppContext,
15
+ TouchAppEvent,
16
+ TouchAppModule,
17
+ TouchAppPreferredWindow,
18
+ TouchAppRegistry,
19
+ TouchAppStorage,
20
+ TouchIconDescriptor,
21
+ WindowManagerAppHostMode,
7
22
  } from '@found-in-space/touch-os';
8
23
  import type {
9
24
  HudPanelDriverOptions,
@@ -97,6 +112,55 @@ export interface TouchOsHudPlugin extends SkykitPlugin {
97
112
 
98
113
  export type TouchOsPanelDriverKind = 'hud' | 'pose-anchored' | 'scene';
99
114
 
115
+ export interface SkykitTabletRootOptions {
116
+ id?: string;
117
+ apps?: readonly TouchAppModule<unknown>[];
118
+ registry?: TouchAppRegistry;
119
+ presentation?: AppShellPresentation;
120
+ appHostMode?: WindowManagerAppHostMode;
121
+ homeKey?: boolean;
122
+ keepAlive?: boolean;
123
+ initialSessions?: readonly AppShellSessionSeed[];
124
+ appStates?: Readonly<Record<string, unknown>>;
125
+ getAppState?: (session: AppShellSession) => unknown;
126
+ forwardAppOutputs?: boolean;
127
+ storage?: TouchAppStorage;
128
+ surfaces?: EmbeddedSurfaceService;
129
+ onAppEvent?: (event: TouchAppEvent) => void;
130
+ onShellChange?: (change: AppShellChange) => void;
131
+ homeControl?: 'button' | 'bar' | 'none';
132
+ taskSwitcher?: 'cards' | 'list' | 'none';
133
+ taskCloseControl?: 'button' | 'none';
134
+ launcherLayout?: TabletHomeLauncherLayoutOptions;
135
+ }
136
+
137
+ export interface SkykitSurfaceAppRenderContext<TState = unknown> {
138
+ context: TouchAppContext;
139
+ state: TState;
140
+ }
141
+
142
+ export interface SkykitSurfaceAppOutputContext {
143
+ context: TouchAppContext;
144
+ }
145
+
146
+ export interface SkykitSurfaceAppOptions<TState = unknown> {
147
+ id: string;
148
+ name: string;
149
+ version?: string;
150
+ icon?: TouchIconDescriptor;
151
+ capabilities?: readonly TouchAppCapability[];
152
+ preferredWindow?: TouchAppPreferredWindow;
153
+ rootId?: string;
154
+ node:
155
+ | DisplayNode
156
+ | ((context: SkykitSurfaceAppRenderContext<TState>) => DisplayNode | null | undefined);
157
+ padding?: number;
158
+ pointerOpaque?: boolean;
159
+ backgroundColor?: string;
160
+ emptyLabel?: string;
161
+ onOutput?: (output: RuntimeOutput, context: SkykitSurfaceAppOutputContext) => void;
162
+ }
163
+
100
164
  export interface TouchOsPanelRootContext {
101
165
  id: string;
102
166
  context: SkykitPluginContext;
@@ -212,6 +276,12 @@ export interface CreateTouchOsHostFrameOptions {
212
276
  events?: readonly ThreePanelHostInputEvent[];
213
277
  }
214
278
 
279
+ export declare function createSkykitTabletRoot(options?: SkykitTabletRootOptions): DisplayNode;
280
+
281
+ export declare function createSkykitSurfaceApp<TState = unknown>(
282
+ options: SkykitSurfaceAppOptions<TState>
283
+ ): TouchAppModule<TState>;
284
+
215
285
  export declare function createTouchOsHudPlugin(options: TouchOsHudPluginOptions): TouchOsHudPlugin;
216
286
  export declare function createTouchOsPanelPlugin(options: TouchOsPanelPluginOptions): TouchOsPanelPlugin;
217
287
 
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
- targetPc: input.targetPc == null ? null : normalizeVector3(input.targetPc, { x: 0, y: 0, z: 0 }),
97
- directionIcrs: input.directionIcrs == null ? null : normalizeVector3(input.directionIcrs, { x: 0, y: 0, z: -1 }),
98
- orientationIcrs: input.orientationIcrs == null ? null : normalizeQuaternion(input.orientationIcrs, IDENTITY_QUATERNION),
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';
@@ -0,0 +1,4 @@
1
+ import * as THREE from 'three';
2
+
3
+ export { THREE };
4
+ export { createSkykitBrowser } from './browser.js';