@zixcel/dom-renderer 0.10.4 → 0.10.6
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 +7 -1
- package/package.json +7 -5
- package/src/book-reader.d.mts +9 -1
- package/src/book-reader.mjs +54 -33
- package/src/dom-elements.d.mts +11 -0
- package/src/dom-elements.mjs +48 -0
- package/src/frame-layout.d.mts +9 -3
- package/src/frame-layout.mjs +22 -22
- package/src/frame.d.mts +22 -10
- package/src/frame.mjs +93 -45
- package/src/geometry-policy.d.mts +3 -0
- package/src/geometry-policy.mjs +17 -0
- package/src/graph-documents.d.mts +48 -0
- package/src/graph-documents.mjs +40 -0
- package/src/graphics-disposal.d.mts +2 -0
- package/src/graphics-disposal.mjs +14 -0
- package/src/index.d.mts +11 -57
- package/src/index.mjs +9 -205
- package/src/input-form.d.mts +5 -0
- package/src/input-form.mjs +103 -0
- package/src/inspection-overview.d.mts +27 -1
- package/src/inspection-overview.mjs +50 -33
- package/src/npc-motion.d.mts +1 -1
- package/src/npc-motion.mjs +5 -5
- package/src/object-presentation.d.mts +22 -26
- package/src/object-presentation.mjs +20 -17
- package/src/open-world-projection.d.mts +19 -10
- package/src/open-world-projection.mjs +31 -22
- package/src/reconcile.d.mts +1 -1
- package/src/reconcile.mjs +31 -19
- package/src/scene-items.d.mts +9 -0
- package/src/scene-items.mjs +29 -0
- package/src/scene-renderer.d.mts +37 -0
- package/src/scene-renderer.mjs +170 -0
- package/src/spatial-patterns.d.mts +22 -27
- package/src/spatial-patterns.mjs +63 -41
- package/src/spatial.d.mts +17 -6
- package/src/spatial.mjs +39 -25
- package/src/structure-graph.d.mts +5 -13
- package/src/structure-graph.mjs +167 -96
- package/src/symbol.d.mts +1 -1
- package/src/symbol.mjs +21 -16
- package/src/world-api.d.mts +56 -0
- package/src/world-api.mjs +8 -0
- package/src/world-camera.d.mts +14 -0
- package/src/world-camera.mjs +74 -0
- package/src/world-clock.d.mts +19 -0
- package/src/world-clock.mjs +30 -0
- package/src/world-commands.d.mts +39 -0
- package/src/world-commands.mjs +1 -0
- package/src/world-context.d.mts +132 -0
- package/src/world-context.mjs +1 -0
- package/src/world-controller.d.mts +43 -0
- package/src/world-controller.mjs +62 -0
- package/src/world-conversation.d.mts +3 -0
- package/src/world-conversation.mjs +46 -0
- package/src/world-events.d.mts +2 -0
- package/src/world-events.mjs +120 -0
- package/src/world-gpu.d.mts +3 -15
- package/src/world-gpu.mjs +159 -74
- package/src/world-input.d.mts +2 -2
- package/src/world-input.mjs +28 -18
- package/src/world-lifecycle.d.mts +12 -0
- package/src/world-lifecycle.mjs +172 -0
- package/src/world-model.d.mts +29 -40
- package/src/world-model.mjs +103 -74
- package/src/world-projection.d.mts +5 -0
- package/src/world-projection.mjs +169 -0
- package/src/world-render-loop.d.mts +9 -0
- package/src/world-render-loop.mjs +110 -0
- package/src/world-resources.d.mts +34 -0
- package/src/world-resources.mjs +8 -0
- package/src/world-runtime.d.mts +2 -36
- package/src/world-runtime.mjs +6 -311
- package/src/world-setup.d.mts +2 -0
- package/src/world-setup.mjs +179 -0
- package/src/world-surface.d.mts +2 -7
- package/src/world-surface.mjs +20 -12
- package/src/world-types.d.mts +74 -0
- package/src/world-types.mjs +1 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Camera } from '@zixcel/projection-client';
|
|
2
|
+
import type { Pose, WorldObject, WorldRelation } from './world-types.mjs';
|
|
3
|
+
import type { SpatialState } from './spatial-patterns.mjs';
|
|
4
|
+
export interface RolePresentation {
|
|
5
|
+
state: string;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ActivityView {
|
|
9
|
+
panel: HTMLElement;
|
|
10
|
+
update(value: unknown): void;
|
|
11
|
+
close?(): void;
|
|
12
|
+
}
|
|
13
|
+
export interface ActivityPresentation {
|
|
14
|
+
describeRole?: (activity: unknown, id: string) => RolePresentation | null;
|
|
15
|
+
createView?: (document: Document, host: HTMLElement) => ActivityView;
|
|
16
|
+
}
|
|
17
|
+
export interface WorldSnapshot extends Pose {
|
|
18
|
+
target: string | null;
|
|
19
|
+
presentation: SpatialState;
|
|
20
|
+
navigation: string;
|
|
21
|
+
populated: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface WorldOptions {
|
|
24
|
+
objects?: WorldObject[];
|
|
25
|
+
relations?: WorldRelation[];
|
|
26
|
+
pose?: WorldSnapshot | null;
|
|
27
|
+
onActivate?: (id: string) => void;
|
|
28
|
+
conversation?: ((item: WorldObject, activate: () => void) => HTMLElement | null) | null;
|
|
29
|
+
activity?: unknown;
|
|
30
|
+
target?: string | null;
|
|
31
|
+
pattern?: string;
|
|
32
|
+
context?: string | null;
|
|
33
|
+
activityPresentation?: ActivityPresentation;
|
|
34
|
+
}
|
|
35
|
+
export interface World {
|
|
36
|
+
update(objects: WorldObject[], relations?: WorldRelation[]): void;
|
|
37
|
+
close(): void;
|
|
38
|
+
park(): void;
|
|
39
|
+
mount(host: HTMLElement, options: WorldOptions): void;
|
|
40
|
+
snapshot(): WorldSnapshot;
|
|
41
|
+
activity(value: unknown): void;
|
|
42
|
+
apply(value: Camera): void;
|
|
43
|
+
change(action: string): void;
|
|
44
|
+
pause(value: boolean): void;
|
|
45
|
+
}
|
|
46
|
+
export interface WorldLease {
|
|
47
|
+
world: World;
|
|
48
|
+
release(): void;
|
|
49
|
+
}
|
|
50
|
+
export interface WorldSurface {
|
|
51
|
+
attach(host: HTMLElement, options: WorldOptions): WorldLease;
|
|
52
|
+
close(): void;
|
|
53
|
+
}
|
|
54
|
+
export declare function record(value: unknown): Record<string, unknown>;
|
|
55
|
+
export declare function eventDetail(event: Event): Record<string, unknown>;
|
|
56
|
+
export declare function copyPose(value: Pose): Pose;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function record(value) { return value !== null && typeof value === 'object' ? value : {}; }
|
|
2
|
+
export function eventDetail(event) { return record('detail' in event ? event.detail : null); }
|
|
3
|
+
export function copyPose(value) {
|
|
4
|
+
const coordinates = [value.x, value.y, value.z, value.yaw, value.pitch, ...(value.focus ? [value.focus.x, value.focus.y, value.focus.z] : [])];
|
|
5
|
+
if (!coordinates.every(Number.isFinite) || value.cameraMode !== undefined && !['overview', 'third-person'].includes(value.cameraMode))
|
|
6
|
+
throw Error('WorldPoseInvalid');
|
|
7
|
+
return { x: value.x, y: value.y, z: value.z, yaw: value.yaw, pitch: value.pitch, ...(value.focus ? { focus: { x: value.focus.x, y: value.focus.y, z: value.focus.z } } : {}), ...(value.cameraMode ? { cameraMode: value.cameraMode } : {}) };
|
|
8
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WorldContext } from './world-context.mjs';
|
|
2
|
+
export declare function objectRectWorld(ctx: WorldContext, id: string): {
|
|
3
|
+
left: number;
|
|
4
|
+
right: number;
|
|
5
|
+
top: number;
|
|
6
|
+
bottom: number;
|
|
7
|
+
} | null;
|
|
8
|
+
export declare function publishAnchorWorld(ctx: WorldContext): void;
|
|
9
|
+
export declare function syncOrbitWorld(ctx: WorldContext): void;
|
|
10
|
+
export declare function overviewWorld(ctx: WorldContext): void;
|
|
11
|
+
export declare function fitViewportWorld(ctx: WorldContext): void;
|
|
12
|
+
export declare function focusObjectWorld(ctx: WorldContext, id: string): void;
|
|
13
|
+
export declare function renderMapWorld(ctx: WorldContext): void;
|
|
14
|
+
export declare function positionBubbleWorld(ctx: WorldContext): void;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { initialPose, cameraMatrix, screenPoint, fitPose, viewCamera } from './world-model.mjs';
|
|
2
|
+
import { objectBounds } from './object-presentation.mjs';
|
|
3
|
+
import { frameLayout } from './frame-layout.mjs';
|
|
4
|
+
import { mapProjection } from './open-world-projection.mjs';
|
|
5
|
+
export function objectRectWorld(ctx, id) {
|
|
6
|
+
const item = ctx.model.items.find(item => item.id === id), width = ctx.host.clientWidth, height = ctx.host.clientHeight;
|
|
7
|
+
if (!item || !width || !height)
|
|
8
|
+
return null;
|
|
9
|
+
const [w, h, d] = objectBounds[item.form], matrix = cameraMatrix(ctx.pose, width / height, ctx.zoom), points = [];
|
|
10
|
+
for (const x of [-w / 2, w / 2])
|
|
11
|
+
for (const y of [0, h])
|
|
12
|
+
for (const z of [-d / 2, d / 2]) {
|
|
13
|
+
const point = screenPoint(matrix, item.x + x, item.y + y, item.z + z, width, height);
|
|
14
|
+
if (point?.visible)
|
|
15
|
+
points.push(point);
|
|
16
|
+
}
|
|
17
|
+
if (!points.length)
|
|
18
|
+
return null;
|
|
19
|
+
return { left: Math.min(...points.map(p => p.x)), right: Math.max(...points.map(p => p.x)), top: Math.min(...points.map(p => p.y)), bottom: Math.max(...points.map(p => p.y)) };
|
|
20
|
+
}
|
|
21
|
+
export function publishAnchorWorld(ctx) { if (ctx.anchorId)
|
|
22
|
+
ctx.shell?.dispatchEvent(new ctx.window.CustomEvent('world-anchor', { detail: { id: ctx.anchorId, bounds: ctx.objectRect(ctx.anchorId) } })); }
|
|
23
|
+
export function syncOrbitWorld(ctx) { ctx.syncing = true; viewCamera(ctx.pose, Math.max(.1, ctx.host.clientWidth / Math.max(1, ctx.host.clientHeight)), ctx.zoom, ctx.orbitCamera); const focus = ctx.pose.focus ?? { x: ctx.pose.x, y: ctx.pose.y, z: ctx.pose.z - 8 }; ctx.orbit.target.set(focus.x, focus.y, focus.z); ctx.orbit.update(); ctx.syncing = false; }
|
|
24
|
+
export function overviewWorld(ctx) { ctx.navigation = 'overview'; ctx.navigationSelect.value = ctx.navigation; ctx.walk.hidden = true; ctx.lastWidth = ctx.host.clientWidth; ctx.lastHeight = ctx.host.clientHeight; ctx.pose = fitPose(ctx.model.items, ctx.lastWidth / Math.max(1, ctx.lastHeight)); ctx.needsFit = false; ctx.stopInput(); ctx.syncOrbit(); ctx.invalidate(); }
|
|
25
|
+
export function fitViewportWorld(ctx) { const width = ctx.host.clientWidth, height = ctx.host.clientHeight; if (!width || !height || (ctx.lastWidth === width && ctx.lastHeight === height))
|
|
26
|
+
return; ctx.lastWidth = width; ctx.lastHeight = height; if (ctx.navigation === 'overview') {
|
|
27
|
+
ctx.pose = fitPose(ctx.model.items, width / height);
|
|
28
|
+
ctx.syncOrbit();
|
|
29
|
+
} ctx.invalidate(); }
|
|
30
|
+
export function focusObjectWorld(ctx, id) {
|
|
31
|
+
const item = ctx.model.items.find(i => i.id === id);
|
|
32
|
+
if (!item)
|
|
33
|
+
return;
|
|
34
|
+
ctx.selectedId = id;
|
|
35
|
+
ctx.wrap.dataset.selectedId = id;
|
|
36
|
+
for (const [ref, node] of ctx.labelNodes)
|
|
37
|
+
node.dataset.selected = String(ref === id);
|
|
38
|
+
ctx.renderMap();
|
|
39
|
+
ctx.pose = ctx.navigation === 'overview' ? fitPose([item], ctx.host.clientWidth / Math.max(1, ctx.host.clientHeight)) : { ...initialPose(), cameraMode: 'third-person', x: item.x, y: item.y + 1.85, z: item.z + 6, pitch: 0 };
|
|
40
|
+
ctx.syncOrbit();
|
|
41
|
+
ctx.invalidate();
|
|
42
|
+
}
|
|
43
|
+
export function renderMapWorld(ctx) {
|
|
44
|
+
const projected = mapProjection(ctx.model.items, { selected: ctx.selectedId, marked: ctx.markedId }), items = projected.items;
|
|
45
|
+
ctx.mapNote.textContent = projected.omitted ? `${items.length} of ${projected.total} objects shown. Narrow this view to see the rest.` : `${items.length} objects in this view`;
|
|
46
|
+
ctx.mapButton.disabled = !ctx.selectedId || !ctx.model.items.some(item => item.id === ctx.selectedId);
|
|
47
|
+
ctx.mapButton.textContent = ctx.selectedId === ctx.markedId ? 'Clear marker' : 'Mark selected';
|
|
48
|
+
ctx.mapList.replaceChildren();
|
|
49
|
+
if (!items.length)
|
|
50
|
+
return;
|
|
51
|
+
const xs = items.map(item => item.x), zs = items.map(item => item.z), minX = Math.min(...xs), maxX = Math.max(...xs), minZ = Math.min(...zs), maxZ = Math.max(...zs);
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
const node = ctx.make('button', 'world-map-point', (item.title ?? item.id).slice(0, 1).toUpperCase());
|
|
54
|
+
node.type = 'button';
|
|
55
|
+
node.title = item.title ?? item.id;
|
|
56
|
+
node.dataset.mapId = item.id;
|
|
57
|
+
node.dataset.selected = String(item.selected);
|
|
58
|
+
node.dataset.marked = String(item.marked);
|
|
59
|
+
node.setAttribute('aria-label', 'Find ' + item.title + ' on map');
|
|
60
|
+
node.style.left = (10 + 80 * (item.x - minX) / Math.max(1, maxX - minX)) + '%';
|
|
61
|
+
node.style.top = (10 + 80 * (item.z - minZ) / Math.max(1, maxZ - minZ)) + '%';
|
|
62
|
+
ctx.mapList.append(node);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function positionBubbleWorld(ctx) {
|
|
66
|
+
if (!ctx.conversationId)
|
|
67
|
+
return;
|
|
68
|
+
if (!ctx.model.items.some(item => item.id === ctx.conversationId)) {
|
|
69
|
+
ctx.closeConversation();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const { panel } = frameLayout(ctx.host.clientWidth, ctx.host.clientHeight, 'actions', ctx.objectRect(ctx.conversationId));
|
|
73
|
+
Object.assign(ctx.bubble.style, { left: panel.x + 'px', top: panel.y + 'px', width: panel.width + 'px', height: panel.height + 'px', maxHeight: panel.height + 'px' });
|
|
74
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Pose } from './world-types.mjs';
|
|
2
|
+
export interface FrameContext {
|
|
3
|
+
frame: number | null;
|
|
4
|
+
closed: boolean;
|
|
5
|
+
attached: boolean;
|
|
6
|
+
paused: boolean;
|
|
7
|
+
lost: boolean;
|
|
8
|
+
dirty: boolean;
|
|
9
|
+
last: number;
|
|
10
|
+
pose: Pose;
|
|
11
|
+
keys: Set<unknown>;
|
|
12
|
+
document: Pick<Document, 'hidden'>;
|
|
13
|
+
window: Pick<Window, 'requestAnimationFrame'>;
|
|
14
|
+
inputBlocked(): boolean;
|
|
15
|
+
tick(now: number): void;
|
|
16
|
+
draw(): void;
|
|
17
|
+
fail(error: unknown): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function tickFrame(ctx: FrameContext, now: number): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { worldLimits, movePose } from './world-model.mjs';
|
|
2
|
+
export function tickFrame(ctx, now) {
|
|
3
|
+
try {
|
|
4
|
+
ctx.frame = null;
|
|
5
|
+
if (ctx.closed || !ctx.attached || ctx.paused || ctx.lost || ctx.document.hidden)
|
|
6
|
+
return;
|
|
7
|
+
if (ctx.inputBlocked())
|
|
8
|
+
ctx.keys.clear();
|
|
9
|
+
const moving = ctx.keys.size > 0;
|
|
10
|
+
if (now - ctx.last < 1000 / worldLimits.fps) {
|
|
11
|
+
if (ctx.dirty || moving)
|
|
12
|
+
ctx.frame = ctx.window.requestAnimationFrame(ctx.tick);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (moving) {
|
|
16
|
+
ctx.pose = movePose(ctx.pose, Number(ctx.keys.has('forward')) - Number(ctx.keys.has('back')), Number(ctx.keys.has('right')) - Number(ctx.keys.has('left')), ctx.last ? (now - ctx.last) / 1000 : 1 / 30, Number(ctx.keys.has('rise')) - Number(ctx.keys.has('descend')));
|
|
17
|
+
ctx.dirty = true;
|
|
18
|
+
}
|
|
19
|
+
ctx.last = now;
|
|
20
|
+
if (ctx.dirty) {
|
|
21
|
+
ctx.dirty = false;
|
|
22
|
+
ctx.draw();
|
|
23
|
+
}
|
|
24
|
+
if (moving)
|
|
25
|
+
ctx.frame = ctx.window.requestAnimationFrame(ctx.tick);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
ctx.fail(error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { WorldOptions, WorldSnapshot } from './world-api.mjs';
|
|
2
|
+
import type { WorldObject, WorldRelation } from './world-types.mjs';
|
|
3
|
+
import type { Camera } from '@zixcel/projection-client';
|
|
4
|
+
export interface WorldCommands {
|
|
5
|
+
objectRect: (id: string) => {
|
|
6
|
+
left: number;
|
|
7
|
+
right: number;
|
|
8
|
+
top: number;
|
|
9
|
+
bottom: number;
|
|
10
|
+
} | null;
|
|
11
|
+
publishAnchor: () => void;
|
|
12
|
+
filterDirectory: () => void;
|
|
13
|
+
syncOrbit: () => void;
|
|
14
|
+
overview: () => void;
|
|
15
|
+
fitViewport: () => void;
|
|
16
|
+
focusObject: (id: string) => void;
|
|
17
|
+
renderMap: () => void;
|
|
18
|
+
closeConversation: () => void;
|
|
19
|
+
activate: (id: string | null | undefined) => void;
|
|
20
|
+
positionBubble: () => void;
|
|
21
|
+
stopInput: () => void;
|
|
22
|
+
stopFrame: () => void;
|
|
23
|
+
invalidate: () => void;
|
|
24
|
+
tick: (now: number) => void;
|
|
25
|
+
draw: () => void;
|
|
26
|
+
fail: (error: unknown) => void;
|
|
27
|
+
initialize: () => void;
|
|
28
|
+
renderUpdates: () => void;
|
|
29
|
+
update: (objects: WorldObject[], relations?: WorldRelation[]) => void;
|
|
30
|
+
onKey: (event: KeyboardEvent) => void;
|
|
31
|
+
snapshot: () => WorldSnapshot;
|
|
32
|
+
park: () => void;
|
|
33
|
+
mount: (nextHost: HTMLElement, options: WorldOptions) => void;
|
|
34
|
+
close: () => void;
|
|
35
|
+
activity: (value: unknown) => void;
|
|
36
|
+
apply: (value: Camera) => void;
|
|
37
|
+
change: (action: string) => void;
|
|
38
|
+
pause: (value: boolean) => void;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { WorldSnapshot, ActivityPresentation, RolePresentation, ActivityView } from './world-api.mjs';
|
|
2
|
+
import type { Pose, WorldObject, WorldRelation, WorldModel, WorldItem, Gpu, ActorRange } from './world-types.mjs';
|
|
3
|
+
import type { SpatialState, SpatialView } from './spatial-patterns.mjs';
|
|
4
|
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
5
|
+
import type { PerspectiveCamera } from 'three';
|
|
6
|
+
import type { WorldCommands } from './world-commands.mjs';
|
|
7
|
+
export interface WorldContext extends WorldCommands {
|
|
8
|
+
host: HTMLElement;
|
|
9
|
+
objects: WorldObject[];
|
|
10
|
+
relations: WorldRelation[];
|
|
11
|
+
restored: WorldSnapshot | null | undefined;
|
|
12
|
+
onActivate: (id: string) => void;
|
|
13
|
+
conversation: ((item: WorldObject, activate: () => void) => HTMLElement | null) | null;
|
|
14
|
+
workActivity: unknown;
|
|
15
|
+
target: string | null;
|
|
16
|
+
pattern: string;
|
|
17
|
+
context: string | null;
|
|
18
|
+
activityPresentation: ActivityPresentation;
|
|
19
|
+
document: Document;
|
|
20
|
+
browser: (Window & typeof globalThis) | null;
|
|
21
|
+
window: Window & typeof globalThis;
|
|
22
|
+
controller: AbortController;
|
|
23
|
+
signal: AbortSignal;
|
|
24
|
+
make: <K extends keyof HTMLElementTagNameMap>(tag: K, className: string, text?: string) => HTMLElementTagNameMap[K];
|
|
25
|
+
wrap: HTMLElement;
|
|
26
|
+
canvas: HTMLCanvasElement;
|
|
27
|
+
labels: HTMLDivElement;
|
|
28
|
+
hud: HTMLDivElement;
|
|
29
|
+
aim: HTMLSpanElement;
|
|
30
|
+
hint: HTMLButtonElement;
|
|
31
|
+
status: HTMLParagraphElement;
|
|
32
|
+
tools: HTMLDivElement;
|
|
33
|
+
nearby: HTMLDetailsElement;
|
|
34
|
+
list: HTMLDivElement;
|
|
35
|
+
summary: HTMLElement;
|
|
36
|
+
roleStatus: (value: unknown, id: string) => RolePresentation | null;
|
|
37
|
+
engine: ActivityView;
|
|
38
|
+
spacing: string;
|
|
39
|
+
placedSpacing: string | null;
|
|
40
|
+
pose: Pose;
|
|
41
|
+
model: WorldModel<WorldObject>;
|
|
42
|
+
gpu: Gpu | null;
|
|
43
|
+
geometry: Float32Array<ArrayBufferLike> | null;
|
|
44
|
+
actorRanges: ActorRange[];
|
|
45
|
+
closed: boolean;
|
|
46
|
+
attached: boolean;
|
|
47
|
+
lost: boolean;
|
|
48
|
+
dirty: boolean;
|
|
49
|
+
frame: number | null;
|
|
50
|
+
motionTimer: number | null;
|
|
51
|
+
last: number;
|
|
52
|
+
frames: number;
|
|
53
|
+
drag: {
|
|
54
|
+
id: number;
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
startX: number;
|
|
58
|
+
startY: number;
|
|
59
|
+
} | null;
|
|
60
|
+
hovered: string | null;
|
|
61
|
+
paused: boolean;
|
|
62
|
+
zoom: number;
|
|
63
|
+
projected: ({
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
depth: number;
|
|
67
|
+
visible: boolean;
|
|
68
|
+
} & {
|
|
69
|
+
item: WorldItem;
|
|
70
|
+
})[];
|
|
71
|
+
labelNodes: Map<string, HTMLButtonElement>;
|
|
72
|
+
directoryNodes: Map<string, HTMLDivElement>;
|
|
73
|
+
geometryKey: string | null;
|
|
74
|
+
uploads: number;
|
|
75
|
+
initializations: number;
|
|
76
|
+
navigation: string;
|
|
77
|
+
needsFit: boolean;
|
|
78
|
+
lastWidth: number;
|
|
79
|
+
lastHeight: number;
|
|
80
|
+
selectedId: string | null;
|
|
81
|
+
markedId: string | null;
|
|
82
|
+
recent: Map<string, {
|
|
83
|
+
title: string;
|
|
84
|
+
kind: string;
|
|
85
|
+
}>;
|
|
86
|
+
poses: Map<string | null, WorldSnapshot>;
|
|
87
|
+
fittedPopulation: boolean;
|
|
88
|
+
sourceObjects: WorldObject[];
|
|
89
|
+
sourceRelations: WorldRelation[];
|
|
90
|
+
view: SpatialState;
|
|
91
|
+
projection: SpatialView | null;
|
|
92
|
+
bubble: HTMLElement;
|
|
93
|
+
conversationId: string | null;
|
|
94
|
+
returnFocus: HTMLElement | null;
|
|
95
|
+
anchorId: string | null;
|
|
96
|
+
shell: Element | null;
|
|
97
|
+
map: HTMLDetailsElement;
|
|
98
|
+
mapList: HTMLDivElement;
|
|
99
|
+
mapNote: HTMLElement;
|
|
100
|
+
mapButton: HTMLButtonElement;
|
|
101
|
+
keys: Set<unknown>;
|
|
102
|
+
inputBlocked: () => boolean;
|
|
103
|
+
button: (text: string, label: string, action: () => void, parent?: HTMLElement) => HTMLButtonElement;
|
|
104
|
+
inspect: HTMLButtonElement;
|
|
105
|
+
reset: HTMLButtonElement;
|
|
106
|
+
settings: HTMLDivElement;
|
|
107
|
+
breadcrumb: HTMLDivElement;
|
|
108
|
+
location: HTMLOutputElement;
|
|
109
|
+
pagination: HTMLDivElement;
|
|
110
|
+
selector: (label: string, entries: readonly (readonly string[])[], change: (value: string) => void) => HTMLSelectElement;
|
|
111
|
+
navigationSelect: HTMLSelectElement;
|
|
112
|
+
patternSelect: HTMLSelectElement;
|
|
113
|
+
groupSelect: HTMLSelectElement;
|
|
114
|
+
legend: HTMLParagraphElement;
|
|
115
|
+
back: HTMLButtonElement;
|
|
116
|
+
previous: HTMLButtonElement;
|
|
117
|
+
pageCount: HTMLOutputElement;
|
|
118
|
+
nextPage: HTMLButtonElement;
|
|
119
|
+
updates: HTMLDetailsElement;
|
|
120
|
+
updatesTitle: HTMLElement;
|
|
121
|
+
updateList: HTMLDivElement;
|
|
122
|
+
updateHelp: HTMLParagraphElement;
|
|
123
|
+
query: HTMLInputElement;
|
|
124
|
+
noMatches: HTMLParagraphElement;
|
|
125
|
+
orbitCamera: PerspectiveCamera;
|
|
126
|
+
orbit: OrbitControls<PerspectiveCamera>;
|
|
127
|
+
syncing: boolean;
|
|
128
|
+
enableOrbit: () => void;
|
|
129
|
+
setText: (node: Element, value: string | undefined) => void;
|
|
130
|
+
walk: HTMLDivElement;
|
|
131
|
+
resize: ResizeObserver;
|
|
132
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Camera } from '@zixcel/projection-client';
|
|
2
|
+
import type { WorldOptions, WorldSnapshot } from './world-api.mjs';
|
|
3
|
+
import type { WorldObject, WorldRelation } from './world-types.mjs';
|
|
4
|
+
import type { WorldContext } from './world-context.mjs';
|
|
5
|
+
export interface WorldController extends WorldContext {
|
|
6
|
+
}
|
|
7
|
+
export declare class WorldController {
|
|
8
|
+
constructor(host: HTMLElement, { objects, relations, pose: restored, onActivate, conversation, activity, target, pattern, context, activityPresentation }?: WorldOptions);
|
|
9
|
+
objectRect: (id: string) => {
|
|
10
|
+
left: number;
|
|
11
|
+
right: number;
|
|
12
|
+
top: number;
|
|
13
|
+
bottom: number;
|
|
14
|
+
} | null;
|
|
15
|
+
publishAnchor: () => void;
|
|
16
|
+
filterDirectory: () => void;
|
|
17
|
+
syncOrbit: () => void;
|
|
18
|
+
overview: () => void;
|
|
19
|
+
fitViewport: () => void;
|
|
20
|
+
focusObject: (id: string) => void;
|
|
21
|
+
renderMap: () => void;
|
|
22
|
+
closeConversation: () => void;
|
|
23
|
+
activate: (id: string | null | undefined) => void;
|
|
24
|
+
positionBubble: () => void;
|
|
25
|
+
stopInput: () => void;
|
|
26
|
+
stopFrame: () => void;
|
|
27
|
+
invalidate: () => void;
|
|
28
|
+
tick: (now: number) => void;
|
|
29
|
+
draw: () => void;
|
|
30
|
+
fail: (error: unknown) => void;
|
|
31
|
+
initialize: () => void;
|
|
32
|
+
renderUpdates: () => void;
|
|
33
|
+
update: (objects: WorldObject[], relations?: WorldRelation[]) => void;
|
|
34
|
+
onKey: (event: KeyboardEvent) => void;
|
|
35
|
+
snapshot: () => WorldSnapshot;
|
|
36
|
+
park: () => void;
|
|
37
|
+
mount: (nextHost: HTMLElement, options: WorldOptions) => void;
|
|
38
|
+
close: () => void;
|
|
39
|
+
activity: (value: unknown) => void;
|
|
40
|
+
apply: (value: Camera) => void;
|
|
41
|
+
change: (action: string) => void;
|
|
42
|
+
pause: (value: boolean) => void;
|
|
43
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { setupWorldView } from './world-setup.mjs';
|
|
2
|
+
import { objectRectWorld, publishAnchorWorld, syncOrbitWorld, overviewWorld, fitViewportWorld, focusObjectWorld, renderMapWorld, positionBubbleWorld } from './world-camera.mjs';
|
|
3
|
+
import { filterDirectoryWorld, renderUpdatesWorld, updateWorld } from './world-projection.mjs';
|
|
4
|
+
import { closeConversationWorld, activateWorld } from './world-conversation.mjs';
|
|
5
|
+
import { stopInputWorld, stopFrameWorld, invalidateWorld, tickWorld, drawWorld, failWorld, initializeWorld } from './world-render-loop.mjs';
|
|
6
|
+
import { onKeyWorld, snapshotWorld, parkWorld, mountWorld, closeWorld, activityWorld, applyWorld, changeWorld, pauseWorld } from './world-lifecycle.mjs';
|
|
7
|
+
export class WorldController {
|
|
8
|
+
constructor(host, { objects = [], relations = [], pose: restored, onActivate = () => { }, conversation = null, activity = null, target = null, pattern = 'ground', context = null, activityPresentation = {} } = {}) {
|
|
9
|
+
this.host = host;
|
|
10
|
+
this.objects = objects;
|
|
11
|
+
this.relations = relations;
|
|
12
|
+
this.restored = restored;
|
|
13
|
+
this.onActivate = onActivate;
|
|
14
|
+
this.conversation = conversation;
|
|
15
|
+
this.workActivity = activity;
|
|
16
|
+
this.target = target;
|
|
17
|
+
this.pattern = pattern;
|
|
18
|
+
this.context = context;
|
|
19
|
+
this.activityPresentation = activityPresentation;
|
|
20
|
+
try {
|
|
21
|
+
setupWorldView(this);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
try {
|
|
25
|
+
this.close();
|
|
26
|
+
}
|
|
27
|
+
catch (cleanup) {
|
|
28
|
+
throw new AggregateError([error, cleanup], 'World setup and cleanup failed');
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
objectRect = (id) => objectRectWorld(this, id);
|
|
34
|
+
publishAnchor = () => publishAnchorWorld(this);
|
|
35
|
+
filterDirectory = () => filterDirectoryWorld(this);
|
|
36
|
+
syncOrbit = () => syncOrbitWorld(this);
|
|
37
|
+
overview = () => overviewWorld(this);
|
|
38
|
+
fitViewport = () => fitViewportWorld(this);
|
|
39
|
+
focusObject = (id) => focusObjectWorld(this, id);
|
|
40
|
+
renderMap = () => renderMapWorld(this);
|
|
41
|
+
closeConversation = () => closeConversationWorld(this);
|
|
42
|
+
activate = (id) => activateWorld(this, id);
|
|
43
|
+
positionBubble = () => positionBubbleWorld(this);
|
|
44
|
+
stopInput = () => stopInputWorld(this);
|
|
45
|
+
stopFrame = () => stopFrameWorld(this);
|
|
46
|
+
invalidate = () => invalidateWorld(this);
|
|
47
|
+
tick = (now) => tickWorld(this, now);
|
|
48
|
+
draw = () => drawWorld(this);
|
|
49
|
+
fail = (error) => failWorld(this, error);
|
|
50
|
+
initialize = () => initializeWorld(this);
|
|
51
|
+
renderUpdates = () => renderUpdatesWorld(this);
|
|
52
|
+
update = (objects, relations = []) => updateWorld(this, objects, relations);
|
|
53
|
+
onKey = (event) => onKeyWorld(this, event);
|
|
54
|
+
snapshot = () => snapshotWorld(this);
|
|
55
|
+
park = () => parkWorld(this);
|
|
56
|
+
mount = (nextHost, options) => mountWorld(this, nextHost, options);
|
|
57
|
+
close = () => closeWorld(this);
|
|
58
|
+
activity = (value) => activityWorld(this, value);
|
|
59
|
+
apply = (value) => applyWorld(this, value);
|
|
60
|
+
change = (action) => changeWorld(this, action);
|
|
61
|
+
pause = (value) => pauseWorld(this, value);
|
|
62
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function closeConversationWorld(ctx) { ctx.conversationId = null; ctx.bubble.hidden = true; ctx.bubble.replaceChildren(); if (ctx.returnFocus?.isConnected)
|
|
2
|
+
ctx.returnFocus.focus({ preventScroll: true }); ctx.returnFocus = null; ctx.invalidate(); }
|
|
3
|
+
export function activateWorld(ctx, id) {
|
|
4
|
+
if (!id)
|
|
5
|
+
return;
|
|
6
|
+
const item = ctx.model.items.find(item => item.id === id);
|
|
7
|
+
if (!item || item.disabled)
|
|
8
|
+
return;
|
|
9
|
+
ctx.selectedId = id;
|
|
10
|
+
ctx.wrap.dataset.selectedId = id;
|
|
11
|
+
for (const [ref, node] of ctx.labelNodes)
|
|
12
|
+
node.dataset.selected = String(ref === id);
|
|
13
|
+
ctx.renderMap();
|
|
14
|
+
ctx.stopInput();
|
|
15
|
+
ctx.nearby.open = false;
|
|
16
|
+
ctx.map.open = false;
|
|
17
|
+
if (item.kind === 'group') {
|
|
18
|
+
ctx.view = { ...ctx.view, group: ctx.view.pattern === 'garden' && ctx.view.group === item.id ? null : item.id, page: 0 };
|
|
19
|
+
ctx.needsFit = ctx.view.pattern !== 'garden';
|
|
20
|
+
ctx.update(ctx.sourceObjects, ctx.sourceRelations);
|
|
21
|
+
if (ctx.view.pattern === 'garden')
|
|
22
|
+
ctx.focusObject(item.id);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
ctx.recent.delete(id);
|
|
26
|
+
ctx.renderUpdates();
|
|
27
|
+
const body = ctx.conversation?.(item, () => { ctx.closeConversation(); ctx.onActivate(id); });
|
|
28
|
+
if (!body) {
|
|
29
|
+
ctx.onActivate(id);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
ctx.returnFocus = ctx.document.activeElement instanceof ctx.window.HTMLElement ? ctx.document.activeElement : null;
|
|
33
|
+
ctx.conversationId = id;
|
|
34
|
+
ctx.bubble.setAttribute('aria-label', 'Conversation with ' + item.title);
|
|
35
|
+
const title = ctx.make('strong', '', item.title), dismiss = ctx.make('button', '', '×');
|
|
36
|
+
dismiss.type = 'button';
|
|
37
|
+
dismiss.setAttribute('aria-label', 'Close conversation');
|
|
38
|
+
dismiss.addEventListener('click', ctx.closeConversation, { once: true });
|
|
39
|
+
const head = ctx.make('header', '');
|
|
40
|
+
head.append(title, dismiss);
|
|
41
|
+
ctx.bubble.replaceChildren(head, body);
|
|
42
|
+
ctx.bubble.hidden = false;
|
|
43
|
+
ctx.bubble.focus({ preventScroll: true });
|
|
44
|
+
ctx.positionBubble();
|
|
45
|
+
ctx.invalidate();
|
|
46
|
+
}
|