@zixcel/dom-renderer 0.10.4 → 0.10.5
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 +6 -0
- package/package.json +6 -4
- 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,120 @@
|
|
|
1
|
+
import { eventDetail } from './world-api.mjs';
|
|
2
|
+
import { movePose, lookPose, clamp, pickWorld } from './world-model.mjs';
|
|
3
|
+
import { keyDirection } from './world-input.mjs';
|
|
4
|
+
export function bindWorldEvents(ctx) {
|
|
5
|
+
ctx.hint.addEventListener('click', () => { ctx.nearby.open = true; }, { signal: ctx.signal });
|
|
6
|
+
ctx.shell?.addEventListener('world-anchor-request', event => { const detail = eventDetail(event); if (detail.id === null || typeof detail.id === 'string') {
|
|
7
|
+
ctx.anchorId = detail.id;
|
|
8
|
+
ctx.publishAnchor();
|
|
9
|
+
} }, { signal: ctx.signal });
|
|
10
|
+
ctx.query.addEventListener('input', ctx.filterDirectory, { signal: ctx.signal });
|
|
11
|
+
ctx.host.closest('.delivery-shell')?.addEventListener('frame-panel-open', () => { ctx.nearby.open = false; ctx.map.open = false; ctx.updates.open = false; ctx.closeConversation(); }, { signal: ctx.signal });
|
|
12
|
+
ctx.nearby.addEventListener('toggle', () => { ctx.map.hidden = ctx.nearby.open; if (ctx.nearby.open) {
|
|
13
|
+
ctx.map.open = false;
|
|
14
|
+
ctx.updates.open = false;
|
|
15
|
+
ctx.closeConversation();
|
|
16
|
+
} }, { signal: ctx.signal });
|
|
17
|
+
ctx.updates.addEventListener('toggle', () => { if (ctx.updates.open) {
|
|
18
|
+
ctx.nearby.open = false;
|
|
19
|
+
ctx.closeConversation();
|
|
20
|
+
} }, { signal: ctx.signal });
|
|
21
|
+
ctx.map.addEventListener('toggle', () => { if (ctx.map.open) {
|
|
22
|
+
ctx.nearby.open = false;
|
|
23
|
+
ctx.updates.open = false;
|
|
24
|
+
ctx.closeConversation();
|
|
25
|
+
ctx.stopInput();
|
|
26
|
+
} }, { signal: ctx.signal });
|
|
27
|
+
ctx.orbit.addEventListener('change', () => { if (ctx.syncing)
|
|
28
|
+
return; ctx.pose = { x: ctx.orbitCamera.position.x, y: ctx.orbitCamera.position.y, z: ctx.orbitCamera.position.z, yaw: -ctx.orbitCamera.rotation.y, pitch: ctx.orbitCamera.rotation.x, focus: { x: ctx.orbit.target.x, y: ctx.orbit.target.y, z: ctx.orbit.target.z } }; ctx.needsFit = false; ctx.invalidate(); });
|
|
29
|
+
ctx.mapList.addEventListener('click', event => { const id = (event.target instanceof ctx.window.Element ? event.target.closest('[data-map-id]') : null)?.dataset.mapId; if (id)
|
|
30
|
+
ctx.focusObject(id); }, { signal: ctx.signal });
|
|
31
|
+
ctx.mapButton.addEventListener('click', () => { if (!ctx.selectedId)
|
|
32
|
+
return; ctx.markedId = ctx.markedId === ctx.selectedId ? null : ctx.selectedId; ctx.wrap.dataset.markedId = ctx.markedId ?? ''; for (const [id, node] of ctx.labelNodes)
|
|
33
|
+
node.dataset.marked = String(id === ctx.markedId); ctx.renderMap(); }, { signal: ctx.signal });
|
|
34
|
+
ctx.canvas.addEventListener('pointerdown', ctx.enableOrbit, { signal: ctx.signal, capture: true });
|
|
35
|
+
ctx.canvas.addEventListener('wheel', ctx.enableOrbit, { signal: ctx.signal, capture: true });
|
|
36
|
+
ctx.updateList.addEventListener('click', event => {
|
|
37
|
+
const id = (event.target instanceof ctx.window.Element ? event.target.closest('[data-change-id]') : null)?.dataset.changeId;
|
|
38
|
+
if (!id)
|
|
39
|
+
return;
|
|
40
|
+
if (ctx.view.pattern === 'garden' && !ctx.model.items.some(item => item.id === id)) {
|
|
41
|
+
ctx.updates.open = false;
|
|
42
|
+
ctx.onActivate(id);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const group = ctx.projection?.groups.find(group => group.memberIds.includes(id));
|
|
46
|
+
if (group) {
|
|
47
|
+
ctx.view = { ...ctx.view, group: group.id, page: 0 };
|
|
48
|
+
ctx.update(ctx.sourceObjects, ctx.sourceRelations);
|
|
49
|
+
const index = (ctx.view.pattern === 'garden' ? group.memberIds : [...ctx.sourceObjects.filter(i => i.kind === 'character').map(i => i.id), ...group.memberIds]).indexOf(id);
|
|
50
|
+
if (index >= 24) {
|
|
51
|
+
ctx.view = { ...ctx.view, page: Math.floor(index / 24) };
|
|
52
|
+
ctx.update(ctx.sourceObjects, ctx.sourceRelations);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
ctx.updates.open = false;
|
|
56
|
+
ctx.focusObject(id);
|
|
57
|
+
}, { signal: ctx.signal });
|
|
58
|
+
ctx.labels.addEventListener('click', event => ctx.activate((event.target instanceof ctx.window.Element ? event.target.closest('[data-world-id]') : null)?.dataset.worldId), { signal: ctx.signal });
|
|
59
|
+
ctx.list.addEventListener('click', event => {
|
|
60
|
+
const control = (event.target instanceof ctx.window.Element ? event.target.closest('button') : null), id = control?.closest('[data-directory-id]')?.dataset.directoryId;
|
|
61
|
+
if (!id || !control)
|
|
62
|
+
return;
|
|
63
|
+
if (control.dataset.focus === 'true') {
|
|
64
|
+
ctx.focusObject(id);
|
|
65
|
+
ctx.nearby.open = false;
|
|
66
|
+
ctx.canvas.focus({ preventScroll: true });
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
ctx.activate(id);
|
|
70
|
+
}, { signal: ctx.signal });
|
|
71
|
+
ctx.canvas.addEventListener('keydown', ctx.onKey, { signal: ctx.signal });
|
|
72
|
+
ctx.window.addEventListener('keyup', event => { const key = keyDirection(event.code); if (key) {
|
|
73
|
+
ctx.keys.delete(key);
|
|
74
|
+
ctx.invalidate();
|
|
75
|
+
} }, { signal: ctx.signal });
|
|
76
|
+
ctx.bubble.addEventListener('keydown', event => { if (event.key === 'Escape') {
|
|
77
|
+
event.stopPropagation();
|
|
78
|
+
ctx.closeConversation();
|
|
79
|
+
} }, { signal: ctx.signal });
|
|
80
|
+
ctx.canvas.addEventListener('pointerdown', event => { if (!event.isPrimary) {
|
|
81
|
+
ctx.drag = null;
|
|
82
|
+
return;
|
|
83
|
+
} if (event.button !== 0 || ctx.inputBlocked())
|
|
84
|
+
return; ctx.canvas.focus({ preventScroll: true }); ctx.drag = { id: event.pointerId, x: event.clientX, y: event.clientY, startX: event.clientX, startY: event.clientY }; ctx.canvas.setPointerCapture(event.pointerId); }, { signal: ctx.signal });
|
|
85
|
+
ctx.canvas.addEventListener('pointermove', event => { if (!ctx.drag || ctx.drag.id !== event.pointerId)
|
|
86
|
+
return; if (ctx.navigation === 'explore')
|
|
87
|
+
ctx.pose = lookPose(ctx.pose, event.clientX - ctx.drag.x, event.clientY - ctx.drag.y); ctx.drag.x = event.clientX; ctx.drag.y = event.clientY; ctx.invalidate(); }, { signal: ctx.signal });
|
|
88
|
+
ctx.canvas.addEventListener('pointerup', event => { if (!ctx.drag || ctx.drag.id !== event.pointerId)
|
|
89
|
+
return; const clicked = Math.hypot(event.clientX - ctx.drag.startX, event.clientY - ctx.drag.startY) < 5; ctx.drag = null; if (clicked) {
|
|
90
|
+
const r = ctx.canvas.getBoundingClientRect();
|
|
91
|
+
ctx.activate(pickWorld(ctx.model.items, ctx.pose, event.clientX - r.left, event.clientY - r.top, r.width, r.height, ctx.zoom));
|
|
92
|
+
} }, { signal: ctx.signal, capture: true });
|
|
93
|
+
for (const type of ['pointercancel', 'lostpointercapture'])
|
|
94
|
+
ctx.canvas.addEventListener(type, () => ctx.drag = null, { signal: ctx.signal });
|
|
95
|
+
ctx.canvas.addEventListener('wheel', event => { if (ctx.navigation === 'overview' || event.ctrlKey || event.metaKey || ctx.inputBlocked() || !Number.isFinite(event.deltaY))
|
|
96
|
+
return; event.preventDefault(); const amount = clamp(-event.deltaY * (event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? ctx.host.clientHeight : 1) / 100, -2, 2); ctx.pose = { ...ctx.pose, x: clamp(ctx.pose.x + Math.sin(ctx.pose.yaw) * amount, -28, 28), z: clamp(ctx.pose.z - Math.cos(ctx.pose.yaw) * amount, -350, 28) }; ctx.invalidate(); }, { signal: ctx.signal, passive: false });
|
|
97
|
+
for (const [direction, icon] of [['forward', '↑'], ['left', '←'], ['back', '↓'], ['right', '→'], ['rise', '⇧'], ['descend', '⇩']]) {
|
|
98
|
+
const b = ctx.button(icon, 'Walk ' + direction, () => { }, ctx.walk);
|
|
99
|
+
b.addEventListener('pointerdown', event => { if (event.button || ctx.inputBlocked())
|
|
100
|
+
return; event.preventDefault(); b.setPointerCapture(event.pointerId); ctx.keys.add(direction); ctx.invalidate(); }, { signal: ctx.signal });
|
|
101
|
+
for (const type of ['pointerup', 'pointercancel', 'lostpointercapture'])
|
|
102
|
+
b.addEventListener(type, () => { ctx.keys.delete(direction); ctx.invalidate(); }, { signal: ctx.signal });
|
|
103
|
+
b.addEventListener('click', event => { if (event.detail === 0 && !ctx.inputBlocked()) {
|
|
104
|
+
ctx.pose = movePose(ctx.pose, direction === 'forward' ? 1 : direction === 'back' ? -1 : 0, direction === 'right' ? 1 : direction === 'left' ? -1 : 0, .05, direction === 'rise' ? 1 : direction === 'descend' ? -1 : 0);
|
|
105
|
+
ctx.invalidate();
|
|
106
|
+
} }, { signal: ctx.signal });
|
|
107
|
+
}
|
|
108
|
+
ctx.nearby.addEventListener('toggle', () => { if (ctx.nearby.open)
|
|
109
|
+
ctx.stopInput(); }, { signal: ctx.signal });
|
|
110
|
+
ctx.window.addEventListener('blur', () => { ctx.wrap.dataset.motionPaused = 'true'; ctx.stopInput(); ctx.stopFrame(); }, { signal: ctx.signal });
|
|
111
|
+
ctx.window.addEventListener('focus', () => { ctx.wrap.dataset.motionPaused = 'false'; ctx.invalidate(); }, { signal: ctx.signal });
|
|
112
|
+
ctx.document.addEventListener('visibilitychange', () => { ctx.wrap.dataset.motionPaused = String(ctx.document.hidden); ctx.stopInput(); if (ctx.document.hidden)
|
|
113
|
+
ctx.stopFrame();
|
|
114
|
+
else
|
|
115
|
+
ctx.invalidate(); }, { signal: ctx.signal });
|
|
116
|
+
ctx.window.addEventListener('pagehide', () => { ctx.paused = true; ctx.stopInput(); ctx.stopFrame(); }, { signal: ctx.signal });
|
|
117
|
+
ctx.window.addEventListener('pageshow', () => { ctx.paused = false; ctx.invalidate(); }, { signal: ctx.signal });
|
|
118
|
+
ctx.canvas.addEventListener('webglcontextlost', event => { event.preventDefault(); ctx.lost = true; ctx.gpu?.close(); ctx.gpu = null; ctx.fail(Error('WorldGraphicsContextLost')); }, { signal: ctx.signal });
|
|
119
|
+
ctx.canvas.addEventListener('webglcontextrestored', ctx.initialize, { signal: ctx.signal });
|
|
120
|
+
}
|
package/src/world-gpu.d.mts
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
motionStates(states: any): void;
|
|
5
|
-
draw(pose: any, aspect: any, zoom: any, size: any, milliseconds?: number, visibleActors?: Set<any>): void;
|
|
6
|
-
readonly count: any;
|
|
7
|
-
readonly stats: {
|
|
8
|
-
calls: any;
|
|
9
|
-
triangles: any;
|
|
10
|
-
geometries: any;
|
|
11
|
-
textures: any;
|
|
12
|
-
};
|
|
13
|
-
close: () => void;
|
|
14
|
-
};
|
|
15
|
-
export function worldGeometry(model: any, actors?: any[]): Float32Array<ArrayBuffer>;
|
|
1
|
+
import type { Gpu, ActorRange, WorldModel } from './world-types.mjs';
|
|
2
|
+
export declare function createGpu(canvas: HTMLCanvasElement): Gpu;
|
|
3
|
+
export declare function worldGeometry(model: WorldModel, actors?: ActorRange[]): Float32Array<ArrayBuffer>;
|
package/src/world-gpu.mjs
CHANGED
|
@@ -1,79 +1,164 @@
|
|
|
1
|
+
import { disposeGraphics } from './graphics-disposal.mjs';
|
|
2
|
+
import { admitGeometry } from './geometry-policy.mjs';
|
|
1
3
|
// One instanced cube mesh, one program, no textures, physics, asset fetches or semantic state.
|
|
2
|
-
import {WebGLRenderer,Scene,PerspectiveCamera,BoxGeometry,MeshLambertMaterial,InstancedMesh,Matrix4,Color,DirectionalLight,AmbientLight,Fog} from 'three'
|
|
3
|
-
import {worldLimits,viewCamera} from './world-model.mjs'
|
|
4
|
-
import {objectTemplates} from './object-presentation.mjs'
|
|
5
|
-
import {npcMotion} from './npc-motion.mjs'
|
|
6
|
-
export function createGpu(canvas){
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
4
|
+
import { WebGLRenderer, Scene, PerspectiveCamera, BoxGeometry, MeshLambertMaterial, InstancedMesh, Matrix4, Color, DirectionalLight, AmbientLight, Fog } from 'three';
|
|
5
|
+
import { worldLimits, viewCamera } from './world-model.mjs';
|
|
6
|
+
import { objectTemplates } from './object-presentation.mjs';
|
|
7
|
+
import { npcMotion } from './npc-motion.mjs';
|
|
8
|
+
export function createGpu(canvas) {
|
|
9
|
+
let disposableRenderer = null, disposableGeometry = null, disposableMaterial = null, disposableMesh = null;
|
|
10
|
+
let closed = false, bufferWidth = 0, bufferHeight = 0, previous = null, pendingStart = Infinity, pendingEnd = 0, baseCount = 0, actors = [], actorStates = new Map(), lastVisible = new Set();
|
|
11
|
+
const scene = new Scene(), camera = new PerspectiveCamera(70, 1, .08, 120), matrix = new Matrix4(), color = new Color();
|
|
12
|
+
function close() { if (closed)
|
|
13
|
+
return; closed = true; previous = null; actors = []; actorStates.clear(); lastVisible.clear(); disposeGraphics([() => disposableMesh?.dispose(), () => disposableGeometry?.dispose(), () => disposableMaterial?.dispose(), () => scene.clear(), () => disposableRenderer?.dispose()]); }
|
|
14
|
+
try {
|
|
15
|
+
const renderer = disposableRenderer = new WebGLRenderer({ canvas, alpha: false, antialias: false, stencil: false, preserveDrawingBuffer: false, powerPreference: 'low-power' });
|
|
16
|
+
const background = new Color('#b8d4de');
|
|
17
|
+
let fog = new Fog('#b8d4de', 16, 64);
|
|
18
|
+
scene.background = background;
|
|
19
|
+
scene.fog = fog;
|
|
20
|
+
scene.add(new AmbientLight(0xffffff, 1.7));
|
|
21
|
+
const light = new DirectionalLight(0xffffff, 2);
|
|
22
|
+
light.position.set(-4, 10, 6);
|
|
23
|
+
scene.add(light);
|
|
24
|
+
const geometry = disposableGeometry = new BoxGeometry(1, 1, 1);
|
|
25
|
+
const material = disposableMaterial = new MeshLambertMaterial();
|
|
26
|
+
const mesh = disposableMesh = new InstancedMesh(geometry, material, worldLimits.instances);
|
|
27
|
+
mesh.count = 0;
|
|
28
|
+
mesh.frustumCulled = false;
|
|
29
|
+
scene.add(mesh);
|
|
30
|
+
return {
|
|
31
|
+
pattern(value) { if (closed)
|
|
32
|
+
throw Error('WorldDisposed'); const space = value === 'constellation', shade = space ? '#101b35' : '#b8d4de'; background.set(shade); fog = new Fog(shade, space ? 40 : 16, space ? 100 : 64); scene.fog = fog; },
|
|
33
|
+
upload(data, ranges = []) {
|
|
34
|
+
if (closed)
|
|
35
|
+
throw Error('WorldDisposed');
|
|
36
|
+
admitGeometry(data, ranges);
|
|
37
|
+
baseCount = data.length / 9;
|
|
38
|
+
mesh.count = baseCount;
|
|
39
|
+
for (let i = 0; i < baseCount; i++) {
|
|
40
|
+
const j = i * 9;
|
|
41
|
+
if (previous && data.subarray(j, j + 9).every((value, index) => value === previous[j + index]))
|
|
42
|
+
continue;
|
|
43
|
+
matrix.makeScale(data[j + 3], data[j + 4], data[j + 5]);
|
|
44
|
+
matrix.setPosition(data[j], data[j + 1], data[j + 2]);
|
|
45
|
+
mesh.setMatrixAt(i, matrix);
|
|
46
|
+
mesh.setColorAt(i, color.setRGB(data[j + 6], data[j + 7], data[j + 8]));
|
|
47
|
+
pendingStart = Math.min(pendingStart, i);
|
|
48
|
+
pendingEnd = Math.max(pendingEnd, i + 1);
|
|
49
|
+
}
|
|
50
|
+
previous = data;
|
|
51
|
+
actors = ranges;
|
|
52
|
+
},
|
|
53
|
+
motionStates(states) { if (closed)
|
|
54
|
+
throw Error('WorldDisposed'); if (states.size > worldLimits.objects)
|
|
55
|
+
throw Error('WorldGeometryLimitExceeded'); actorStates = new Map(states); },
|
|
56
|
+
draw(pose, aspect, zoom, size, milliseconds = 0, visibleActors = new Set()) {
|
|
57
|
+
if (closed)
|
|
58
|
+
return;
|
|
59
|
+
if (size.width !== bufferWidth || size.height !== bufferHeight) {
|
|
60
|
+
renderer.setSize(size.width, size.height, false);
|
|
61
|
+
bufferWidth = size.width;
|
|
62
|
+
bufferHeight = size.height;
|
|
63
|
+
}
|
|
64
|
+
const distance = pose.focus ? Math.hypot(pose.x - pose.focus.x, pose.y - pose.focus.y, pose.z - pose.focus.z) : 0;
|
|
65
|
+
fog.near = Math.max(16, distance * .75);
|
|
66
|
+
fog.far = Math.max(64, distance * 2 + 20);
|
|
67
|
+
mesh.count = baseCount + (pose.cameraMode === 'third-person' ? 1 : 0);
|
|
68
|
+
let matrixStart = pendingStart, matrixEnd = pendingEnd, colorStart = pendingStart, colorEnd = pendingEnd;
|
|
69
|
+
const coordinates = previous;
|
|
70
|
+
if (coordinates)
|
|
71
|
+
for (const actor of actors) {
|
|
72
|
+
if (!visibleActors.has(actor.id) && !lastVisible.has(actor.id))
|
|
73
|
+
continue;
|
|
74
|
+
const motion = visibleActors.has(actor.id) ? npcMotion(actor.id, milliseconds, actorStates.get(actor.id)) : { x: 0, y: 0, swing: 0 };
|
|
75
|
+
for (let i = actor.start; i < actor.end; i++) {
|
|
76
|
+
const j = i * 9, part = i - actor.start, limb = part === 1 || part === 4 ? 1 : part === 2 || part === 5 ? -1 : 0;
|
|
77
|
+
matrix.makeScale(coordinates[j + 3], coordinates[j + 4], coordinates[j + 5]);
|
|
78
|
+
matrix.setPosition(coordinates[j] + motion.x, coordinates[j + 1] + motion.y + limb * motion.swing, coordinates[j + 2]);
|
|
79
|
+
mesh.setMatrixAt(i, matrix);
|
|
80
|
+
}
|
|
81
|
+
matrixStart = Math.min(matrixStart, actor.start);
|
|
82
|
+
matrixEnd = Math.max(matrixEnd, actor.end);
|
|
83
|
+
}
|
|
84
|
+
lastVisible = new Set(visibleActors);
|
|
85
|
+
if (pose.cameraMode === 'third-person') {
|
|
86
|
+
matrix.makeScale(.48, .9, .42);
|
|
87
|
+
matrix.setPosition(pose.x, pose.y - 1.4, pose.z);
|
|
88
|
+
mesh.setMatrixAt(baseCount, matrix);
|
|
89
|
+
mesh.setColorAt(baseCount, color.setRGB(.96, .76, .28));
|
|
90
|
+
matrixStart = Math.min(matrixStart, baseCount);
|
|
91
|
+
matrixEnd = Math.max(matrixEnd, baseCount + 1);
|
|
92
|
+
colorStart = Math.min(colorStart, baseCount);
|
|
93
|
+
colorEnd = Math.max(colorEnd, baseCount + 1);
|
|
94
|
+
}
|
|
95
|
+
const updates = [[mesh.instanceMatrix, matrixStart, matrixEnd, 16], [mesh.instanceColor, colorStart, colorEnd, 3]];
|
|
96
|
+
for (const [attribute, start, end, size] of updates)
|
|
97
|
+
if (attribute) {
|
|
98
|
+
attribute.clearUpdateRanges();
|
|
99
|
+
if (start !== Infinity) {
|
|
100
|
+
attribute.addUpdateRange(start * size, (end - start) * size);
|
|
101
|
+
attribute.needsUpdate = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
viewCamera(pose, aspect, zoom, camera);
|
|
105
|
+
renderer.render(scene, camera);
|
|
106
|
+
pendingStart = Infinity;
|
|
107
|
+
pendingEnd = 0;
|
|
108
|
+
},
|
|
109
|
+
get count() { return mesh.count; }, get stats() { return { calls: renderer.info.render.calls, triangles: renderer.info.render.triangles, geometries: renderer.info.memory.geometries, textures: renderer.info.memory.textures }; }, close
|
|
110
|
+
};
|
|
26
111
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
matrixStart=Math.min(matrixStart,actor.start);matrixEnd=Math.max(matrixEnd,actor.end)
|
|
112
|
+
catch (error) {
|
|
113
|
+
try {
|
|
114
|
+
close();
|
|
115
|
+
}
|
|
116
|
+
catch (cleanup) {
|
|
117
|
+
throw new AggregateError([error, cleanup], 'Graphics initialization and cleanup failed');
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
36
120
|
}
|
|
37
|
-
lastVisible=new Set(visibleActors)
|
|
38
|
-
if(pose.cameraMode==='third-person'){
|
|
39
|
-
matrix.makeScale(.48,.9,.42);matrix.setPosition(pose.x,pose.y-1.4,pose.z);mesh.setMatrixAt(baseCount,matrix);mesh.setColorAt(baseCount,color.setRGB(.96,.76,.28))
|
|
40
|
-
matrixStart=Math.min(matrixStart,baseCount);matrixEnd=Math.max(matrixEnd,baseCount+1);colorStart=Math.min(colorStart,baseCount);colorEnd=Math.max(colorEnd,baseCount+1)
|
|
41
|
-
}
|
|
42
|
-
for(const [attribute,start,end,size]of [[mesh.instanceMatrix,matrixStart,matrixEnd,16],[mesh.instanceColor,colorStart,colorEnd,3]])if(attribute){attribute.clearUpdateRanges();if(start!==Infinity){attribute.addUpdateRange(start*size,(end-start)*size);attribute.needsUpdate=true}}
|
|
43
|
-
viewCamera(pose,aspect,zoom,camera);renderer.render(scene,camera);pendingStart=Infinity;pendingEnd=0},
|
|
44
|
-
get count(){return mesh.count},get stats(){return {calls:renderer.info.render.calls,triangles:renderer.info.render.triangles,geometries:renderer.info.memory.geometries,textures:renderer.info.memory.textures}},close
|
|
45
|
-
}
|
|
46
|
-
}catch(error){close();throw error}
|
|
47
121
|
}
|
|
48
|
-
const palette=[[.39
|
|
49
|
-
export function worldGeometry(model,actors=[]){
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
122
|
+
const palette = [[.39, .68, .64], [.62, .49, .79], [.89, .61, .38], [.37, .59, .79], [.76, .43, .53]];
|
|
123
|
+
export function worldGeometry(model, actors = []) {
|
|
124
|
+
let offsetY = 0;
|
|
125
|
+
const values = [], cube = (x, y, z, sx, sy, sz, color) => values.push(x, y + offsetY, z, sx, sy, sz, ...color);
|
|
126
|
+
// A bounded, decorative thinking garden. Terrain does not assert knowledge or relationships.
|
|
127
|
+
if (model.pattern === 'constellation') {
|
|
128
|
+
// Fixed procedural background, no texture loading or permanent animation loop.
|
|
129
|
+
for (let i = 0; i < 96; i++)
|
|
130
|
+
cube(Math.sin(i * 2.399) * 35, 2 + (i % 13) * 2.5, -15 - (i % 17) * 3, .07, .07, .07, [.64, .74, .92]);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
cube(0, -.65, -150, 60, 1.2, 370, [.55, .65, .55]);
|
|
134
|
+
cube(0, -.015, -150, 2.3, .06, 368, [.83, .82, .68]);
|
|
135
|
+
}
|
|
136
|
+
const byId = new Map(model.items.map(item => [item.id, item]));
|
|
137
|
+
for (const edge of model.relations) {
|
|
138
|
+
const from = byId.get(edge.from), to = byId.get(edge.to);
|
|
139
|
+
if (!from || !to)
|
|
140
|
+
throw Error('WorldIdentityInvalid');
|
|
141
|
+
const color = edge.active ? [.92, .61, .21] : [.24, .55, .62], fy = (from.y ?? 0) + .2, ty = (to.y ?? 0) + .2;
|
|
142
|
+
// Orthogonal connectors use the same instanced mesh; only declared edges.
|
|
143
|
+
cube((from.x + to.x) / 2, fy, from.z, Math.abs(to.x - from.x) + .06, .06, .06, color);
|
|
144
|
+
cube(to.x, (fy + ty) / 2, from.z, .06, Math.abs(ty - fy) + .06, .06, color);
|
|
145
|
+
cube(to.x, ty, (from.z + to.z) / 2, .06, .06, Math.abs(to.z - from.z) + .06, color);
|
|
146
|
+
}
|
|
147
|
+
for (const item of model.items) {
|
|
148
|
+
const start = values.length / 9;
|
|
149
|
+
const hash = [...item.id].reduce((v, c) => (Math.imul(v, 31) + c.charCodeAt(0)) | 0, 0);
|
|
150
|
+
const color = item.disabled ? [.52, .54, .56] : palette[(hash >>> 0) % palette.length];
|
|
151
|
+
const { x, z } = item;
|
|
152
|
+
offsetY = item.y ?? 0;
|
|
153
|
+
if (item.form !== 'book')
|
|
154
|
+
cube(x, .07, z, 2, .18, 1.8, item.selected ? [.98, .77, .34] : [.75, .80, .70]);
|
|
155
|
+
const tints = { accent: item.needsReview ? [.9, .61, .16] : color, dark: [.25, .31, .4], face: [.89, .77, .63], light: [.94, .96, .96], warning: [.9, .61, .16] };
|
|
156
|
+
for (const [dx, y, dz, sx, sy, sz, tint] of objectTemplates[item.form])
|
|
157
|
+
cube(x + dx, y, z + dz, sx, sy, sz, tints[tint]);
|
|
158
|
+
if (item.kind === 'character')
|
|
159
|
+
actors.push({ id: item.id, start, end: values.length / 9 });
|
|
160
|
+
}
|
|
161
|
+
if (values.length / 9 >= worldLimits.instances)
|
|
162
|
+
throw Error('WorldGeometryLimitExceeded');
|
|
163
|
+
return new Float32Array(values);
|
|
79
164
|
}
|
package/src/world-input.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
+
export declare const keyDirection: (code: string) => string | undefined;
|
|
1
2
|
/** @param {Document} document */
|
|
2
|
-
export function worldControlGuide(document: Document): HTMLElement;
|
|
3
|
-
export function keyDirection(code: any): any;
|
|
3
|
+
export declare function worldControlGuide(document: Document): HTMLElement;
|
package/src/world-input.mjs
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
// Presentation input only: shared movement bindings and on-demand control guide.
|
|
2
|
-
const movement=[
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
]
|
|
7
|
-
const directions=new Map(movement.flatMap(([action
|
|
8
|
-
export const keyDirection=code=>directions.get(code)
|
|
2
|
+
const movement = [
|
|
3
|
+
['forward', 'W / ↑', ['KeyW', 'ArrowUp']], ['back', 'S / ↓', ['KeyS', 'ArrowDown']],
|
|
4
|
+
['left', 'A / ←', ['KeyA', 'ArrowLeft']], ['right', 'D / →', ['KeyD', 'ArrowRight']],
|
|
5
|
+
['rise', 'R / Page Up', ['KeyR', 'PageUp']], ['descend', 'F / Page Down', ['KeyF', 'PageDown']]
|
|
6
|
+
];
|
|
7
|
+
const directions = new Map(movement.flatMap(([action, , codes]) => codes.map(code => [code, action])));
|
|
8
|
+
export const keyDirection = (code) => directions.get(code);
|
|
9
9
|
/** @param {Document} document */
|
|
10
|
-
export function worldControlGuide(document){
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
export function worldControlGuide(document) {
|
|
11
|
+
const guide = document.createElement('section');
|
|
12
|
+
guide.setAttribute('aria-label', 'World controls');
|
|
13
|
+
const section = (title, rows) => {
|
|
14
|
+
const heading = document.createElement('h3'), list = document.createElement('dl');
|
|
15
|
+
heading.textContent = title;
|
|
16
|
+
for (const [input, action] of rows) {
|
|
17
|
+
const term = document.createElement('dt'), description = document.createElement('dd');
|
|
18
|
+
term.textContent = input;
|
|
19
|
+
description.textContent = action;
|
|
20
|
+
list.append(term, description);
|
|
21
|
+
}
|
|
22
|
+
guide.append(heading, list);
|
|
23
|
+
};
|
|
24
|
+
section('Overview', [['Drag', 'Pan across the world'], ['Wheel / pinch', 'Zoom'], ['Click / tap an object', 'Select a target and open its context'], ['Map / M', 'Find a projected target by the same reference'], ['View all', 'Fit the whole space']]);
|
|
25
|
+
const note = document.createElement('p');
|
|
26
|
+
note.textContent = 'Choose Walk through world under In this space → Navigation, then close the panel and select the world to use movement keys.';
|
|
27
|
+
guide.append(note);
|
|
28
|
+
section('Walk through world', movement.map(([action, label]) => [label, action[0].toUpperCase() + action.slice(1)]).concat([['Drag', 'Look around'], ['Wheel', 'Move forward / back'], ['E', 'Inspect the aimed object'], ['M', 'Open or close Map'], ['Q', 'Mark the aimed or selected target'], ['Escape', 'Pause movement or close the open panel']]));
|
|
29
|
+
section('Keyboard navigation', [['Tab / Shift + Tab', 'Move between controls'], ['Enter / Space', 'Activate the focused button']]);
|
|
30
|
+
return guide;
|
|
21
31
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Camera } from '@zixcel/projection-client';
|
|
2
|
+
import type { WorldOptions, WorldSnapshot } from './world-api.mjs';
|
|
3
|
+
import type { WorldContext } from './world-context.mjs';
|
|
4
|
+
export declare function onKeyWorld(ctx: WorldContext, event: KeyboardEvent): void;
|
|
5
|
+
export declare function snapshotWorld(ctx: WorldContext): WorldSnapshot;
|
|
6
|
+
export declare function parkWorld(ctx: WorldContext): void;
|
|
7
|
+
export declare function mountWorld(ctx: WorldContext, nextHost: HTMLElement, options: WorldOptions): void;
|
|
8
|
+
export declare function closeWorld(ctx: WorldContext): void;
|
|
9
|
+
export declare function activityWorld(ctx: WorldContext, value: unknown): void;
|
|
10
|
+
export declare function applyWorld(ctx: WorldContext, value: Camera): void;
|
|
11
|
+
export declare function changeWorld(ctx: WorldContext, action: string): void;
|
|
12
|
+
export declare function pauseWorld(ctx: WorldContext, value: boolean): void;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { copyPose } from './world-api.mjs';
|
|
2
|
+
import { disposeWorldResources } from './world-resources.mjs';
|
|
3
|
+
import { initialPose, movePose, clamp } from './world-model.mjs';
|
|
4
|
+
import { spatialPattern } from './spatial-patterns.mjs';
|
|
5
|
+
import { keyDirection } from './world-input.mjs';
|
|
6
|
+
export function onKeyWorld(ctx, event) {
|
|
7
|
+
if (event.target !== ctx.canvas || event.ctrlKey || event.metaKey || event.altKey || event.isComposing)
|
|
8
|
+
return;
|
|
9
|
+
if (event.type === 'keydown' && event.code === 'KeyM') {
|
|
10
|
+
event.preventDefault();
|
|
11
|
+
ctx.map.open = !ctx.map.open;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (event.type === 'keydown' && event.code === 'KeyQ') {
|
|
15
|
+
event.preventDefault();
|
|
16
|
+
const target = ctx.hovered ?? ctx.selectedId;
|
|
17
|
+
if (target) {
|
|
18
|
+
ctx.selectedId = target;
|
|
19
|
+
ctx.mapButton.click();
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (ctx.navigation === 'overview') {
|
|
24
|
+
if (event.type === 'keydown' && event.code === 'KeyE')
|
|
25
|
+
ctx.activate(ctx.hovered);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const key = keyDirection(event.code);
|
|
29
|
+
if (key) {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
if (event.type === 'keydown' && !ctx.inputBlocked())
|
|
32
|
+
ctx.keys.add(key);
|
|
33
|
+
else
|
|
34
|
+
ctx.keys.delete(key);
|
|
35
|
+
ctx.invalidate();
|
|
36
|
+
}
|
|
37
|
+
else if (event.type === 'keydown' && !event.repeat) {
|
|
38
|
+
if (event.code === 'KeyE') {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
ctx.activate(ctx.hovered);
|
|
41
|
+
}
|
|
42
|
+
if (event.code === 'Escape') {
|
|
43
|
+
ctx.stopInput();
|
|
44
|
+
ctx.canvas.blur();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function snapshotWorld(ctx) { return { ...copyPose(ctx.pose), target: ctx.target, presentation: { ...ctx.view }, navigation: ctx.navigation, populated: ctx.fittedPopulation }; }
|
|
49
|
+
export function parkWorld(ctx) { ctx.attached = false; ctx.paused = true; ctx.stopInput(); ctx.stopFrame(); ctx.orbit.enabled = false; ctx.resize.disconnect(); ctx.wrap.remove(); }
|
|
50
|
+
export function mountWorld(ctx, nextHost, options) {
|
|
51
|
+
if (ctx.closed)
|
|
52
|
+
throw Error('WorldDisposed');
|
|
53
|
+
const nextTarget = options.target ?? null;
|
|
54
|
+
spatialPattern(options.objects ?? [], options.relations ?? [], ctx.target !== nextTarget ? (ctx.poses.get(nextTarget)?.presentation ?? { pattern: options.pattern ?? 'ground' }) : ctx.view);
|
|
55
|
+
const changedTarget = ctx.target !== (options.target ?? null);
|
|
56
|
+
const changedContext = ctx.context !== (options.context ?? null);
|
|
57
|
+
ctx.context = options.context ?? null;
|
|
58
|
+
if (changedTarget) {
|
|
59
|
+
ctx.poses.set(ctx.target, ctx.snapshot());
|
|
60
|
+
while (ctx.poses.size > 8)
|
|
61
|
+
ctx.poses.delete(ctx.poses.keys().next().value);
|
|
62
|
+
}
|
|
63
|
+
ctx.host = nextHost;
|
|
64
|
+
ctx.attached = true;
|
|
65
|
+
ctx.host.prepend(ctx.wrap);
|
|
66
|
+
ctx.resize.disconnect();
|
|
67
|
+
ctx.resize.observe(ctx.host);
|
|
68
|
+
ctx.onActivate = options.onActivate ?? (() => { });
|
|
69
|
+
ctx.conversation = options.conversation ?? null;
|
|
70
|
+
ctx.workActivity = options.activity ?? null;
|
|
71
|
+
ctx.target = options.target ?? null;
|
|
72
|
+
ctx.wrap.dataset.target = ctx.target ?? '';
|
|
73
|
+
ctx.paused = false;
|
|
74
|
+
ctx.engine.panel.hidden = options.pattern === 'garden';
|
|
75
|
+
if (ctx.conversationId)
|
|
76
|
+
ctx.closeConversation();
|
|
77
|
+
if (changedTarget) {
|
|
78
|
+
const saved = ctx.poses.get(ctx.target);
|
|
79
|
+
ctx.view = saved?.presentation ?? { pattern: options.pattern ?? 'ground', groupBy: 'declared', group: null, page: 0 };
|
|
80
|
+
ctx.navigation = saved?.navigation ?? 'overview';
|
|
81
|
+
ctx.pose = saved ? copyPose(saved) : initialPose();
|
|
82
|
+
ctx.fittedPopulation = Boolean(saved?.populated);
|
|
83
|
+
ctx.needsFit = !saved;
|
|
84
|
+
ctx.sourceObjects = [];
|
|
85
|
+
ctx.recent.clear();
|
|
86
|
+
ctx.query.value = '';
|
|
87
|
+
ctx.updates.open = false;
|
|
88
|
+
ctx.nearby.open = false;
|
|
89
|
+
ctx.map.open = false;
|
|
90
|
+
ctx.selectedId = null;
|
|
91
|
+
ctx.markedId = null;
|
|
92
|
+
ctx.wrap.dataset.selectedId = '';
|
|
93
|
+
ctx.wrap.dataset.markedId = '';
|
|
94
|
+
ctx.model = { items: [], relations: [] };
|
|
95
|
+
}
|
|
96
|
+
else if (changedContext) {
|
|
97
|
+
ctx.view = { ...ctx.view, group: null, page: 0 };
|
|
98
|
+
ctx.sourceObjects = [];
|
|
99
|
+
ctx.recent.clear();
|
|
100
|
+
ctx.query.value = '';
|
|
101
|
+
ctx.updates.open = false;
|
|
102
|
+
}
|
|
103
|
+
ctx.navigationSelect.value = ctx.navigation;
|
|
104
|
+
ctx.walk.hidden = ctx.navigation !== 'explore';
|
|
105
|
+
ctx.update(options.objects ?? [], options.relations);
|
|
106
|
+
ctx.syncOrbit();
|
|
107
|
+
ctx.invalidate();
|
|
108
|
+
}
|
|
109
|
+
export function closeWorld(ctx) {
|
|
110
|
+
if (ctx.closed)
|
|
111
|
+
return;
|
|
112
|
+
ctx.closed = true;
|
|
113
|
+
ctx.attached = false;
|
|
114
|
+
const previousGpu = ctx.gpu;
|
|
115
|
+
ctx.gpu = null;
|
|
116
|
+
ctx.geometry = null;
|
|
117
|
+
ctx.objects = [];
|
|
118
|
+
ctx.relations = [];
|
|
119
|
+
ctx.sourceObjects = [];
|
|
120
|
+
ctx.sourceRelations = [];
|
|
121
|
+
ctx.actorRanges = [];
|
|
122
|
+
ctx.projected = [];
|
|
123
|
+
ctx.projection = null;
|
|
124
|
+
ctx.model = { items: [], relations: [] };
|
|
125
|
+
ctx.restored = null;
|
|
126
|
+
try {
|
|
127
|
+
disposeWorldResources(ctx, previousGpu);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
ctx.workActivity = null;
|
|
131
|
+
ctx.conversation = null;
|
|
132
|
+
ctx.onActivate = () => { };
|
|
133
|
+
ctx.activityPresentation = {};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function activityWorld(ctx, value) { if (ctx.closed)
|
|
137
|
+
throw Error('WorldDisposed'); ctx.workActivity = value ?? null; ctx.update(ctx.sourceObjects, ctx.sourceRelations); }
|
|
138
|
+
export function applyWorld(ctx, value) { const next = clamp(value.z, .65, 1.5); if (next !== ctx.zoom) {
|
|
139
|
+
ctx.zoom = next;
|
|
140
|
+
ctx.syncOrbit();
|
|
141
|
+
ctx.invalidate();
|
|
142
|
+
} }
|
|
143
|
+
export function changeWorld(ctx, action) {
|
|
144
|
+
ctx.fitViewport();
|
|
145
|
+
if (action === 'reset') {
|
|
146
|
+
ctx.overview();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (ctx.navigation === 'overview') {
|
|
150
|
+
ctx.syncOrbit();
|
|
151
|
+
if (action === 'in')
|
|
152
|
+
ctx.orbit.dollyIn(1 / 1.2);
|
|
153
|
+
else if (action === 'out')
|
|
154
|
+
ctx.orbit.dollyOut(1 / 1.2);
|
|
155
|
+
else
|
|
156
|
+
ctx.orbit.pan(action === 'left' ? 40 : action === 'right' ? -40 : 0, action === 'up' ? 40 : action === 'down' ? -40 : 0);
|
|
157
|
+
ctx.orbit.update();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
else if (['in', 'out', 'up', 'down', 'left', 'right'].includes(action)) {
|
|
161
|
+
const forward = ['in', 'up'].includes(action) ? 1 : ['out', 'down'].includes(action) ? -1 : 0, side = action === 'left' ? -1 : action === 'right' ? 1 : 0;
|
|
162
|
+
for (let i = 0; i < 5; i++)
|
|
163
|
+
ctx.pose = movePose(ctx.pose, forward, side, .05);
|
|
164
|
+
}
|
|
165
|
+
ctx.invalidate();
|
|
166
|
+
}
|
|
167
|
+
export function pauseWorld(ctx, value) { ctx.paused = value; if (value) {
|
|
168
|
+
ctx.stopInput();
|
|
169
|
+
ctx.stopFrame();
|
|
170
|
+
}
|
|
171
|
+
else
|
|
172
|
+
ctx.invalidate(); }
|