@zixcel/dom-renderer 0.10.3 → 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.
Files changed (80) hide show
  1. package/README.md +6 -0
  2. package/package.json +19 -11
  3. package/src/book-reader.d.mts +9 -1
  4. package/src/book-reader.mjs +54 -33
  5. package/src/dom-elements.d.mts +11 -0
  6. package/src/dom-elements.mjs +48 -0
  7. package/src/frame-layout.d.mts +9 -3
  8. package/src/frame-layout.mjs +22 -22
  9. package/src/frame.d.mts +22 -10
  10. package/src/frame.mjs +93 -45
  11. package/src/geometry-policy.d.mts +3 -0
  12. package/src/geometry-policy.mjs +17 -0
  13. package/src/graph-documents.d.mts +48 -0
  14. package/src/graph-documents.mjs +40 -0
  15. package/src/graphics-disposal.d.mts +2 -0
  16. package/src/graphics-disposal.mjs +14 -0
  17. package/src/index.d.mts +11 -57
  18. package/src/index.mjs +9 -205
  19. package/src/input-form.d.mts +5 -0
  20. package/src/input-form.mjs +103 -0
  21. package/src/inspection-overview.d.mts +27 -1
  22. package/src/inspection-overview.mjs +50 -33
  23. package/src/npc-motion.d.mts +1 -1
  24. package/src/npc-motion.mjs +5 -5
  25. package/src/object-presentation.d.mts +22 -26
  26. package/src/object-presentation.mjs +20 -17
  27. package/src/open-world-projection.d.mts +19 -10
  28. package/src/open-world-projection.mjs +31 -22
  29. package/src/reconcile.d.mts +1 -1
  30. package/src/reconcile.mjs +31 -19
  31. package/src/scene-items.d.mts +9 -0
  32. package/src/scene-items.mjs +29 -0
  33. package/src/scene-renderer.d.mts +37 -0
  34. package/src/scene-renderer.mjs +170 -0
  35. package/src/spatial-patterns.d.mts +22 -27
  36. package/src/spatial-patterns.mjs +63 -41
  37. package/src/spatial.d.mts +17 -6
  38. package/src/spatial.mjs +39 -25
  39. package/src/structure-graph.d.mts +5 -13
  40. package/src/structure-graph.mjs +167 -96
  41. package/src/symbol.d.mts +1 -1
  42. package/src/symbol.mjs +21 -16
  43. package/src/world-api.d.mts +56 -0
  44. package/src/world-api.mjs +8 -0
  45. package/src/world-camera.d.mts +14 -0
  46. package/src/world-camera.mjs +74 -0
  47. package/src/world-clock.d.mts +19 -0
  48. package/src/world-clock.mjs +30 -0
  49. package/src/world-commands.d.mts +39 -0
  50. package/src/world-commands.mjs +1 -0
  51. package/src/world-context.d.mts +132 -0
  52. package/src/world-context.mjs +1 -0
  53. package/src/world-controller.d.mts +43 -0
  54. package/src/world-controller.mjs +62 -0
  55. package/src/world-conversation.d.mts +3 -0
  56. package/src/world-conversation.mjs +46 -0
  57. package/src/world-events.d.mts +2 -0
  58. package/src/world-events.mjs +120 -0
  59. package/src/world-gpu.d.mts +3 -15
  60. package/src/world-gpu.mjs +159 -74
  61. package/src/world-input.d.mts +2 -2
  62. package/src/world-input.mjs +28 -18
  63. package/src/world-lifecycle.d.mts +12 -0
  64. package/src/world-lifecycle.mjs +172 -0
  65. package/src/world-model.d.mts +29 -40
  66. package/src/world-model.mjs +103 -74
  67. package/src/world-projection.d.mts +5 -0
  68. package/src/world-projection.mjs +169 -0
  69. package/src/world-render-loop.d.mts +9 -0
  70. package/src/world-render-loop.mjs +110 -0
  71. package/src/world-resources.d.mts +34 -0
  72. package/src/world-resources.mjs +8 -0
  73. package/src/world-runtime.d.mts +2 -36
  74. package/src/world-runtime.mjs +6 -311
  75. package/src/world-setup.d.mts +2 -0
  76. package/src/world-setup.mjs +179 -0
  77. package/src/world-surface.d.mts +2 -7
  78. package/src/world-surface.mjs +20 -12
  79. package/src/world-types.d.mts +74 -0
  80. 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
+ }
@@ -1,15 +1,3 @@
1
- export function createGpu(canvas: any): {
2
- pattern(value: any): void;
3
- upload(data: any, ranges?: any[]): void;
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
- let renderer=null,geometry=null,material=null,mesh=null,closed=false,bufferWidth=0,bufferHeight=0,previous=null,pendingStart=Infinity,pendingEnd=0,baseCount=0,actors=[],actorStates=new Map(),lastVisible=new Set()
8
- const scene=new Scene(),camera=new PerspectiveCamera(70,1,.08,120),matrix=new Matrix4(),color=new Color()
9
- function close(){if(closed)return;closed=true;previous=null;actors=[];actorStates.clear();lastVisible.clear();mesh?.dispose();geometry?.dispose();material?.dispose();scene.clear();renderer?.dispose()}
10
- try{
11
- renderer=new WebGLRenderer({canvas,alpha:false,antialias:false,stencil:false,preserveDrawingBuffer:false,powerPreference:'low-power'})
12
- scene.background=new Color('#b8d4de');scene.fog=new Fog('#b8d4de',16,64)
13
- scene.add(new AmbientLight(0xffffff,1.7));const light=new DirectionalLight(0xffffff,2);light.position.set(-4,10,6);scene.add(light)
14
- geometry=new BoxGeometry(1,1,1);material=new MeshLambertMaterial()
15
- mesh=new InstancedMesh(geometry,material,worldLimits.instances);mesh.count=0;mesh.frustumCulled=false;scene.add(mesh)
16
- return {
17
- pattern(value){const space=value==='constellation',background=space?'#101b35':'#b8d4de';scene.background.set(background);scene.fog=new Fog(background,space?40:16,space?100:64)},
18
- upload(data,ranges=[]){
19
- if(closed)throw Error('WorldDisposed')
20
- if(data.length%9||data.length/9>=worldLimits.instances)throw Error('WorldGeometryLimitExceeded')
21
- baseCount=data.length/9;mesh.count=baseCount
22
- for(let i=0;i<baseCount;i++){
23
- const j=i*9;if(previous&&data.subarray(j,j+9).every((value,index)=>value===previous[j+index]))continue
24
- matrix.makeScale(data[j+3],data[j+4],data[j+5]);matrix.setPosition(data[j],data[j+1],data[j+2]);mesh.setMatrixAt(i,matrix);mesh.setColorAt(i,color.setRGB(data[j+6],data[j+7],data[j+8]))
25
- pendingStart=Math.min(pendingStart,i);pendingEnd=Math.max(pendingEnd,i+1)
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
- previous=data;actors=ranges
28
- },
29
- motionStates(states){actorStates=states},
30
- draw(pose,aspect,zoom,size,milliseconds=0,visibleActors=new Set()){if(closed)return;if(size.width!==bufferWidth||size.height!==bufferHeight){renderer.setSize(size.width,size.height,false);bufferWidth=size.width;bufferHeight=size.height}const distance=pose.focus?Math.hypot(pose.x-pose.focus.x,pose.y-pose.focus.y,pose.z-pose.focus.z):0;scene.fog.near=Math.max(16,distance*.75);scene.fog.far=Math.max(64,distance*2+20)
31
- mesh.count=baseCount+(pose.cameraMode==='third-person'?1:0)
32
- let matrixStart=pendingStart,matrixEnd=pendingEnd,colorStart=pendingStart,colorEnd=pendingEnd
33
- for(const actor of actors){if(!visibleActors.has(actor.id)&&!lastVisible.has(actor.id))continue;const motion=visibleActors.has(actor.id)?npcMotion(actor.id,milliseconds,actorStates.get(actor.id)):{x:0,y:0,swing:0}
34
- for(let i=actor.start;i<actor.end;i++){const j=i*9,part=i-actor.start,limb=part===1||part===4?1:part===2||part===5?-1:0;matrix.makeScale(previous[j+3],previous[j+4],previous[j+5]);matrix.setPosition(previous[j]+motion.x,previous[j+1]+motion.y+limb*motion.swing,previous[j+2]);mesh.setMatrixAt(i,matrix)}
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,.68,.64],[.62,.49,.79],[.89,.61,.38],[.37,.59,.79],[.76,.43,.53]]
49
- export function worldGeometry(model,actors=[]){
50
- let offsetY=0
51
- const values=[],cube=(x,y,z,sx,sy,sz,color)=>values.push(x,y+offsetY,z,sx,sy,sz,...color)
52
- // A bounded, decorative thinking garden. Terrain does not assert knowledge or relationships.
53
- if(model.pattern==='constellation'){
54
- // Fixed procedural background, no texture loading or permanent animation loop.
55
- for(let i=0;i<96;i++)cube(Math.sin(i*2.399)*35,2+(i%13)*2.5,-15-(i%17)*3,.07,.07,.07,[.64,.74,.92])
56
- }else{
57
- cube(0,-.65,-150,60,1.2,370,[.55,.65,.55])
58
- cube(0,-.015,-150,2.3,.06,368,[.83,.82,.68])
59
- }
60
- const byId=new Map(model.items.map(item=>[item.id,item]))
61
- for(const edge of model.relations){
62
- const from=byId.get(edge.from),to=byId.get(edge.to),color=edge.active?[.92,.61,.21]:[.24,.55,.62],fy=(from.y??0)+.2,ty=(to.y??0)+.2
63
- // Orthogonal connectors use the same instanced mesh; only declared edges.
64
- cube((from.x+to.x)/2,fy,from.z,Math.abs(to.x-from.x)+.06,.06,.06,color)
65
- cube(to.x,(fy+ty)/2,from.z,.06,Math.abs(ty-fy)+.06,.06,color)
66
- cube(to.x,ty,(from.z+to.z)/2,.06,.06,Math.abs(to.z-from.z)+.06,color)
67
- }
68
- for(const item of model.items){
69
- const start=values.length/9
70
- const hash=[...item.id].reduce((v,c)=>(Math.imul(v,31)+c.charCodeAt(0))|0,0),color=item.disabled?[.52,.54,.56]:palette[(hash>>>0)%palette.length],{x,z}=item
71
- offsetY=item.y??0
72
- if(item.form!=='book')cube(x,.07,z,2,.18,1.8,item.selected?[.98,.77,.34]:[.75,.80,.70])
73
- 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]}
74
- for(const [dx,y,dz,sx,sy,sz,tint]of objectTemplates[item.form])cube(x+dx,y,z+dz,sx,sy,sz,tints[tint])
75
- if(item.kind==='character')actors.push({id:item.id,start,end:values.length/9})
76
- }
77
- if(values.length/9>=worldLimits.instances)throw Error('WorldGeometryLimitExceeded')
78
- return new Float32Array(values)
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
  }
@@ -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;
@@ -1,21 +1,31 @@
1
1
  // Presentation input only: shared movement bindings and on-demand control guide.
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)
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
- const guide=document.createElement('section');guide.setAttribute('aria-label','World controls')
12
- const section=(title,rows)=>{const heading=document.createElement('h3'),list=document.createElement('dl');heading.textContent=title
13
- for(const [input,action]of rows){const term=document.createElement('dt'),description=document.createElement('dd');term.textContent=input;description.textContent=action;list.append(term,description)}
14
- guide.append(heading,list)
15
- }
16
- 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']])
17
- const note=document.createElement('p');note.textContent='Choose Walk through world under In this space → Navigation, then close the panel and select the world to use movement keys.';guide.append(note)
18
- 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']]))
19
- section('Keyboard navigation',[['Tab / Shift + Tab','Move between controls'],['Enter / Space','Activate the focused button']])
20
- return guide
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(); }