@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
package/src/world-model.d.mts
CHANGED
|
@@ -1,49 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import type { Pose, WorldObject, WorldRelation, WorldModel } from './world-types.mjs';
|
|
2
|
+
import { PerspectiveCamera } from 'three';
|
|
3
|
+
export declare const worldLimits: Readonly<{
|
|
4
|
+
objects: 512;
|
|
5
|
+
relations: 512;
|
|
6
|
+
pixels: 1440000;
|
|
7
|
+
dpr: 1.5;
|
|
8
|
+
fps: 30;
|
|
9
|
+
instances: 8192;
|
|
10
|
+
radius: 28;
|
|
11
|
+
}>;
|
|
12
|
+
export declare const clamp: (value: number, min: number, max: number) => number;
|
|
13
|
+
export declare function layoutWorld<T extends WorldObject>(objects: readonly T[], relations?: readonly WorldRelation[], pattern?: string, spacing?: string): WorldModel<T>;
|
|
14
|
+
export declare function initialPose(): Pose;
|
|
15
|
+
export declare function fitPose(items: readonly {
|
|
7
16
|
x: number;
|
|
8
|
-
y
|
|
17
|
+
y?: number;
|
|
9
18
|
z: number;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
z: any;
|
|
17
|
-
yaw: number;
|
|
18
|
-
pitch: number;
|
|
19
|
-
focus: {
|
|
20
|
-
x: any;
|
|
21
|
-
y: any;
|
|
22
|
-
z: any;
|
|
23
|
-
};
|
|
24
|
-
};
|
|
25
|
-
export function movePose(pose: any, forward: any, side: any, seconds: any, vertical?: number): any;
|
|
26
|
-
export function lookPose(pose: any, dx: any, dy: any): any;
|
|
27
|
-
export function viewCamera(pose: any, aspect: any, zoom?: number, camera?: any): any;
|
|
28
|
-
export function cameraMatrix(pose: any, aspect: any, zoom?: number): any;
|
|
29
|
-
export function screenPoint(matrix: any, x: any, y: any, z: any, width: any, height: any): {
|
|
19
|
+
}[], aspect?: number): Pose;
|
|
20
|
+
export declare function movePose(pose: Pose, forward: number, side: number, seconds: number, vertical?: number): Pose;
|
|
21
|
+
export declare function lookPose(pose: Pose, dx: number, dy: number): Pose;
|
|
22
|
+
export declare function viewCamera(pose: Pose, aspect: number, zoom?: number, camera?: PerspectiveCamera): PerspectiveCamera;
|
|
23
|
+
export declare function cameraMatrix(pose: Pose, aspect: number, zoom?: number): import("three").Matrix4Tuple;
|
|
24
|
+
export declare function screenPoint(matrix: readonly number[], x: number, y: number, z: number, width: number, height: number): {
|
|
30
25
|
x: number;
|
|
31
26
|
y: number;
|
|
32
27
|
depth: number;
|
|
33
28
|
visible: boolean;
|
|
34
|
-
};
|
|
35
|
-
export function renderSize(width:
|
|
29
|
+
} | null;
|
|
30
|
+
export declare function renderSize(width: number, height: number, dpr: number): {
|
|
36
31
|
width: number;
|
|
37
32
|
height: number;
|
|
38
33
|
};
|
|
39
|
-
export function pickWorld(items:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
dpr: 1.5;
|
|
45
|
-
fps: 30;
|
|
46
|
-
instances: 8192;
|
|
47
|
-
radius: 28;
|
|
48
|
-
}>;
|
|
49
|
-
export function clamp(value: any, min: any, max: any): number;
|
|
34
|
+
export declare function pickWorld(items: readonly (WorldObject & {
|
|
35
|
+
x: number;
|
|
36
|
+
y?: number;
|
|
37
|
+
z: number;
|
|
38
|
+
})[], pose: Pose, x: number, y: number, width: number, height: number, zoom?: number): string | null;
|
package/src/world-model.mjs
CHANGED
|
@@ -1,83 +1,112 @@
|
|
|
1
|
-
import {PerspectiveCamera,Vector3,Raycaster,Vector2,Box3} from 'three'
|
|
2
|
-
import {objectPresentation,spacingPresets,libraryLayout,objectBounds} from './object-presentation.mjs'
|
|
1
|
+
import { PerspectiveCamera, Vector3, Raycaster, Vector2, Box3 } from 'three';
|
|
2
|
+
import { objectPresentation, spacingPresets, libraryLayout, objectBounds } from './object-presentation.mjs';
|
|
3
3
|
// Presentation geometry only. IDs and declared relations are never inferred from position.
|
|
4
|
-
export const worldLimits=Object.freeze({objects:512,relations:512,pixels:1440000,dpr:1.5,fps:30,instances:8192,radius:28})
|
|
5
|
-
export const clamp=(value,min,max)=>Math.max(min,Math.min(max,value))
|
|
6
|
-
export function layoutWorld(objects,relations=[],pattern='ground',spacing='balanced'){
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
4
|
+
export const worldLimits = Object.freeze({ objects: 512, relations: 512, pixels: 1440000, dpr: 1.5, fps: 30, instances: 8192, radius: 28 });
|
|
5
|
+
export const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
6
|
+
export function layoutWorld(objects, relations = [], pattern = 'ground', spacing = 'balanced') {
|
|
7
|
+
if (!Object.hasOwn(spacingPresets, spacing))
|
|
8
|
+
throw Error('SpatialSpacingInvalid');
|
|
9
|
+
const distances = spacingPresets[spacing];
|
|
10
|
+
if (!['ground', 'constellation', 'garden'].includes(pattern))
|
|
11
|
+
throw Error('SpatialPatternInvalid');
|
|
12
|
+
if (!Array.isArray(objects) || objects.length > worldLimits.objects || !Array.isArray(relations) || relations.length > worldLimits.relations)
|
|
13
|
+
throw Error('WorldCapacityExceeded');
|
|
14
|
+
const ids = new Set();
|
|
15
|
+
const items = objects.map((object, index) => {
|
|
16
|
+
const presentation = objectPresentation(object);
|
|
17
|
+
object = { ...object, ...presentation };
|
|
18
|
+
if (typeof object.id !== 'string' || !object.id || ids.has(object.id))
|
|
19
|
+
throw Error('WorldIdentityInvalid');
|
|
20
|
+
ids.add(object.id);
|
|
21
|
+
if (pattern === 'garden') {
|
|
22
|
+
const actors = objects.filter(o => o.kind === 'character'), actor = object.kind === 'character' ? actors.findIndex(o => o.id === object.id) : actors.findIndex(o => o.id === object.anchor);
|
|
23
|
+
const base = { x: actor < 0 ? 0 : (actor % 7 - 3) * distances.person, z: actor < 0 ? -8 : -Math.floor(actor / 7) * distances.person * 2 }, plot = object.plot ?? 0;
|
|
24
|
+
if (object.kind === 'character')
|
|
25
|
+
return { ...object, ...presentation, ...base, y: 0 };
|
|
26
|
+
const x = base.x + distances.group + (plot % 4) * distances.group, z = base.z - distances.group - Math.floor(plot / 4) * distances.group;
|
|
27
|
+
if (object.kind === 'group')
|
|
28
|
+
return { ...object, ...presentation, x, y: 0, z };
|
|
29
|
+
const slot = object.member ?? 0, l = libraryLayout;
|
|
30
|
+
return { ...object, ...presentation, x: x + (slot % l.columns - (l.columns - 1) / 2) * l.slotWidth, y: l.baseY + Math.floor(slot / l.columns) * l.rowHeight, z: z + l.frontZ };
|
|
31
|
+
}
|
|
32
|
+
if (pattern === 'constellation') {
|
|
33
|
+
const actors = objects.filter(i => i.kind === 'character'), isActor = object.kind === 'character', order = isActor ? actors.findIndex(o => o.id === object.id) : objects.slice(0, index).filter(i => i.kind !== 'character').length;
|
|
34
|
+
const count = objects.length - actors.length, ring = Math.floor(order / 8), angle = (order % 8) / Math.min(8, count - ring * 8) * Math.PI * 2;
|
|
35
|
+
return { ...object, ...presentation, x: isActor ? (order - (actors.length - 1) / 2) * 3.4 : Math.sin(angle) * (5 + ring * 4), y: isActor ? 0 : 2 + ring * 3 + (order % 3) * 1.2, z: isActor ? -1 : -8 + Math.cos(angle) * (3 + ring * 2) };
|
|
36
|
+
}
|
|
37
|
+
const row = Math.floor(index / 7), count = Math.min(7, objects.length - row * 7), column = index % 7;
|
|
38
|
+
return { ...object, ...presentation, x: (column - (count - 1) / 2) * 3.4, z: -row * 4.5 - 1 - Math.cos(column) * .4, y: 0 };
|
|
39
|
+
});
|
|
40
|
+
return { items, relations: relations.filter(edge => ids.has(edge.from) && ids.has(edge.to)), ...(pattern !== 'ground' ? { pattern } : {}) };
|
|
33
41
|
}
|
|
34
|
-
export function initialPose(){return {x:0,y:1.85,z:9,yaw:0,pitch
|
|
35
|
-
export function fitPose(items,aspect=1){
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
export function initialPose() { return { x: 0, y: 1.85, z: 9, yaw: 0, pitch: -.035 }; }
|
|
43
|
+
export function fitPose(items, aspect = 1) {
|
|
44
|
+
if (!items.length)
|
|
45
|
+
return { ...initialPose(), y: 8, z: 14, pitch: -.6, focus: { x: 0, y: 0, z: 0 } };
|
|
46
|
+
const box = new Box3();
|
|
47
|
+
for (const item of items) {
|
|
48
|
+
box.expandByPoint(new Vector3(item.x - 1, item.y ?? 0, item.z - 1));
|
|
49
|
+
box.expandByPoint(new Vector3(item.x + 1, (item.y ?? 0) + 3, item.z + 1));
|
|
50
|
+
}
|
|
51
|
+
const center = box.getCenter(new Vector3()), radius = box.getSize(new Vector3()).length() / 2;
|
|
52
|
+
const angle = Math.min(70 * Math.PI / 360, Math.atan(Math.tan(70 * Math.PI / 360) * Math.max(.1, aspect)));
|
|
53
|
+
const distance = Math.max(8, radius / Math.sin(angle) * 1.45);
|
|
54
|
+
return { x: center.x, y: center.y + distance * .6, z: center.z + distance * .8, yaw: 0, pitch: -Math.asin(.6), focus: { x: center.x, y: center.y, z: center.z } };
|
|
43
55
|
}
|
|
44
|
-
export function movePose(pose,forward,side,seconds,vertical=0){
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
export function movePose(pose, forward, side, seconds, vertical = 0) {
|
|
57
|
+
const length = Math.max(1, Math.hypot(forward, side, vertical)), speed = 4.5 * Math.min(.05, Math.max(0, seconds)) / length;
|
|
58
|
+
return { ...pose, x: clamp(pose.x + (Math.cos(pose.yaw) * side + Math.sin(pose.yaw) * forward) * speed, -worldLimits.radius, worldLimits.radius), y: clamp(pose.y + vertical * speed, .4, 18), z: clamp(pose.z + (Math.sin(pose.yaw) * side - Math.cos(pose.yaw) * forward) * speed, -350, worldLimits.radius) };
|
|
47
59
|
}
|
|
48
|
-
export function lookPose(pose,dx,dy){return {...pose,yaw:(pose.yaw+clamp(dx
|
|
49
|
-
export function viewCamera(pose,aspect,zoom=1,camera=new PerspectiveCamera(70,aspect
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
export function lookPose(pose, dx, dy) { return { ...pose, yaw: (pose.yaw + clamp(dx, -150, 150) * .004) % (Math.PI * 2), pitch: clamp(pose.pitch - clamp(dy, -150, 150) * .004, -.9, .9) }; }
|
|
61
|
+
export function viewCamera(pose, aspect, zoom = 1, camera = new PerspectiveCamera(70, aspect, .08, 120)) {
|
|
62
|
+
camera.aspect = aspect;
|
|
63
|
+
camera.far = 2000;
|
|
64
|
+
camera.zoom = clamp(zoom, .65, 1.5);
|
|
65
|
+
if (pose.cameraMode === 'third-person')
|
|
66
|
+
camera.position.set(pose.x - Math.sin(pose.yaw) * 3, pose.y + 1.2, pose.z + Math.cos(pose.yaw) * 3);
|
|
67
|
+
else
|
|
68
|
+
camera.position.set(pose.x, pose.y, pose.z);
|
|
69
|
+
camera.rotation.set(pose.pitch - (pose.cameraMode === 'third-person' ? .17 : 0), -pose.yaw, 0, 'YXZ');
|
|
70
|
+
camera.updateProjectionMatrix();
|
|
71
|
+
camera.updateMatrixWorld();
|
|
72
|
+
return camera;
|
|
55
73
|
}
|
|
56
|
-
export function cameraMatrix(pose,aspect,zoom=1){
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
export function cameraMatrix(pose, aspect, zoom = 1) {
|
|
75
|
+
const camera = viewCamera(pose, aspect, zoom);
|
|
76
|
+
return camera.projectionMatrix.clone().multiply(camera.matrixWorldInverse).elements;
|
|
59
77
|
}
|
|
60
|
-
export function screenPoint(matrix,x,y,z,width,height){
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
78
|
+
export function screenPoint(matrix, x, y, z, width, height) {
|
|
79
|
+
const v = [x, y, z, 1], q = [0, 0, 0, 0];
|
|
80
|
+
for (let r = 0; r < 4; r++)
|
|
81
|
+
for (let c = 0; c < 4; c++)
|
|
82
|
+
q[r] += matrix[c * 4 + r] * v[c];
|
|
83
|
+
if (q[3] <= .08)
|
|
84
|
+
return null;
|
|
85
|
+
const nx = q[0] / q[3], ny = q[1] / q[3];
|
|
86
|
+
return { x: (nx * .5 + .5) * width, y: (.5 - ny * .5) * height, depth: q[3], visible: Math.abs(nx) < .95 && Math.abs(ny) < .88 };
|
|
66
87
|
}
|
|
67
|
-
export function renderSize(width,height,dpr){
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
export function renderSize(width, height, dpr) {
|
|
89
|
+
const scale = Math.min(worldLimits.dpr, Math.max(1, dpr), Math.sqrt(worldLimits.pixels / Math.max(1, width * height)));
|
|
90
|
+
return { width: Math.max(1, Math.floor(width * scale)), height: Math.max(1, Math.floor(height * scale)) };
|
|
70
91
|
}
|
|
71
|
-
export function pickWorld(items,pose,x,y,width,height,zoom=1){
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
export function pickWorld(items, pose, x, y, width, height, zoom = 1) {
|
|
93
|
+
const ray = new Raycaster(), camera = viewCamera(pose, width / height, zoom), point = new Vector3(), box = new Box3();
|
|
94
|
+
ray.setFromCamera(new Vector2(2 * x / width - 1, 1 - 2 * y / height), camera);
|
|
95
|
+
let best = null, distance = 2000;
|
|
96
|
+
for (const item of items) {
|
|
97
|
+
if (item.disabled)
|
|
98
|
+
continue;
|
|
99
|
+
const y = item.y ?? 0;
|
|
100
|
+
const [width, height, depth] = objectBounds[item.form ?? objectPresentation(item).form];
|
|
101
|
+
box.min.set(item.x - width / 2, y, item.z - depth / 2);
|
|
102
|
+
box.max.set(item.x + width / 2, y + height, item.z + depth / 2);
|
|
103
|
+
if (ray.ray.intersectBox(box, point)) {
|
|
104
|
+
const near = point.distanceTo(camera.position);
|
|
105
|
+
if (near < distance) {
|
|
106
|
+
best = item.id;
|
|
107
|
+
distance = near;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return best;
|
|
83
112
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { WorldObject, WorldRelation } from './world-types.mjs';
|
|
2
|
+
import type { WorldContext } from './world-context.mjs';
|
|
3
|
+
export declare function filterDirectoryWorld(ctx: WorldContext): void;
|
|
4
|
+
export declare function renderUpdatesWorld(ctx: WorldContext): void;
|
|
5
|
+
export declare function updateWorld(ctx: WorldContext, objects: WorldObject[], relations?: WorldRelation[]): void;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { layoutWorld } from './world-model.mjs';
|
|
2
|
+
import { worldGeometry } from './world-gpu.mjs';
|
|
3
|
+
import { spatialPattern } from './spatial-patterns.mjs';
|
|
4
|
+
import { objectForms } from './object-presentation.mjs';
|
|
5
|
+
export function filterDirectoryWorld(ctx) { let count = 0; const term = ctx.query.value.trim().toLocaleLowerCase(); for (const row of ctx.directoryNodes.values()) {
|
|
6
|
+
row.hidden = !(row.textContent ?? '').toLocaleLowerCase().includes(term);
|
|
7
|
+
if (!row.hidden)
|
|
8
|
+
count++;
|
|
9
|
+
} ctx.noMatches.hidden = !term || count > 0; }
|
|
10
|
+
export function renderUpdatesWorld(ctx) {
|
|
11
|
+
const latest = [...ctx.recent.values()].at(-1);
|
|
12
|
+
ctx.updatesTitle.textContent = latest ? 'Observed changes · ' + ctx.recent.size + ' · ' + latest.title + ' · ' + latest.kind : 'No changes observed in this view';
|
|
13
|
+
ctx.updateList.replaceChildren();
|
|
14
|
+
for (const [id, change] of [...ctx.recent].reverse()) {
|
|
15
|
+
const button = ctx.make('button', '', change.title + ' · ' + change.kind);
|
|
16
|
+
button.type = 'button';
|
|
17
|
+
button.dataset.changeId = id;
|
|
18
|
+
button.disabled = !ctx.sourceObjects.some(item => item.id === id);
|
|
19
|
+
ctx.updateList.append(button);
|
|
20
|
+
}
|
|
21
|
+
for (const [id, node] of ctx.labelNodes)
|
|
22
|
+
node.dataset.updated = String(ctx.recent.has(id));
|
|
23
|
+
}
|
|
24
|
+
export function updateWorld(ctx, objects, relations = []) {
|
|
25
|
+
if (ctx.closed)
|
|
26
|
+
throw Error('WorldDisposed');
|
|
27
|
+
ctx.engine.update(ctx.workActivity);
|
|
28
|
+
const nextProjection = spatialPattern(objects, relations, ctx.view), next = layoutWorld(nextProjection.objects, nextProjection.relations, nextProjection.state.pattern, ctx.spacing);
|
|
29
|
+
// Keep remembered positions for unchanged membership, even if producer ordering changes.
|
|
30
|
+
if (ctx.placedSpacing === ctx.spacing && (ctx.model.pattern ?? 'ground') === (next.pattern ?? 'ground') && ctx.model.items.length === next.items.length && ctx.model.items.every(item => next.items.some(n => n.id === item.id && n.anchor === item.anchor && n.plot === item.plot && n.member === item.member))) {
|
|
31
|
+
const previous = new Map(ctx.model.items.map(item => [item.id, item]));
|
|
32
|
+
for (const item of next.items) {
|
|
33
|
+
const before = previous.get(item.id);
|
|
34
|
+
item.x = before.x;
|
|
35
|
+
item.y = before.y;
|
|
36
|
+
item.z = before.z;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
ctx.placedSpacing = ctx.spacing;
|
|
40
|
+
const key = JSON.stringify([next.pattern, next.items.map(i => [i.id, i.kind, i.form, i.needsReview, i.x, i.y, i.z, i.selected, i.disabled, i.memberIds?.length]), next.relations.map(r => [r.from, r.to, r.active])]);
|
|
41
|
+
const changedGeometry = key !== ctx.geometryKey, nextActors = changedGeometry ? [] : ctx.actorRanges, nextGeometry = changedGeometry ? worldGeometry(next, nextActors) : ctx.geometry;
|
|
42
|
+
const before = new Map(ctx.sourceObjects.map(item => [item.id, item]));
|
|
43
|
+
let changed = false;
|
|
44
|
+
if (before.size) {
|
|
45
|
+
for (const item of objects) {
|
|
46
|
+
const previous = before.get(item.id);
|
|
47
|
+
const kind = !previous ? 'Added to this view' : previous.disabled !== item.disabled ? 'Availability changed' : previous.title !== item.title || previous.summary !== item.summary || previous.changeKey !== item.changeKey ? 'Content changed' : null;
|
|
48
|
+
if (kind) {
|
|
49
|
+
ctx.recent.delete(item.id);
|
|
50
|
+
ctx.recent.set(item.id, { title: item.title ?? item.id, kind });
|
|
51
|
+
changed = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const ids = new Set(objects.map(item => item.id));
|
|
55
|
+
for (const item of before.values())
|
|
56
|
+
if (!ids.has(item.id)) {
|
|
57
|
+
ctx.recent.delete(item.id);
|
|
58
|
+
ctx.recent.set(item.id, { title: item.title ?? item.id, kind: 'No longer in this view' });
|
|
59
|
+
changed = true;
|
|
60
|
+
}
|
|
61
|
+
while (ctx.recent.size > 5)
|
|
62
|
+
ctx.recent.delete(ctx.recent.keys().next().value);
|
|
63
|
+
}
|
|
64
|
+
ctx.sourceObjects = objects.map(item => ({ ...item }));
|
|
65
|
+
ctx.sourceRelations = relations;
|
|
66
|
+
ctx.projection = nextProjection;
|
|
67
|
+
ctx.view = ctx.projection.state;
|
|
68
|
+
if (ctx.conversationId && (changed || !objects.some(i => i.id === ctx.conversationId)))
|
|
69
|
+
ctx.closeConversation();
|
|
70
|
+
ctx.model = next;
|
|
71
|
+
ctx.geometry = nextGeometry;
|
|
72
|
+
ctx.actorRanges = nextActors;
|
|
73
|
+
ctx.geometryKey = key;
|
|
74
|
+
if (!ctx.fittedPopulation && ctx.model.items.length) {
|
|
75
|
+
ctx.fittedPopulation = true;
|
|
76
|
+
if (ctx.navigation === 'overview')
|
|
77
|
+
ctx.needsFit = true;
|
|
78
|
+
}
|
|
79
|
+
const ids = new Set(ctx.model.items.map(item => item.id));
|
|
80
|
+
for (const [id, node] of ctx.labelNodes)
|
|
81
|
+
if (!ids.has(id)) {
|
|
82
|
+
node.remove();
|
|
83
|
+
ctx.labelNodes.delete(id);
|
|
84
|
+
ctx.directoryNodes.get(id)?.remove();
|
|
85
|
+
ctx.directoryNodes.delete(id);
|
|
86
|
+
}
|
|
87
|
+
ctx.patternSelect.value = ctx.view.pattern;
|
|
88
|
+
ctx.groupSelect.value = ctx.view.groupBy;
|
|
89
|
+
ctx.groupSelect.parentElement.hidden = ctx.view.pattern === 'ground' || ctx.view.pattern === 'garden';
|
|
90
|
+
ctx.patternSelect.parentElement.hidden = ctx.pattern === 'garden';
|
|
91
|
+
ctx.breadcrumb.hidden = ctx.view.pattern === 'ground' || ctx.view.pattern === 'garden';
|
|
92
|
+
ctx.back.hidden = !ctx.view.group;
|
|
93
|
+
ctx.location.textContent = ctx.projection.label + ' · ' + ctx.projection.total + ' objects';
|
|
94
|
+
ctx.pagination.hidden = ctx.projection.pages === 1;
|
|
95
|
+
ctx.previous.disabled = ctx.view.page === 0;
|
|
96
|
+
ctx.nextPage.disabled = ctx.view.page === ctx.projection.pages - 1;
|
|
97
|
+
ctx.pageCount.textContent = (ctx.view.page + 1) + ' / ' + ctx.projection.pages;
|
|
98
|
+
ctx.wrap.dataset.pattern = ctx.view.pattern;
|
|
99
|
+
ctx.wrap.dataset.groupBy = ctx.view.groupBy;
|
|
100
|
+
ctx.wrap.dataset.group = ctx.view.group ?? '';
|
|
101
|
+
ctx.wrap.dataset.sourceObjects = String(objects.length);
|
|
102
|
+
for (const [index, item] of ctx.model.items.entries()) {
|
|
103
|
+
let b = ctx.labelNodes.get(item.id), row = ctx.directoryNodes.get(item.id);
|
|
104
|
+
if (!b) {
|
|
105
|
+
b = ctx.make('button', 'world-name');
|
|
106
|
+
b.type = 'button';
|
|
107
|
+
b.dataset.worldId = item.id;
|
|
108
|
+
b.append(ctx.make('strong', ''), ctx.make('span', ''));
|
|
109
|
+
ctx.labelNodes.set(item.id, b);
|
|
110
|
+
row = ctx.make('div', '');
|
|
111
|
+
row.dataset.directoryId = item.id;
|
|
112
|
+
const focus = ctx.make('button', ''), open = ctx.make('button', '');
|
|
113
|
+
focus.type = open.type = 'button';
|
|
114
|
+
focus.dataset.focus = 'true';
|
|
115
|
+
row.append(focus, open);
|
|
116
|
+
ctx.directoryNodes.set(item.id, row);
|
|
117
|
+
}
|
|
118
|
+
if (!row)
|
|
119
|
+
throw Error('WorldDirectoryUnavailable');
|
|
120
|
+
const review = item.kind === 'character' ? ctx.roleStatus(ctx.workActivity, item.id) : null, state = review?.state ?? null;
|
|
121
|
+
const detail = item.disabled ? 'Unavailable' : review ? review.label : objectForms[item.form] + ' · ' + (item.summary || 'Inspect information');
|
|
122
|
+
ctx.setText(b.children[0], item.title);
|
|
123
|
+
ctx.setText(b.children[1], detail);
|
|
124
|
+
b.dataset.worldKind = item.kind ?? 'record';
|
|
125
|
+
b.disabled = Boolean(item.disabled);
|
|
126
|
+
b.setAttribute('aria-label', item.title + '. ' + detail);
|
|
127
|
+
b.dataset.position = JSON.stringify([item.x, item.y, item.z]);
|
|
128
|
+
b.dataset.objectForm = item.form;
|
|
129
|
+
b.dataset.activity = state ?? '';
|
|
130
|
+
b.dataset.supportRef = item.supportRef ?? '';
|
|
131
|
+
b.dataset.selected = String(item.id === ctx.selectedId);
|
|
132
|
+
b.dataset.marked = String(item.id === ctx.markedId);
|
|
133
|
+
if (ctx.labels.children[index] !== b)
|
|
134
|
+
ctx.labels.insertBefore(b, ctx.labels.children[index] ?? null);
|
|
135
|
+
const [focus, open] = row.querySelectorAll('button');
|
|
136
|
+
ctx.setText(focus, item.title);
|
|
137
|
+
focus.setAttribute('aria-label', 'Find ' + item.title);
|
|
138
|
+
open.disabled = Boolean(item.disabled);
|
|
139
|
+
const action = item.kind === 'group' ? 'Browse shelf' : ctx.view.pattern === 'garden' && item.kind === 'character' ? 'Talk to' : 'Inspect';
|
|
140
|
+
ctx.setText(open, action);
|
|
141
|
+
open.setAttribute('aria-label', action + ' ' + item.title);
|
|
142
|
+
if (item.kind === 'group')
|
|
143
|
+
row.dataset.members = JSON.stringify(item.memberIds);
|
|
144
|
+
else
|
|
145
|
+
delete row.dataset.members;
|
|
146
|
+
if (ctx.list.children[index] !== row)
|
|
147
|
+
ctx.list.insertBefore(row, ctx.list.children[index] ?? null);
|
|
148
|
+
}
|
|
149
|
+
ctx.summary.textContent = (ctx.view.pattern === 'garden' ? 'People' : 'In this space') + ' · ' + ctx.model.items.length;
|
|
150
|
+
ctx.renderMap();
|
|
151
|
+
ctx.filterDirectory();
|
|
152
|
+
ctx.renderUpdates();
|
|
153
|
+
if (ctx.needsFit) {
|
|
154
|
+
ctx.overview();
|
|
155
|
+
ctx.needsFit = false;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
ctx.gpu?.pattern(ctx.view.pattern);
|
|
159
|
+
if (changedGeometry && ctx.gpu && ctx.geometry) {
|
|
160
|
+
ctx.gpu.upload(ctx.geometry, ctx.actorRanges);
|
|
161
|
+
ctx.uploads++;
|
|
162
|
+
}
|
|
163
|
+
ctx.gpu?.motionStates(new Map(ctx.model.items.filter(i => i.kind === 'character').map(i => [i.id, ctx.roleStatus(ctx.workActivity, i.id)?.state ?? 'Idle'])));
|
|
164
|
+
ctx.invalidate();
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
ctx.fail(error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { tickFrame } from './world-clock.mjs';
|
|
2
|
+
import type { WorldContext } from './world-context.mjs';
|
|
3
|
+
export declare function stopInputWorld(ctx: WorldContext): void;
|
|
4
|
+
export declare function stopFrameWorld(ctx: WorldContext): void;
|
|
5
|
+
export declare function invalidateWorld(ctx: WorldContext): void;
|
|
6
|
+
export declare const tickWorld: typeof tickFrame;
|
|
7
|
+
export declare function drawWorld(ctx: WorldContext): void;
|
|
8
|
+
export declare function failWorld(ctx: WorldContext, error: unknown): void;
|
|
9
|
+
export declare function initializeWorld(ctx: WorldContext): void;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { tickFrame } from './world-clock.mjs';
|
|
2
|
+
import { record } from './world-api.mjs';
|
|
3
|
+
import { cameraMatrix, screenPoint, renderSize, clamp, pickWorld } from './world-model.mjs';
|
|
4
|
+
import { createGpu } from './world-gpu.mjs';
|
|
5
|
+
import { objectBounds } from './object-presentation.mjs';
|
|
6
|
+
export function stopInputWorld(ctx) { ctx.keys?.clear(); ctx.drag = null; if (ctx.canvas && ctx.document?.pointerLockElement === ctx.canvas)
|
|
7
|
+
ctx.document.exitPointerLock(); }
|
|
8
|
+
export function stopFrameWorld(ctx) { if (typeof ctx.frame === 'number') {
|
|
9
|
+
ctx.window.cancelAnimationFrame(ctx.frame);
|
|
10
|
+
ctx.frame = null;
|
|
11
|
+
} if (typeof ctx.motionTimer === 'number') {
|
|
12
|
+
ctx.window.clearTimeout(ctx.motionTimer);
|
|
13
|
+
ctx.motionTimer = null;
|
|
14
|
+
} ctx.last = 0; }
|
|
15
|
+
export function invalidateWorld(ctx) { ctx.dirty = true; if (!ctx.closed && ctx.attached && !ctx.paused && !ctx.lost && !ctx.document.hidden && ctx.frame === null)
|
|
16
|
+
ctx.frame = ctx.window.requestAnimationFrame(ctx.tick); }
|
|
17
|
+
export const tickWorld = tickFrame;
|
|
18
|
+
export function drawWorld(ctx) {
|
|
19
|
+
if (!ctx.gpu)
|
|
20
|
+
return;
|
|
21
|
+
const width = ctx.host.clientWidth, height = ctx.host.clientHeight;
|
|
22
|
+
if (!width || !height)
|
|
23
|
+
return;
|
|
24
|
+
ctx.fitViewport();
|
|
25
|
+
const size = renderSize(width, height, ctx.window.devicePixelRatio || 1);
|
|
26
|
+
const matrix = cameraMatrix(ctx.pose, width / height, ctx.zoom);
|
|
27
|
+
const visibleActors = new Set(ctx.model.items.filter(item => item.kind === 'character').filter(item => { const point = screenPoint(matrix, item.x, item.y + 1.5, item.z, width, height); return point?.visible && point.depth < 150; }).map(item => item.id));
|
|
28
|
+
if (ctx.window.matchMedia('(prefers-reduced-motion: reduce)').matches)
|
|
29
|
+
visibleActors.clear();
|
|
30
|
+
ctx.gpu.draw(ctx.pose, width / height, ctx.zoom, size, ctx.window.performance.now(), visibleActors);
|
|
31
|
+
ctx.frames++;
|
|
32
|
+
ctx.positionBubble();
|
|
33
|
+
ctx.wrap.dataset.animatedNpcs = String(visibleActors.size);
|
|
34
|
+
if (visibleActors.size && ctx.motionTimer === null && !ctx.paused && !ctx.document.hidden)
|
|
35
|
+
ctx.motionTimer = ctx.window.setTimeout(() => { ctx.motionTimer = null; ctx.invalidate(); }, 200);
|
|
36
|
+
ctx.projected = [];
|
|
37
|
+
for (const item of ctx.model.items) {
|
|
38
|
+
const point = screenPoint(matrix, item.x, item.y + objectBounds[item.form][1] + .45, item.z, width, height);
|
|
39
|
+
if (point?.visible && point.depth < 1800)
|
|
40
|
+
ctx.projected.push({ item, ...point });
|
|
41
|
+
}
|
|
42
|
+
ctx.projected.sort((a, b) => a.depth - b.depth);
|
|
43
|
+
const displayed = [], labelWidth = width < 600 ? 138 : 182;
|
|
44
|
+
for (const point of ctx.projected) {
|
|
45
|
+
const x = clamp(point.x, 90, width - 90), y = clamp(point.y, 96, Math.max(96, height - 80));
|
|
46
|
+
if (displayed.some(p => Math.abs(p.x - x) < labelWidth && Math.abs(p.y - y) < 85))
|
|
47
|
+
continue;
|
|
48
|
+
displayed.push({ ...point, x, y });
|
|
49
|
+
if (displayed.length === 8)
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
const visible = new Set(displayed.map(p => p.item.id));
|
|
53
|
+
for (const [id, node] of ctx.labelNodes)
|
|
54
|
+
node.hidden = !visible.has(id);
|
|
55
|
+
for (const point of displayed) {
|
|
56
|
+
const node = ctx.labelNodes.get(point.item.id);
|
|
57
|
+
node.style.left = point.x + 'px';
|
|
58
|
+
node.style.top = point.y + 'px';
|
|
59
|
+
node.dataset.distance = point.depth.toFixed(1);
|
|
60
|
+
}
|
|
61
|
+
ctx.publishAnchor();
|
|
62
|
+
ctx.hovered = pickWorld(ctx.model.items, ctx.pose, width / 2, height / 2, width, height, ctx.zoom);
|
|
63
|
+
const target = ctx.model.items.find(item => item.id === ctx.hovered);
|
|
64
|
+
ctx.inspect.disabled = !ctx.hovered;
|
|
65
|
+
ctx.inspect.hidden = ctx.navigation === 'overview';
|
|
66
|
+
ctx.aim.hidden = ctx.navigation === 'overview';
|
|
67
|
+
ctx.hint.textContent = ctx.navigation === 'overview' ? (ctx.view.pattern === 'garden' ? `Open people · ${ctx.model.items.filter(item => item.kind === 'character').length}` : `Browse this space · ${ctx.model.items.length}`) : target ? target.title + ' · Inspect' : ctx.model.items.length ? 'Select a person or a group' : 'No information is visible in this view';
|
|
68
|
+
ctx.hint.hidden = ctx.navigation !== 'overview' || !ctx.model.items.length;
|
|
69
|
+
ctx.canvas.setAttribute('aria-label', ctx.navigation === 'overview' ? 'Explore your world. Drag to pan. Wheel or pinch to zoom. Select an object to focus. M for Map.' : 'Explore your world. W A S D or arrows to walk. Drag to look. E to interact. M for Map. Q to mark.');
|
|
70
|
+
for (const [id, node] of ctx.labelNodes)
|
|
71
|
+
node.dataset.aimed = String(id === ctx.hovered);
|
|
72
|
+
ctx.wrap.dataset.pose = JSON.stringify(ctx.pose);
|
|
73
|
+
ctx.wrap.dataset.frames = String(ctx.frames);
|
|
74
|
+
ctx.wrap.dataset.instances = String(ctx.gpu.count);
|
|
75
|
+
ctx.wrap.dataset.objects = String(ctx.model.items.length);
|
|
76
|
+
ctx.wrap.dataset.bufferPixels = String(ctx.canvas.width * ctx.canvas.height);
|
|
77
|
+
ctx.wrap.dataset.renderer = 'three';
|
|
78
|
+
ctx.wrap.dataset.renderStats = JSON.stringify(ctx.gpu.stats);
|
|
79
|
+
ctx.wrap.dataset.cameraMode = ctx.pose.cameraMode ?? 'overview';
|
|
80
|
+
ctx.wrap.dataset.avatar = ctx.pose.cameraMode === 'third-person' ? 'present' : 'hidden';
|
|
81
|
+
ctx.wrap.dataset.uploads = String(ctx.uploads);
|
|
82
|
+
ctx.wrap.dataset.initializations = String(ctx.initializations);
|
|
83
|
+
ctx.wrap.dataset.navigation = ctx.navigation;
|
|
84
|
+
}
|
|
85
|
+
export function failWorld(ctx, error) { const e = record(error); ctx.stopInput(); ctx.stopFrame(); ctx.status.hidden = false; ctx.status.textContent = (e.code ?? e.message ?? 'WorldRenderingUnavailable') + ' — The 3D view is unavailable. Use “In this space” to inspect the available information.'; ctx.wrap.dataset.gpu = 'unavailable'; }
|
|
86
|
+
export function initializeWorld(ctx) { try {
|
|
87
|
+
ctx.gpu = createGpu(ctx.canvas);
|
|
88
|
+
ctx.initializations++;
|
|
89
|
+
ctx.gpu.pattern(ctx.view.pattern);
|
|
90
|
+
if (ctx.geometry) {
|
|
91
|
+
ctx.gpu.upload(ctx.geometry, ctx.actorRanges);
|
|
92
|
+
ctx.gpu.motionStates(new Map(ctx.model.items.filter(i => i.kind === 'character').map(i => [i.id, ctx.roleStatus(ctx.workActivity, i.id)?.state ?? 'Idle'])));
|
|
93
|
+
ctx.uploads++;
|
|
94
|
+
}
|
|
95
|
+
ctx.lost = false;
|
|
96
|
+
ctx.status.hidden = true;
|
|
97
|
+
ctx.wrap.dataset.gpu = 'ready';
|
|
98
|
+
ctx.invalidate();
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
const failed = ctx.gpu;
|
|
102
|
+
ctx.gpu = null;
|
|
103
|
+
try {
|
|
104
|
+
failed?.close();
|
|
105
|
+
}
|
|
106
|
+
catch (cleanup) {
|
|
107
|
+
error = new AggregateError([error, cleanup], 'Graphics initialization and cleanup failed');
|
|
108
|
+
}
|
|
109
|
+
ctx.fail(error);
|
|
110
|
+
} }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
interface Clearable {
|
|
2
|
+
clear(): void;
|
|
3
|
+
}
|
|
4
|
+
interface Closable {
|
|
5
|
+
close(): void;
|
|
6
|
+
}
|
|
7
|
+
export interface WorldResources {
|
|
8
|
+
engine?: {
|
|
9
|
+
close?(): void;
|
|
10
|
+
};
|
|
11
|
+
stopInput?: () => void;
|
|
12
|
+
stopFrame?: () => void;
|
|
13
|
+
resize?: {
|
|
14
|
+
disconnect(): void;
|
|
15
|
+
};
|
|
16
|
+
controller?: {
|
|
17
|
+
abort(): void;
|
|
18
|
+
};
|
|
19
|
+
orbit?: {
|
|
20
|
+
dispose(): void;
|
|
21
|
+
};
|
|
22
|
+
gpu?: Closable | null;
|
|
23
|
+
canvas?: Pick<HTMLCanvasElement, 'getContext'>;
|
|
24
|
+
initializations?: number;
|
|
25
|
+
labelNodes?: Clearable;
|
|
26
|
+
directoryNodes?: Clearable;
|
|
27
|
+
recent?: Clearable;
|
|
28
|
+
poses?: Clearable;
|
|
29
|
+
wrap?: {
|
|
30
|
+
remove(): void;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export declare function disposeWorldResources(ctx: WorldResources, gpu?: Closable | null | undefined): void;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { disposeGraphics } from './graphics-disposal.mjs';
|
|
2
|
+
// May also run after partial setup. Every acquired resource gets its own attempt.
|
|
3
|
+
export function disposeWorldResources(ctx, gpu = ctx.gpu) {
|
|
4
|
+
disposeGraphics([() => ctx.stopInput?.(), () => ctx.stopFrame?.(), () => ctx.resize?.disconnect(), () => ctx.controller?.abort(), () => ctx.engine?.close?.(), () => ctx.orbit?.dispose(), () => gpu?.close(),
|
|
5
|
+
() => { if ((ctx.initializations ?? 0) > 0)
|
|
6
|
+
ctx.canvas?.getContext('webgl2')?.getExtension('WEBGL_lose_context')?.loseContext(); },
|
|
7
|
+
() => ctx.labelNodes?.clear(), () => ctx.directoryNodes?.clear(), () => ctx.recent?.clear(), () => ctx.poses?.clear(), () => ctx.wrap?.remove()]);
|
|
8
|
+
}
|