@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.
- package/README.md +6 -0
- package/package.json +19 -11
- 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-runtime.d.mts
CHANGED
|
@@ -1,36 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
relations?: any[];
|
|
4
|
-
onActivate?: () => void;
|
|
5
|
-
conversation?: any;
|
|
6
|
-
activity?: any;
|
|
7
|
-
target?: any;
|
|
8
|
-
pattern?: string;
|
|
9
|
-
context?: any;
|
|
10
|
-
activityPresentation?: {};
|
|
11
|
-
}): {
|
|
12
|
-
update: (objects: any, relations?: any[]) => void;
|
|
13
|
-
close: () => void;
|
|
14
|
-
mount: (nextHost: any, options: any) => void;
|
|
15
|
-
park: () => void;
|
|
16
|
-
snapshot: () => {
|
|
17
|
-
target: any;
|
|
18
|
-
presentation: any;
|
|
19
|
-
navigation: string;
|
|
20
|
-
populated: boolean;
|
|
21
|
-
x: number;
|
|
22
|
-
y: number;
|
|
23
|
-
z: number;
|
|
24
|
-
yaw: number;
|
|
25
|
-
pitch: number;
|
|
26
|
-
} | {
|
|
27
|
-
target: any;
|
|
28
|
-
presentation: any;
|
|
29
|
-
navigation: string;
|
|
30
|
-
populated: boolean;
|
|
31
|
-
};
|
|
32
|
-
activity(value: any): void;
|
|
33
|
-
apply(value: any): void;
|
|
34
|
-
change(action: any): void;
|
|
35
|
-
pause(value: any): void;
|
|
36
|
-
};
|
|
1
|
+
import type { WorldOptions, World } from './world-api.mjs';
|
|
2
|
+
export declare function createWorld(host: HTMLElement, options?: WorldOptions): World;
|
package/src/world-runtime.mjs
CHANGED
|
@@ -1,312 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import {keyDirection} from './world-input.mjs'
|
|
8
|
-
import {frameLayout} from './frame-layout.mjs'
|
|
9
|
-
import {mapProjection} from './open-world-projection.mjs'
|
|
10
|
-
|
|
11
|
-
// Host-local presentation. No persistence, network, grants, inference or synthetic people.
|
|
12
|
-
export function createWorld(host,{objects=[],relations=[],pose:restored,onActivate=()=>{},conversation=null,activity=null,target=null,pattern='ground',context=null,activityPresentation={}}={}){
|
|
13
|
-
if(restored?.target!==target)restored=null
|
|
14
|
-
const document=host.ownerDocument,window=document.defaultView,controller=new AbortController(),{signal}=controller
|
|
15
|
-
const make=(tag,className,text)=>{const n=document.createElement(tag);n.className=className;if(text)n.textContent=text;return n}
|
|
16
|
-
const wrap=make('section','mind-world'),canvas=make('canvas','world-canvas'),labels=make('div','world-labels'),hud=make('div','world-hud')
|
|
17
|
-
canvas.tabIndex=0;canvas.setAttribute('aria-label','Explore your world. W A S D or arrows to walk. Drag to look. E to interact. M for Map. Q to mark.')
|
|
18
|
-
wrap.setAttribute('aria-label','Thinking garden');wrap.dataset.presentationOnly='true'
|
|
19
|
-
const aim=make('span','world-aim','+'),hint=make('button','world-hint'),status=make('p','world-gpu-error')
|
|
20
|
-
aim.setAttribute('aria-hidden','true');status.hidden=true;status.setAttribute('role','alert')
|
|
21
|
-
const tools=make('div','world-controls'),nearby=make('details','world-nearby'),list=make('div','world-directory'),summary=make('summary','','In this space')
|
|
22
|
-
hint.type='button';hint.addEventListener('click',()=>{nearby.open=true},{signal})
|
|
23
|
-
nearby.append(summary,list);hud.append(hint,tools,nearby)
|
|
24
|
-
wrap.append(canvas,labels,aim,hud,status);host.prepend(wrap)
|
|
25
|
-
const roleStatus=(value,id)=>activityPresentation.describeRole?.(value,id)??null
|
|
26
|
-
const engine=activityPresentation.createView?.(document,wrap)??{panel:document.createElement('div'),update(){}}
|
|
27
|
-
engine.panel.hidden=pattern==='garden'
|
|
28
|
-
let spacing='balanced',placedSpacing=null
|
|
29
|
-
let pose=restored?Object.fromEntries(Object.keys(initialPose()).map(key=>[key,restored[key]])):initialPose(),model={items:[],relations:[]},gpu=null,geometry=null,actorRanges=[],closed=false,lost=false,dirty=true,frame=null,motionTimer=null,last=0,frames=0,drag=null,hovered=null,paused=false,zoom=1
|
|
30
|
-
let projected=[],labelNodes=new Map(),directoryNodes=new Map(),geometryKey=null,uploads=0,initializations=0,navigation='overview',needsFit=!restored,lastWidth=0,lastHeight=0,selectedId=null,markedId=null
|
|
31
|
-
const recent=new Map(),poses=new Map()
|
|
32
|
-
let fittedPopulation=Boolean(restored?.populated)
|
|
33
|
-
let sourceObjects=[],sourceRelations=relations,view=restored?.presentation??{pattern,groupBy:'declared',group:null,page:0},projection=null,inputKey=null
|
|
34
|
-
wrap.dataset.target=target??''
|
|
35
|
-
const bubble=make('aside','world-conversation');bubble.hidden=true;bubble.setAttribute('role','dialog');bubble.tabIndex=-1;wrap.append(bubble)
|
|
36
|
-
let conversationId=null,returnFocus=null
|
|
37
|
-
let anchorId=null
|
|
38
|
-
const shell=host.closest('.delivery-shell')
|
|
39
|
-
shell?.addEventListener('world-anchor-request',event=>{anchorId=event.detail.id;publishAnchor()},{signal})
|
|
40
|
-
function objectRect(id){
|
|
41
|
-
const item=model.items.find(item=>item.id===id),width=host.clientWidth,height=host.clientHeight
|
|
42
|
-
if(!item||!width||!height)return null
|
|
43
|
-
const [w,h,d]=objectBounds[item.form],matrix=cameraMatrix(pose,width/height,zoom),points=[]
|
|
44
|
-
for(const x of [-w/2,w/2])for(const y of [0,h])for(const z of [-d/2,d/2]){
|
|
45
|
-
const point=screenPoint(matrix,item.x+x,item.y+y,item.z+z,width,height)
|
|
46
|
-
if(point?.visible)points.push(point)
|
|
47
|
-
}
|
|
48
|
-
if(!points.length)return null
|
|
49
|
-
return {left:Math.min(...points.map(p=>p.x)),right:Math.max(...points.map(p=>p.x)),top:Math.min(...points.map(p=>p.y)),bottom:Math.max(...points.map(p=>p.y))}
|
|
50
|
-
}
|
|
51
|
-
function publishAnchor(){if(anchorId)shell?.dispatchEvent(new window.CustomEvent('world-anchor',{detail:{id:anchorId,bounds:objectRect(anchorId)}}))}
|
|
52
|
-
const map=make('details','world-map'),mapList=make('div','world-map-list'),mapNote=make('small',''),mapButton=make('button','','Mark selected')
|
|
53
|
-
mapButton.type='button';map.append(make('summary','','Map'),mapNote,mapList,mapButton);hud.append(map)
|
|
54
|
-
const keys=new Set(),inputBlocked=()=>paused||nearby.open||map.open||!bubble.hidden||!document.hasFocus()||Boolean(document.querySelector('.frame-panel:not([hidden])'))
|
|
55
|
-
const button=(text,label,action,parent=tools)=>{const b=make('button','',text);b.type='button';b.setAttribute('aria-label',label);b.addEventListener('click',action,{signal});parent.append(b);return b}
|
|
56
|
-
const inspect=button('Inspect','Inspect aimed object',()=>activate(hovered))
|
|
57
|
-
const reset=button('⛶','View all',()=>overview());reset.dataset.overviewControl='true'
|
|
58
|
-
const settings=make('div','world-layout'),breadcrumb=make('div','world-breadcrumb'),location=make('output',''),pagination=make('div','world-pagination')
|
|
59
|
-
const selector=(label,entries,change)=>{const field=make('label','',label),select=make('select','');select.setAttribute('aria-label',label);for(const [id,title]of entries){const option=make('option','',title);option.value=id;select.append(option)}select.addEventListener('change',()=>change(select.value),{signal});field.append(select);settings.append(field);return select}
|
|
60
|
-
const navigationSelect=selector('Navigation',[['overview','Overview'],['explore','Walk through world']],value=>{navigation=value;walk.hidden=value!=='explore';if(value==='overview')overview();else{pose={...initialPose(),cameraMode:'third-person'};stopInput();invalidate()}})
|
|
61
|
-
const patternSelect=selector('Spatial layout',spatialPatterns,value=>{view={...view,pattern:value,group:null,page:0};needsFit=true;update(sourceObjects,sourceRelations)})
|
|
62
|
-
const groupSelect=selector('Group by',spatialGroupings,value=>{view={...view,groupBy:value,group:null,page:0};needsFit=true;update(sourceObjects,sourceRelations)})
|
|
63
|
-
selector('Object spacing',Object.keys(spacingPresets).map(id=>[id,id[0].toUpperCase()+id.slice(1)]),value=>{spacing=value;needsFit=true;update(sourceObjects,sourceRelations)}).value=spacing
|
|
64
|
-
const legend=make('p','world-legend',pattern==='garden'?'People are shown here. Their published information is available inside each person’s panel; movement is visual only.':'Placement follows layout settings; only lines represent declared relationships.');settings.append(legend)
|
|
65
|
-
const back=button('All shelves','Return to all shelves',()=>{view={...view,group:null,page:0};needsFit=true;update(sourceObjects,sourceRelations)},breadcrumb)
|
|
66
|
-
breadcrumb.append(location);hud.append(breadcrumb);nearby.append(pagination,settings)
|
|
67
|
-
const previous=button('Previous','Previous objects',()=>{view={...view,page:view.page-1};needsFit=true;update(sourceObjects,sourceRelations)},pagination),pageCount=make('output','')
|
|
68
|
-
pagination.append(pageCount)
|
|
69
|
-
const nextPage=button('Next','Next objects',()=>{view={...view,page:view.page+1};needsFit=true;update(sourceObjects,sourceRelations)},pagination)
|
|
70
|
-
const updates=make('details','world-updates'),updatesTitle=make('summary','','Observed changes'),updateList=make('div',''),updateHelp=make('p','','Changes observed while this view is open. Not execution history.')
|
|
71
|
-
updates.setAttribute('aria-label','Changes in this view');updates.append(updatesTitle,updateHelp,updateList);hud.append(updates)
|
|
72
|
-
const query=make('input',''),noMatches=make('p','','No matching objects in this scope.')
|
|
73
|
-
query.type='search';query.maxLength=128;query.placeholder='Find in this space';query.setAttribute('aria-label','Find in this space');noMatches.hidden=true
|
|
74
|
-
nearby.insertBefore(query,list);nearby.insertBefore(noMatches,list)
|
|
75
|
-
function filterDirectory(){let count=0;const term=query.value.trim().toLocaleLowerCase();for(const row of directoryNodes.values()){row.hidden=!row.textContent.toLocaleLowerCase().includes(term);if(!row.hidden)count++}noMatches.hidden=!term||count>0}
|
|
76
|
-
query.addEventListener('input',filterDirectory,{signal})
|
|
77
|
-
host.closest('.delivery-shell')?.addEventListener('frame-panel-open',()=>{nearby.open=false;map.open=false;updates.open=false;closeConversation()},{signal})
|
|
78
|
-
nearby.addEventListener('toggle',()=>{map.hidden=nearby.open;if(nearby.open){map.open=false;updates.open=false;closeConversation()}},{signal})
|
|
79
|
-
updates.addEventListener('toggle',()=>{if(updates.open){nearby.open=false;closeConversation()}},{signal})
|
|
80
|
-
map.addEventListener('toggle',()=>{if(map.open){nearby.open=false;updates.open=false;closeConversation();stopInput()}},{signal})
|
|
81
|
-
const orbitCamera=viewCamera(pose,1),orbit=new OrbitControls(orbitCamera,canvas)
|
|
82
|
-
orbit.enableDamping=false;orbit.autoRotate=false;orbit.enableRotate=false;orbit.screenSpacePanning=true;orbit.minDistance=3;orbit.maxDistance=1500
|
|
83
|
-
orbit.mouseButtons.LEFT=MOUSE.PAN;orbit.touches.ONE=TOUCH.PAN;orbit.touches.TWO=TOUCH.DOLLY_PAN
|
|
84
|
-
let syncing=false
|
|
85
|
-
orbit.addEventListener('change',()=>{if(syncing)return;pose={x:orbitCamera.position.x,y:orbitCamera.position.y,z:orbitCamera.position.z,yaw:-orbitCamera.rotation.y,pitch:orbitCamera.rotation.x,focus:{x:orbit.target.x,y:orbit.target.y,z:orbit.target.z}};needsFit=false;invalidate()})
|
|
86
|
-
function syncOrbit(){syncing=true;viewCamera(pose,Math.max(.1,host.clientWidth/Math.max(1,host.clientHeight)),zoom,orbitCamera);const focus=pose.focus??{x:pose.x,y:pose.y,z:pose.z-8};orbit.target.set(focus.x,focus.y,focus.z);orbit.update();syncing=false}
|
|
87
|
-
function overview(){navigation='overview';navigationSelect.value=navigation;walk.hidden=true;lastWidth=host.clientWidth;lastHeight=host.clientHeight;pose=fitPose(model.items,lastWidth/Math.max(1,lastHeight));needsFit=false;stopInput();syncOrbit();invalidate()}
|
|
88
|
-
function fitViewport(){const width=host.clientWidth,height=host.clientHeight;if(!width||!height||(lastWidth===width&&lastHeight===height))return;lastWidth=width;lastHeight=height;if(navigation==='overview'){pose=fitPose(model.items,width/height);syncOrbit()}invalidate()}
|
|
89
|
-
function focusObject(id){const item=model.items.find(i=>i.id===id);if(!item)return;selectedId=id;wrap.dataset.selectedId=id
|
|
90
|
-
for(const [ref,node]of labelNodes)node.dataset.selected=String(ref===id)
|
|
91
|
-
renderMap();pose=navigation==='overview'?fitPose([item],host.clientWidth/Math.max(1,host.clientHeight)):{...initialPose(),cameraMode:'third-person',x:item.x,y:item.y+1.85,z:item.z+6,pitch:0};syncOrbit();invalidate()}
|
|
92
|
-
function renderMap(){
|
|
93
|
-
const projected=mapProjection(model.items,{selected:selectedId,marked:markedId}),items=projected.items
|
|
94
|
-
mapNote.textContent=projected.omitted?`${items.length} of ${projected.total} objects shown. Narrow this view to see the rest.`:`${items.length} objects in this view`
|
|
95
|
-
mapButton.disabled=!selectedId||!model.items.some(item=>item.id===selectedId);mapButton.textContent=selectedId===markedId?'Clear marker':'Mark selected'
|
|
96
|
-
mapList.replaceChildren();if(!items.length)return
|
|
97
|
-
const xs=items.map(item=>item.x),zs=items.map(item=>item.z),minX=Math.min(...xs),maxX=Math.max(...xs),minZ=Math.min(...zs),maxZ=Math.max(...zs)
|
|
98
|
-
for(const item of items){const node=make('button','world-map-point',item.title.slice(0,1).toUpperCase());node.type='button';node.title=item.title;node.dataset.mapId=item.id;node.dataset.selected=String(item.selected);node.dataset.marked=String(item.marked)
|
|
99
|
-
node.setAttribute('aria-label','Find '+item.title+' on map');node.style.left=(10+80*(item.x-minX)/Math.max(1,maxX-minX))+'%';node.style.top=(10+80*(item.z-minZ)/Math.max(1,maxZ-minZ))+'%';mapList.append(node)}
|
|
100
|
-
}
|
|
101
|
-
mapList.addEventListener('click',event=>{const id=event.target.closest('[data-map-id]')?.dataset.mapId;if(id)focusObject(id)},{signal})
|
|
102
|
-
mapButton.addEventListener('click',()=>{if(!selectedId)return;markedId=markedId===selectedId?null:selectedId;wrap.dataset.markedId=markedId??'';for(const [id,node]of labelNodes)node.dataset.marked=String(id===markedId);renderMap()},{signal})
|
|
103
|
-
const enableOrbit=()=>{orbit.enabled=navigation==='overview'&&!inputBlocked();if(orbit.enabled)syncOrbit()}
|
|
104
|
-
canvas.addEventListener('pointerdown',enableOrbit,{signal,capture:true});canvas.addEventListener('wheel',enableOrbit,{signal,capture:true})
|
|
105
|
-
function closeConversation(){conversationId=null;bubble.hidden=true;bubble.replaceChildren();if(returnFocus?.isConnected)returnFocus.focus({preventScroll:true});returnFocus=null;invalidate()}
|
|
106
|
-
function activate(id){const item=model.items.find(item=>item.id===id);if(!item||item.disabled)return;selectedId=id;wrap.dataset.selectedId=id;for(const [ref,node]of labelNodes)node.dataset.selected=String(ref===id);renderMap();stopInput();nearby.open=false;map.open=false
|
|
107
|
-
if(item.kind==='group'){view={...view,group:view.pattern==='garden'&&view.group===item.id?null:item.id,page:0};needsFit=view.pattern!=='garden';update(sourceObjects,sourceRelations);if(view.pattern==='garden')focusObject(item.id);return}
|
|
108
|
-
recent.delete(id);renderUpdates()
|
|
109
|
-
const body=conversation?.(item,()=>{closeConversation();onActivate(id)})
|
|
110
|
-
if(!body){onActivate(id);return}
|
|
111
|
-
returnFocus=document.activeElement;conversationId=id;bubble.setAttribute('aria-label','Conversation with '+item.title)
|
|
112
|
-
const title=make('strong','',item.title),dismiss=make('button','','×');dismiss.type='button';dismiss.setAttribute('aria-label','Close conversation');dismiss.addEventListener('click',closeConversation,{once:true})
|
|
113
|
-
const head=make('header','');head.append(title,dismiss);bubble.replaceChildren(head,body);bubble.hidden=false;bubble.focus({preventScroll:true});positionBubble();invalidate()
|
|
114
|
-
}
|
|
115
|
-
function positionBubble(){
|
|
116
|
-
if(!conversationId)return
|
|
117
|
-
if(!model.items.some(item=>item.id===conversationId)){closeConversation();return}
|
|
118
|
-
const {panel}=frameLayout(host.clientWidth,host.clientHeight,'actions',objectRect(conversationId))
|
|
119
|
-
Object.assign(bubble.style,{left:panel.x+'px',top:panel.y+'px',width:panel.width+'px',height:panel.height+'px',maxHeight:panel.height+'px'})
|
|
120
|
-
}
|
|
121
|
-
function stopInput(){keys.clear();drag=null;if(document.pointerLockElement===canvas)document.exitPointerLock()}
|
|
122
|
-
function stopFrame(){if(frame!==null){window.cancelAnimationFrame(frame);frame=null}if(motionTimer!==null){window.clearTimeout(motionTimer);motionTimer=null}last=0}
|
|
123
|
-
function invalidate(){dirty=true;if(!closed&&!paused&&!lost&&!document.hidden&&frame===null)frame=window.requestAnimationFrame(tick)}
|
|
124
|
-
function tick(now){
|
|
125
|
-
frame=null;if(closed||paused||lost||document.hidden)return
|
|
126
|
-
if(inputBlocked())keys.clear()
|
|
127
|
-
const moving=keys.size>0
|
|
128
|
-
if(now-last<1000/worldLimits.fps){if(dirty||moving)frame=window.requestAnimationFrame(tick);return}
|
|
129
|
-
if(moving){pose=movePose(pose,Number(keys.has('forward'))-Number(keys.has('back')),Number(keys.has('right'))-Number(keys.has('left')),last?(now-last)/1000:1/30,Number(keys.has('rise'))-Number(keys.has('descend')));dirty=true}
|
|
130
|
-
last=now
|
|
131
|
-
if(dirty){dirty=false;draw()}
|
|
132
|
-
if(moving)frame=window.requestAnimationFrame(tick)
|
|
133
|
-
}
|
|
134
|
-
function draw(){
|
|
135
|
-
if(!gpu)return
|
|
136
|
-
const width=host.clientWidth,height=host.clientHeight;if(!width||!height)return
|
|
137
|
-
fitViewport()
|
|
138
|
-
const size=renderSize(width,height,window.devicePixelRatio||1)
|
|
139
|
-
const matrix=cameraMatrix(pose,width/height,zoom)
|
|
140
|
-
const visibleActors=new Set(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))
|
|
141
|
-
if(window.matchMedia('(prefers-reduced-motion: reduce)').matches)visibleActors.clear()
|
|
142
|
-
gpu.draw(pose,width/height,zoom,size,window.performance.now(),visibleActors);frames++;positionBubble()
|
|
143
|
-
wrap.dataset.animatedNpcs=String(visibleActors.size)
|
|
144
|
-
if(visibleActors.size&&motionTimer===null&&!paused&&!document.hidden)
|
|
145
|
-
motionTimer=window.setTimeout(()=>{motionTimer=null;invalidate()},200)
|
|
146
|
-
projected=[]
|
|
147
|
-
for(const item of model.items){const point=screenPoint(matrix,item.x,item.y+objectBounds[item.form][1]+.45,item.z,width,height);if(point?.visible&&point.depth<1800)projected.push({item,...point})}
|
|
148
|
-
projected.sort((a,b)=>a.depth-b.depth)
|
|
149
|
-
const displayed=[],labelWidth=width<600?138:182
|
|
150
|
-
for(const point of projected){
|
|
151
|
-
const x=clamp(point.x,90,width-90),y=clamp(point.y,96,Math.max(96,height-80))
|
|
152
|
-
if(displayed.some(p=>Math.abs(p.x-x)<labelWidth&&Math.abs(p.y-y)<85))continue
|
|
153
|
-
displayed.push({...point,x,y});if(displayed.length===8)break
|
|
154
|
-
}
|
|
155
|
-
const visible=new Set(displayed.map(p=>p.item.id))
|
|
156
|
-
for(const [id,node]of labelNodes)node.hidden=!visible.has(id)
|
|
157
|
-
for(const point of displayed){const node=labelNodes.get(point.item.id);node.style.left=point.x+'px';node.style.top=point.y+'px';node.dataset.distance=point.depth.toFixed(1)}
|
|
158
|
-
publishAnchor()
|
|
159
|
-
hovered=pickWorld(model.items,pose,width/2,height/2,width,height,zoom)
|
|
160
|
-
const target=model.items.find(item=>item.id===hovered)
|
|
161
|
-
inspect.disabled=!hovered;inspect.hidden=navigation==='overview';aim.hidden=navigation==='overview'
|
|
162
|
-
hint.textContent=navigation==='overview'?(view.pattern==='garden'?`Open people · ${model.items.filter(item=>item.kind==='character').length}`:`Browse this space · ${model.items.length}`):target?target.title+' · Inspect':model.items.length?'Select a person or a group':'No information is visible in this view'
|
|
163
|
-
hint.hidden=navigation!=='overview'||!model.items.length
|
|
164
|
-
canvas.setAttribute('aria-label',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.')
|
|
165
|
-
for(const [id,node]of labelNodes)node.dataset.aimed=String(id===hovered)
|
|
166
|
-
wrap.dataset.pose=JSON.stringify(pose);wrap.dataset.frames=String(frames);wrap.dataset.instances=String(gpu.count);wrap.dataset.objects=String(model.items.length);wrap.dataset.bufferPixels=String(canvas.width*canvas.height);wrap.dataset.renderer='three';wrap.dataset.renderStats=JSON.stringify(gpu.stats);wrap.dataset.cameraMode=pose.cameraMode??'overview';wrap.dataset.avatar=pose.cameraMode==='third-person'?'present':'hidden'
|
|
167
|
-
wrap.dataset.uploads=String(uploads);wrap.dataset.initializations=String(initializations);wrap.dataset.navigation=navigation
|
|
168
|
-
}
|
|
169
|
-
function fail(error){stopInput();stopFrame();status.hidden=false;status.textContent=(error.code??error.message??'WorldRenderingUnavailable')+' — The 3D view is unavailable. Use “In this space” to inspect the available information.';wrap.dataset.gpu='unavailable'}
|
|
170
|
-
function initialize(){try{gpu=createGpu(canvas);initializations++;gpu.pattern(view.pattern);if(geometry){gpu.upload(geometry,actorRanges);gpu.motionStates(new Map(model.items.filter(i=>i.kind==='character').map(i=>[i.id,roleStatus(activity,i.id)?.state??'Idle'])));uploads++}lost=false;status.hidden=true;wrap.dataset.gpu='ready';invalidate()}catch(error){gpu?.close();gpu=null;fail(error)}}
|
|
171
|
-
const setText=(node,value)=>{if(node.textContent!==value)node.textContent=value}
|
|
172
|
-
function renderUpdates(){
|
|
173
|
-
const latest=[...recent.values()].at(-1)
|
|
174
|
-
updatesTitle.textContent=latest?'Observed changes · '+recent.size+' · '+latest.title+' · '+latest.kind:'No changes observed in this view'
|
|
175
|
-
updateList.replaceChildren()
|
|
176
|
-
for(const [id,change]of [...recent].reverse()){
|
|
177
|
-
const button=make('button','',change.title+' · '+change.kind);button.type='button';button.dataset.changeId=id
|
|
178
|
-
button.disabled=!sourceObjects.some(item=>item.id===id);updateList.append(button)
|
|
179
|
-
}
|
|
180
|
-
for(const [id,node]of labelNodes)node.dataset.updated=String(recent.has(id))
|
|
181
|
-
}
|
|
182
|
-
updateList.addEventListener('click',event=>{
|
|
183
|
-
const id=event.target.closest('[data-change-id]')?.dataset.changeId;if(!id)return
|
|
184
|
-
if(view.pattern==='garden'&&!model.items.some(item=>item.id===id)){updates.open=false;onActivate(id);return}
|
|
185
|
-
const group=projection?.groups.find(group=>group.memberIds.includes(id))
|
|
186
|
-
if(group){view={...view,group:group.id,page:0};update(sourceObjects,sourceRelations)
|
|
187
|
-
const index=(view.pattern==='garden'?group.memberIds:[...sourceObjects.filter(i=>i.kind==='character').map(i=>i.id),...group.memberIds]).indexOf(id)
|
|
188
|
-
if(index>=24){view={...view,page:Math.floor(index/24)};update(sourceObjects,sourceRelations)}
|
|
189
|
-
}
|
|
190
|
-
updates.open=false;focusObject(id)
|
|
191
|
-
},{signal})
|
|
192
|
-
labels.addEventListener('click',event=>activate(event.target.closest('[data-world-id]')?.dataset.worldId),{signal})
|
|
193
|
-
list.addEventListener('click',event=>{
|
|
194
|
-
const control=event.target.closest('button'),id=control?.closest('[data-directory-id]')?.dataset.directoryId
|
|
195
|
-
if(!id)return
|
|
196
|
-
if(control.dataset.focus==='true'){focusObject(id);nearby.open=false;canvas.focus({preventScroll:true})}else activate(id)
|
|
197
|
-
},{signal})
|
|
198
|
-
function update(objects,relations=[]){
|
|
199
|
-
engine.update(activity)
|
|
200
|
-
const nextProjection=spatialPattern(objects,relations,view),next=layoutWorld(nextProjection.objects,nextProjection.relations,nextProjection.state.pattern,spacing)
|
|
201
|
-
const nextInputKey=JSON.stringify([objects,relations,nextProjection.state,spacing,activity])
|
|
202
|
-
if(inputKey===nextInputKey&&!needsFit)return
|
|
203
|
-
// Keep remembered positions for unchanged membership, even if producer ordering changes.
|
|
204
|
-
if(placedSpacing===spacing&&(model.pattern??'ground')===(next.pattern??'ground')&&model.items.length===next.items.length&&model.items.every(item=>next.items.some(n=>n.id===item.id&&n.anchor===item.anchor&&n.plot===item.plot&&n.member===item.member))){
|
|
205
|
-
const previous=new Map(model.items.map(item=>[item.id,item]))
|
|
206
|
-
for(const item of next.items){const before=previous.get(item.id);item.x=before.x;item.y=before.y;item.z=before.z}
|
|
207
|
-
}
|
|
208
|
-
placedSpacing=spacing
|
|
209
|
-
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])])
|
|
210
|
-
const changedGeometry=key!==geometryKey,nextActors=changedGeometry?[]:actorRanges,nextGeometry=changedGeometry?worldGeometry(next,nextActors):geometry
|
|
211
|
-
const before=new Map(sourceObjects.map(item=>[item.id,item]));let changed=false
|
|
212
|
-
if(before.size){
|
|
213
|
-
for(const item of objects){const previous=before.get(item.id)
|
|
214
|
-
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
|
|
215
|
-
if(kind){recent.delete(item.id);recent.set(item.id,{title:item.title,kind});changed=true}
|
|
216
|
-
}
|
|
217
|
-
const ids=new Set(objects.map(item=>item.id));for(const item of before.values())if(!ids.has(item.id)){recent.delete(item.id);recent.set(item.id,{title:item.title,kind:'No longer in this view'});changed=true}
|
|
218
|
-
while(recent.size>5)recent.delete(recent.keys().next().value)
|
|
219
|
-
}
|
|
220
|
-
sourceObjects=objects.map(item=>({...item}));sourceRelations=relations;projection=nextProjection;view=projection.state;inputKey=nextInputKey
|
|
221
|
-
if(conversationId&&(changed||!objects.some(i=>i.id===conversationId)))closeConversation()
|
|
222
|
-
model=next;geometry=nextGeometry;actorRanges=nextActors;geometryKey=key
|
|
223
|
-
if(!fittedPopulation&&model.items.length){fittedPopulation=true;if(navigation==='overview')needsFit=true}
|
|
224
|
-
const ids=new Set(model.items.map(item=>item.id))
|
|
225
|
-
for(const [id,node]of labelNodes)if(!ids.has(id)){node.remove();labelNodes.delete(id);directoryNodes.get(id)?.remove();directoryNodes.delete(id)}
|
|
226
|
-
patternSelect.value=view.pattern;groupSelect.value=view.groupBy;groupSelect.parentElement.hidden=view.pattern==='ground'||view.pattern==='garden'
|
|
227
|
-
patternSelect.parentElement.hidden=pattern==='garden'
|
|
228
|
-
breadcrumb.hidden=view.pattern==='ground'||view.pattern==='garden';back.hidden=!view.group;location.textContent=projection.label+' · '+projection.total+' objects'
|
|
229
|
-
pagination.hidden=projection.pages===1;previous.disabled=view.page===0;nextPage.disabled=view.page===projection.pages-1;pageCount.textContent=(view.page+1)+' / '+projection.pages
|
|
230
|
-
wrap.dataset.pattern=view.pattern;wrap.dataset.groupBy=view.groupBy;wrap.dataset.group=view.group??'';wrap.dataset.sourceObjects=String(objects.length)
|
|
231
|
-
for(const [index,item]of model.items.entries()){
|
|
232
|
-
let b=labelNodes.get(item.id),row=directoryNodes.get(item.id)
|
|
233
|
-
if(!b){
|
|
234
|
-
b=make('button','world-name');b.type='button';b.dataset.worldId=item.id;b.append(make('strong',''),make('span',''));labelNodes.set(item.id,b)
|
|
235
|
-
row=make('div','');row.dataset.directoryId=item.id
|
|
236
|
-
const focus=make('button',''),open=make('button','');focus.type=open.type='button';focus.dataset.focus='true';row.append(focus,open);directoryNodes.set(item.id,row)
|
|
237
|
-
}
|
|
238
|
-
const review=item.kind==='character'?roleStatus(activity,item.id):null,state=review?.state??null
|
|
239
|
-
const detail=item.disabled?'Unavailable':review?review.label:objectForms[item.form]+' · '+(item.summary||'Inspect information')
|
|
240
|
-
setText(b.children[0],item.title);setText(b.children[1],detail);b.dataset.worldKind=item.kind;b.disabled=Boolean(item.disabled);b.setAttribute('aria-label',item.title+'. '+detail)
|
|
241
|
-
b.dataset.position=JSON.stringify([item.x,item.y,item.z])
|
|
242
|
-
b.dataset.objectForm=item.form;b.dataset.activity=state??'';b.dataset.supportRef=item.supportRef??'';b.dataset.selected=String(item.id===selectedId);b.dataset.marked=String(item.id===markedId)
|
|
243
|
-
if(labels.children[index]!==b)labels.insertBefore(b,labels.children[index]??null)
|
|
244
|
-
const [focus,open]=row.children;setText(focus,item.title);focus.setAttribute('aria-label','Find '+item.title);open.disabled=Boolean(item.disabled)
|
|
245
|
-
const action=item.kind==='group'?'Browse shelf':view.pattern==='garden'&&item.kind==='character'?'Talk to':'Inspect'
|
|
246
|
-
setText(open,action);open.setAttribute('aria-label',action+' '+item.title)
|
|
247
|
-
if(item.kind==='group')row.dataset.members=JSON.stringify(item.memberIds);else delete row.dataset.members
|
|
248
|
-
if(list.children[index]!==row)list.insertBefore(row,list.children[index]??null)
|
|
249
|
-
}
|
|
250
|
-
summary.textContent=(view.pattern==='garden'?'People':'In this space')+' · '+model.items.length
|
|
251
|
-
renderMap()
|
|
252
|
-
filterDirectory()
|
|
253
|
-
renderUpdates()
|
|
254
|
-
if(needsFit){overview();needsFit=false}
|
|
255
|
-
try{gpu?.pattern(view.pattern);if(changedGeometry&&gpu){gpu.upload(geometry,actorRanges);uploads++}gpu?.motionStates(new Map(model.items.filter(i=>i.kind==='character').map(i=>[i.id,roleStatus(activity,i.id)?.state??'Idle'])));invalidate()}catch(error){fail(error)}
|
|
256
|
-
}
|
|
257
|
-
function onKey(event){
|
|
258
|
-
if(event.target!==canvas||event.ctrlKey||event.metaKey||event.altKey||event.isComposing)return
|
|
259
|
-
if(event.type==='keydown'&&event.code==='KeyM'){event.preventDefault();map.open=!map.open;return}
|
|
260
|
-
if(event.type==='keydown'&&event.code==='KeyQ'){event.preventDefault();const target=hovered??selectedId;if(target){selectedId=target;mapButton.click()}return}
|
|
261
|
-
if(navigation==='overview'){if(event.type==='keydown'&&event.code==='KeyE')activate(hovered);return}
|
|
262
|
-
const key=keyDirection(event.code)
|
|
263
|
-
if(key){event.preventDefault();if(event.type==='keydown'&&!inputBlocked())keys.add(key);else keys.delete(key);invalidate()}
|
|
264
|
-
else if(event.type==='keydown'&&!event.repeat){if(event.code==='KeyE'){event.preventDefault();activate(hovered)}if(event.code==='Escape'){stopInput();canvas.blur()}}
|
|
265
|
-
}
|
|
266
|
-
canvas.addEventListener('keydown',onKey,{signal});window.addEventListener('keyup',event=>{const key=keyDirection(event.code);if(key){keys.delete(key);invalidate()}},{signal})
|
|
267
|
-
bubble.addEventListener('keydown',event=>{if(event.key==='Escape'){event.stopPropagation();closeConversation()}},{signal})
|
|
268
|
-
canvas.addEventListener('pointerdown',event=>{if(!event.isPrimary){drag=null;return}if(event.button!==0||inputBlocked())return;canvas.focus({preventScroll:true});drag={id:event.pointerId,x:event.clientX,y:event.clientY,startX:event.clientX,startY:event.clientY};canvas.setPointerCapture(event.pointerId)},{signal})
|
|
269
|
-
canvas.addEventListener('pointermove',event=>{if(drag?.id!==event.pointerId)return;if(navigation==='explore')pose=lookPose(pose,event.clientX-drag.x,event.clientY-drag.y);drag.x=event.clientX;drag.y=event.clientY;invalidate()},{signal})
|
|
270
|
-
canvas.addEventListener('pointerup',event=>{if(drag?.id!==event.pointerId)return;const clicked=Math.hypot(event.clientX-drag.startX,event.clientY-drag.startY)<5;drag=null;if(clicked){const r=canvas.getBoundingClientRect();activate(pickWorld(model.items,pose,event.clientX-r.left,event.clientY-r.top,r.width,r.height,zoom))}},{signal,capture:true})
|
|
271
|
-
for(const type of ['pointercancel','lostpointercapture'])canvas.addEventListener(type,()=>drag=null,{signal})
|
|
272
|
-
canvas.addEventListener('wheel',event=>{if(navigation==='overview'||event.ctrlKey||event.metaKey||inputBlocked()||!Number.isFinite(event.deltaY))return;event.preventDefault();const amount=clamp(-event.deltaY*(event.deltaMode===1?16:event.deltaMode===2?host.clientHeight:1)/100,-2,2);pose={...pose,x:clamp(pose.x+Math.sin(pose.yaw)*amount,-28,28),z:clamp(pose.z-Math.cos(pose.yaw)*amount,-350,28)};invalidate()},{signal,passive:false})
|
|
273
|
-
const walk=make('div','world-walk');walk.hidden=true;walk.setAttribute('role','group');walk.setAttribute('aria-label','Walk controls');hud.append(walk)
|
|
274
|
-
for(const [direction,icon]of [['forward','↑'],['left','←'],['back','↓'],['right','→'],['rise','⇧'],['descend','⇩']]){
|
|
275
|
-
const b=button(icon,'Walk '+direction,()=>{},walk)
|
|
276
|
-
b.addEventListener('pointerdown',event=>{if(event.button||inputBlocked())return;event.preventDefault();b.setPointerCapture(event.pointerId);keys.add(direction);invalidate()},{signal})
|
|
277
|
-
for(const type of ['pointerup','pointercancel','lostpointercapture'])b.addEventListener(type,()=>{keys.delete(direction);invalidate()},{signal})
|
|
278
|
-
b.addEventListener('click',event=>{if(event.detail===0&&!inputBlocked()){pose=movePose(pose,direction==='forward'?1:direction==='back'?-1:0,direction==='right'?1:direction==='left'?-1:0,.05,direction==='rise'?1:direction==='descend'?-1:0);invalidate()}},{signal})
|
|
279
|
-
}
|
|
280
|
-
nearby.addEventListener('toggle',()=>{if(nearby.open)stopInput()},{signal})
|
|
281
|
-
window.addEventListener('blur',()=>{wrap.dataset.motionPaused='true';stopInput();stopFrame()},{signal});window.addEventListener('focus',()=>{wrap.dataset.motionPaused='false';invalidate()},{signal})
|
|
282
|
-
document.addEventListener('visibilitychange',()=>{wrap.dataset.motionPaused=String(document.hidden);stopInput();if(document.hidden)stopFrame();else invalidate()},{signal})
|
|
283
|
-
window.addEventListener('pagehide',()=>{paused=true;stopInput();stopFrame()},{signal})
|
|
284
|
-
window.addEventListener('pageshow',()=>{paused=false;invalidate()},{signal})
|
|
285
|
-
canvas.addEventListener('webglcontextlost',event=>{event.preventDefault();lost=true;gpu?.close();gpu=null;fail(Error('WorldGraphicsContextLost'))},{signal})
|
|
286
|
-
canvas.addEventListener('webglcontextrestored',initialize,{signal})
|
|
287
|
-
const resize=new window.ResizeObserver(fitViewport);resize.observe(host)
|
|
288
|
-
update(objects,relations);initialize()
|
|
289
|
-
function snapshot(){return {...pose,target,presentation:{...view},navigation,populated:fittedPopulation}}
|
|
290
|
-
function park(){paused=true;stopInput();stopFrame();orbit.enabled=false;resize.disconnect();wrap.remove()}
|
|
291
|
-
function mount(nextHost,options){
|
|
292
|
-
if(closed)throw Error('WorldDisposed')
|
|
293
|
-
const changedTarget=target!==(options.target??null)
|
|
294
|
-
const changedContext=context!==(options.context??null);context=options.context??null
|
|
295
|
-
if(changedTarget){poses.set(target,snapshot());while(poses.size>8)poses.delete(poses.keys().next().value)}
|
|
296
|
-
host=nextHost;host.prepend(wrap);resize.disconnect();resize.observe(host)
|
|
297
|
-
onActivate=options.onActivate;conversation=options.conversation;activity=options.activity??null;target=options.target??null;wrap.dataset.target=target??'';paused=false
|
|
298
|
-
engine.panel.hidden=options.pattern==='garden'
|
|
299
|
-
if(conversationId)closeConversation()
|
|
300
|
-
if(changedTarget){const saved=poses.get(target);view=saved?.presentation??{pattern:options.pattern??'ground',groupBy:'declared',group:null,page:0};navigation=saved?.navigation??'overview';pose=saved??initialPose();fittedPopulation=Boolean(saved?.populated);needsFit=!saved;sourceObjects=[];recent.clear();query.value='';updates.open=false;nearby.open=false;map.open=false;selectedId=null;markedId=null;wrap.dataset.selectedId='';wrap.dataset.markedId='';inputKey=null;model={items:[],relations:[]}}
|
|
301
|
-
else if(changedContext){view={...view,group:null,page:0};sourceObjects=[];recent.clear();query.value='';updates.open=false;inputKey=null}
|
|
302
|
-
navigationSelect.value=navigation;walk.hidden=navigation!=='explore';update(options.objects,options.relations);syncOrbit();invalidate()
|
|
303
|
-
}
|
|
304
|
-
function close(){if(closed)return;closed=true;stopInput();stopFrame();resize.disconnect();controller.abort();orbit.dispose();gpu?.close();gpu=null;canvas.getContext('webgl2')?.getExtension('WEBGL_lose_context')?.loseContext();geometry=null;labelNodes.clear();directoryNodes.clear();recent.clear();poses.clear();wrap.remove()}
|
|
305
|
-
return {update,close,mount,park,snapshot,activity(value){activity=value??null;update(sourceObjects,sourceRelations)},apply(value){const next=clamp(value.z,.65,1.5);if(next!==zoom){zoom=next;syncOrbit();invalidate()}},change(action){
|
|
306
|
-
fitViewport()
|
|
307
|
-
if(action==='reset'){overview();return}
|
|
308
|
-
if(navigation==='overview'){syncOrbit();if(action==='in')orbit.dollyIn(1/1.2);else if(action==='out')orbit.dollyOut(1/1.2);else orbit.pan(action==='left'?40:action==='right'?-40:0,action==='up'?40:action==='down'?-40:0);orbit.update();return}
|
|
309
|
-
else if(['in','out','up','down','left','right'].includes(action)){const forward=['in','up'].includes(action)?1:['out','down'].includes(action)?-1:0,side=action==='left'?-1:action==='right'?1:0;for(let i=0;i<5;i++)pose=movePose(pose,forward,side,.05)}
|
|
310
|
-
invalidate()
|
|
311
|
-
},pause(value){paused=value;if(value){stopInput();stopFrame()}else invalidate()}}
|
|
1
|
+
import { WorldController } from './world-controller.mjs';
|
|
2
|
+
export function createWorld(host, options = {}) {
|
|
3
|
+
const controller = new WorldController(host, options);
|
|
4
|
+
// Only the installed presentation interface escapes; mutable lifecycle state stays private.
|
|
5
|
+
return Object.freeze({ update: controller.update, close: controller.close, park: controller.park, mount: controller.mount, snapshot: controller.snapshot,
|
|
6
|
+
activity: controller.activity, apply: controller.apply, change: controller.change, pause: controller.pause });
|
|
312
7
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { copyPose } from './world-api.mjs';
|
|
2
|
+
import { initialPose, viewCamera } from './world-model.mjs';
|
|
3
|
+
import { MOUSE, TOUCH } from 'three';
|
|
4
|
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
5
|
+
import { spatialPattern, spatialPatterns, spatialGroupings } from './spatial-patterns.mjs';
|
|
6
|
+
import { spacingPresets } from './object-presentation.mjs';
|
|
7
|
+
import { bindWorldEvents } from './world-events.mjs';
|
|
8
|
+
export function setupWorldView(ctx) {
|
|
9
|
+
if (ctx.restored?.target !== ctx.target)
|
|
10
|
+
ctx.restored = null;
|
|
11
|
+
spatialPattern(ctx.objects, ctx.relations, ctx.restored?.presentation ?? { pattern: ctx.pattern });
|
|
12
|
+
ctx.pose = ctx.restored ? copyPose(ctx.restored) : initialPose();
|
|
13
|
+
ctx.document = ctx.host.ownerDocument;
|
|
14
|
+
ctx.browser = ctx.document.defaultView;
|
|
15
|
+
if (!ctx.browser)
|
|
16
|
+
throw Error('WorldDocumentUnavailable');
|
|
17
|
+
ctx.window = ctx.browser;
|
|
18
|
+
ctx.controller = new AbortController();
|
|
19
|
+
ctx.signal = ctx.controller.signal;
|
|
20
|
+
ctx.make = (tag, className, text) => { const n = ctx.document.createElement(tag); n.className = className; if (text)
|
|
21
|
+
n.textContent = text; return n; };
|
|
22
|
+
ctx.wrap = ctx.make('section', 'mind-world');
|
|
23
|
+
ctx.canvas = ctx.make('canvas', 'world-canvas');
|
|
24
|
+
ctx.labels = ctx.make('div', 'world-labels');
|
|
25
|
+
ctx.hud = ctx.make('div', 'world-hud');
|
|
26
|
+
ctx.canvas.tabIndex = 0;
|
|
27
|
+
ctx.canvas.setAttribute('aria-label', 'Explore your world. W A S D or arrows to walk. Drag to look. E to interact. M for Map. Q to mark.');
|
|
28
|
+
ctx.wrap.setAttribute('aria-label', 'Thinking garden');
|
|
29
|
+
ctx.wrap.dataset.presentationOnly = 'true';
|
|
30
|
+
ctx.aim = ctx.make('span', 'world-aim', '+');
|
|
31
|
+
ctx.hint = ctx.make('button', 'world-hint');
|
|
32
|
+
ctx.status = ctx.make('p', 'world-gpu-error');
|
|
33
|
+
ctx.aim.setAttribute('aria-hidden', 'true');
|
|
34
|
+
ctx.status.hidden = true;
|
|
35
|
+
ctx.status.setAttribute('role', 'alert');
|
|
36
|
+
ctx.tools = ctx.make('div', 'world-controls');
|
|
37
|
+
ctx.nearby = ctx.make('details', 'world-nearby');
|
|
38
|
+
ctx.list = ctx.make('div', 'world-directory');
|
|
39
|
+
ctx.summary = ctx.make('summary', '', 'In this space');
|
|
40
|
+
ctx.hint.type = 'button';
|
|
41
|
+
ctx.nearby.append(ctx.summary, ctx.list);
|
|
42
|
+
ctx.hud.append(ctx.hint, ctx.tools, ctx.nearby);
|
|
43
|
+
ctx.wrap.append(ctx.canvas, ctx.labels, ctx.aim, ctx.hud, ctx.status);
|
|
44
|
+
ctx.host.prepend(ctx.wrap);
|
|
45
|
+
ctx.roleStatus = (value, id) => ctx.activityPresentation.describeRole?.(value, id) ?? null;
|
|
46
|
+
ctx.engine = ctx.activityPresentation.createView?.(ctx.document, ctx.wrap) ?? { panel: ctx.document.createElement('div'), update() { } };
|
|
47
|
+
ctx.engine.panel.hidden = ctx.pattern === 'garden';
|
|
48
|
+
ctx.spacing = 'balanced';
|
|
49
|
+
ctx.placedSpacing = null;
|
|
50
|
+
ctx.model = { items: [], relations: [] };
|
|
51
|
+
ctx.gpu = null;
|
|
52
|
+
ctx.geometry = null;
|
|
53
|
+
ctx.actorRanges = [];
|
|
54
|
+
ctx.closed = false;
|
|
55
|
+
ctx.attached = true;
|
|
56
|
+
ctx.lost = false;
|
|
57
|
+
ctx.dirty = true;
|
|
58
|
+
ctx.frame = null;
|
|
59
|
+
ctx.motionTimer = null;
|
|
60
|
+
ctx.last = 0;
|
|
61
|
+
ctx.frames = 0;
|
|
62
|
+
ctx.drag = null;
|
|
63
|
+
ctx.hovered = null;
|
|
64
|
+
ctx.paused = false;
|
|
65
|
+
ctx.zoom = 1;
|
|
66
|
+
ctx.projected = [];
|
|
67
|
+
ctx.labelNodes = new Map();
|
|
68
|
+
ctx.directoryNodes = new Map();
|
|
69
|
+
ctx.geometryKey = null;
|
|
70
|
+
ctx.uploads = 0;
|
|
71
|
+
ctx.initializations = 0;
|
|
72
|
+
ctx.navigation = 'overview';
|
|
73
|
+
ctx.needsFit = !ctx.restored;
|
|
74
|
+
ctx.lastWidth = 0;
|
|
75
|
+
ctx.lastHeight = 0;
|
|
76
|
+
ctx.selectedId = null;
|
|
77
|
+
ctx.markedId = null;
|
|
78
|
+
ctx.recent = new Map();
|
|
79
|
+
ctx.poses = new Map();
|
|
80
|
+
ctx.fittedPopulation = Boolean(ctx.restored?.populated);
|
|
81
|
+
ctx.sourceObjects = [];
|
|
82
|
+
ctx.sourceRelations = ctx.relations;
|
|
83
|
+
ctx.view = ctx.restored?.presentation ?? { pattern: ctx.pattern, groupBy: 'declared', group: null, page: 0 };
|
|
84
|
+
ctx.projection = null;
|
|
85
|
+
ctx.wrap.dataset.target = ctx.target ?? '';
|
|
86
|
+
ctx.bubble = ctx.make('aside', 'world-conversation');
|
|
87
|
+
ctx.bubble.hidden = true;
|
|
88
|
+
ctx.bubble.setAttribute('role', 'dialog');
|
|
89
|
+
ctx.bubble.tabIndex = -1;
|
|
90
|
+
ctx.wrap.append(ctx.bubble);
|
|
91
|
+
ctx.conversationId = null;
|
|
92
|
+
ctx.returnFocus = null;
|
|
93
|
+
ctx.anchorId = null;
|
|
94
|
+
ctx.shell = ctx.host.closest('.delivery-shell');
|
|
95
|
+
ctx.map = ctx.make('details', 'world-map');
|
|
96
|
+
ctx.mapList = ctx.make('div', 'world-map-list');
|
|
97
|
+
ctx.mapNote = ctx.make('small', '');
|
|
98
|
+
ctx.mapButton = ctx.make('button', '', 'Mark selected');
|
|
99
|
+
ctx.mapButton.type = 'button';
|
|
100
|
+
ctx.map.append(ctx.make('summary', '', 'Map'), ctx.mapNote, ctx.mapList, ctx.mapButton);
|
|
101
|
+
ctx.hud.append(ctx.map);
|
|
102
|
+
ctx.keys = new Set();
|
|
103
|
+
ctx.inputBlocked = () => ctx.paused || ctx.nearby.open || ctx.map.open || !ctx.bubble.hidden || !ctx.document.hasFocus() || Boolean(ctx.document.querySelector('.frame-panel:not([hidden])'));
|
|
104
|
+
ctx.button = (text, label, action, parent = ctx.tools) => { const b = ctx.make('button', '', text); b.type = 'button'; b.setAttribute('aria-label', label); b.addEventListener('click', action, { signal: ctx.signal }); parent.append(b); return b; };
|
|
105
|
+
ctx.inspect = ctx.button('Inspect', 'Inspect aimed object', () => ctx.activate(ctx.hovered));
|
|
106
|
+
ctx.reset = ctx.button('⛶', 'View all', () => ctx.overview());
|
|
107
|
+
ctx.reset.dataset.overviewControl = 'true';
|
|
108
|
+
ctx.settings = ctx.make('div', 'world-layout');
|
|
109
|
+
ctx.breadcrumb = ctx.make('div', 'world-breadcrumb');
|
|
110
|
+
ctx.location = ctx.make('output', '');
|
|
111
|
+
ctx.pagination = ctx.make('div', 'world-pagination');
|
|
112
|
+
ctx.selector = (label, entries, change) => { const field = ctx.make('label', '', label), select = ctx.make('select', ''); select.setAttribute('aria-label', label); for (const [id, title] of entries) {
|
|
113
|
+
const option = ctx.make('option', '', title);
|
|
114
|
+
option.value = id;
|
|
115
|
+
select.append(option);
|
|
116
|
+
} select.addEventListener('change', () => change(select.value), { signal: ctx.signal }); field.append(select); ctx.settings.append(field); return select; };
|
|
117
|
+
ctx.navigationSelect = ctx.selector('Navigation', [['overview', 'Overview'], ['explore', 'Walk through world']], value => { ctx.navigation = value; ctx.walk.hidden = value !== 'explore'; if (value === 'overview')
|
|
118
|
+
ctx.overview();
|
|
119
|
+
else {
|
|
120
|
+
ctx.pose = { ...initialPose(), cameraMode: 'third-person' };
|
|
121
|
+
ctx.stopInput();
|
|
122
|
+
ctx.invalidate();
|
|
123
|
+
} });
|
|
124
|
+
ctx.patternSelect = ctx.selector('Spatial layout', spatialPatterns, value => { ctx.view = { ...ctx.view, pattern: value, group: null, page: 0 }; ctx.needsFit = true; ctx.update(ctx.sourceObjects, ctx.sourceRelations); });
|
|
125
|
+
ctx.groupSelect = ctx.selector('Group by', spatialGroupings, value => { ctx.view = { ...ctx.view, groupBy: value, group: null, page: 0 }; ctx.needsFit = true; ctx.update(ctx.sourceObjects, ctx.sourceRelations); });
|
|
126
|
+
ctx.selector('Object spacing', Object.keys(spacingPresets).map(id => [id, id[0].toUpperCase() + id.slice(1)]), value => { ctx.spacing = value; ctx.needsFit = true; ctx.update(ctx.sourceObjects, ctx.sourceRelations); }).value = ctx.spacing;
|
|
127
|
+
ctx.legend = ctx.make('p', 'world-legend', ctx.pattern === 'garden' ? 'People are shown here. Their published information is available inside each person’s panel; movement is visual only.' : 'Placement follows layout settings; only lines represent declared relationships.');
|
|
128
|
+
ctx.settings.append(ctx.legend);
|
|
129
|
+
ctx.back = ctx.button('All shelves', 'Return to all shelves', () => { ctx.view = { ...ctx.view, group: null, page: 0 }; ctx.needsFit = true; ctx.update(ctx.sourceObjects, ctx.sourceRelations); }, ctx.breadcrumb);
|
|
130
|
+
ctx.breadcrumb.append(ctx.location);
|
|
131
|
+
ctx.hud.append(ctx.breadcrumb);
|
|
132
|
+
ctx.nearby.append(ctx.pagination, ctx.settings);
|
|
133
|
+
ctx.previous = ctx.button('Previous', 'Previous objects', () => { ctx.view = { ...ctx.view, page: ctx.view.page - 1 }; ctx.needsFit = true; ctx.update(ctx.sourceObjects, ctx.sourceRelations); }, ctx.pagination);
|
|
134
|
+
ctx.pageCount = ctx.make('output', '');
|
|
135
|
+
ctx.pagination.append(ctx.pageCount);
|
|
136
|
+
ctx.nextPage = ctx.button('Next', 'Next objects', () => { ctx.view = { ...ctx.view, page: ctx.view.page + 1 }; ctx.needsFit = true; ctx.update(ctx.sourceObjects, ctx.sourceRelations); }, ctx.pagination);
|
|
137
|
+
ctx.updates = ctx.make('details', 'world-updates');
|
|
138
|
+
ctx.updatesTitle = ctx.make('summary', '', 'Observed changes');
|
|
139
|
+
ctx.updateList = ctx.make('div', '');
|
|
140
|
+
ctx.updateHelp = ctx.make('p', '', 'Changes observed while this view is open. Not execution history.');
|
|
141
|
+
ctx.updates.setAttribute('aria-label', 'Changes in this view');
|
|
142
|
+
ctx.updates.append(ctx.updatesTitle, ctx.updateHelp, ctx.updateList);
|
|
143
|
+
ctx.hud.append(ctx.updates);
|
|
144
|
+
ctx.query = ctx.make('input', '');
|
|
145
|
+
ctx.noMatches = ctx.make('p', '', 'No matching objects in this scope.');
|
|
146
|
+
ctx.query.type = 'search';
|
|
147
|
+
ctx.query.maxLength = 128;
|
|
148
|
+
ctx.query.placeholder = 'Find in this space';
|
|
149
|
+
ctx.query.setAttribute('aria-label', 'Find in this space');
|
|
150
|
+
ctx.noMatches.hidden = true;
|
|
151
|
+
ctx.nearby.insertBefore(ctx.query, ctx.list);
|
|
152
|
+
ctx.nearby.insertBefore(ctx.noMatches, ctx.list);
|
|
153
|
+
ctx.orbitCamera = viewCamera(ctx.pose, 1);
|
|
154
|
+
ctx.orbit = new OrbitControls(ctx.orbitCamera, ctx.canvas);
|
|
155
|
+
ctx.orbit.enableDamping = false;
|
|
156
|
+
ctx.orbit.autoRotate = false;
|
|
157
|
+
ctx.orbit.enableRotate = false;
|
|
158
|
+
ctx.orbit.screenSpacePanning = true;
|
|
159
|
+
ctx.orbit.minDistance = 3;
|
|
160
|
+
ctx.orbit.maxDistance = 1500;
|
|
161
|
+
ctx.orbit.mouseButtons.LEFT = MOUSE.PAN;
|
|
162
|
+
ctx.orbit.touches.ONE = TOUCH.PAN;
|
|
163
|
+
ctx.orbit.touches.TWO = TOUCH.DOLLY_PAN;
|
|
164
|
+
ctx.syncing = false;
|
|
165
|
+
ctx.enableOrbit = () => { ctx.orbit.enabled = ctx.navigation === 'overview' && !ctx.inputBlocked(); if (ctx.orbit.enabled)
|
|
166
|
+
ctx.syncOrbit(); };
|
|
167
|
+
ctx.setText = (node, value) => { if (node.textContent !== value)
|
|
168
|
+
node.textContent = value ?? ''; };
|
|
169
|
+
ctx.walk = ctx.make('div', 'world-walk');
|
|
170
|
+
ctx.walk.hidden = true;
|
|
171
|
+
ctx.walk.setAttribute('role', 'group');
|
|
172
|
+
ctx.walk.setAttribute('aria-label', 'Walk controls');
|
|
173
|
+
ctx.hud.append(ctx.walk);
|
|
174
|
+
ctx.resize = new ctx.window.ResizeObserver(ctx.fitViewport);
|
|
175
|
+
ctx.resize.observe(ctx.host);
|
|
176
|
+
bindWorldEvents(ctx);
|
|
177
|
+
ctx.update(ctx.objects, ctx.relations);
|
|
178
|
+
ctx.initialize();
|
|
179
|
+
}
|
package/src/world-surface.d.mts
CHANGED
|
@@ -1,7 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
world: any;
|
|
4
|
-
release(): void;
|
|
5
|
-
};
|
|
6
|
-
close(): void;
|
|
7
|
-
};
|
|
1
|
+
import type { ActivityPresentation, WorldSurface } from './world-api.mjs';
|
|
2
|
+
export declare function createWorldSurface(activityPresentation?: ActivityPresentation): WorldSurface;
|
package/src/world-surface.mjs
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import {createWorld} from './world-runtime.mjs'
|
|
1
|
+
import { createWorld } from './world-runtime.mjs';
|
|
2
2
|
// One explicit owner per delivery application; stale leases cannot pause a new view.
|
|
3
|
-
export function createWorldSurface(activityPresentation={}){
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
export function createWorldSurface(activityPresentation = {}) {
|
|
4
|
+
for (const value of Object.values(activityPresentation))
|
|
5
|
+
if (typeof value !== 'function')
|
|
6
|
+
throw Error('InvalidPresentationAdapter');
|
|
7
|
+
const trustedPresentation = Object.freeze({ ...activityPresentation });
|
|
8
|
+
let world = null, generation = 0, closed = false;
|
|
9
|
+
return { attach(host, options) {
|
|
10
|
+
if (closed)
|
|
11
|
+
throw Error('WorldSurfaceClosed');
|
|
12
|
+
options = { ...options, activityPresentation: trustedPresentation };
|
|
13
|
+
const lease = ++generation;
|
|
14
|
+
if (world)
|
|
15
|
+
world.mount(host, options);
|
|
16
|
+
else
|
|
17
|
+
world = createWorld(host, options);
|
|
18
|
+
return { world, release() { if (lease === generation)
|
|
19
|
+
world?.park(); } };
|
|
20
|
+
}, close() { if (closed)
|
|
21
|
+
return; closed = true; ++generation; const previous = world; world = null; previous?.close(); } };
|
|
14
22
|
}
|