@zixcel/dom-renderer 0.10.4 → 0.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +7 -1
  2. package/package.json +7 -5
  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
@@ -1,36 +1,2 @@
1
- export function createWorld(host: any, { objects, relations, pose: restored, onActivate, conversation, activity, target, pattern, context, activityPresentation }?: {
2
- objects?: any[];
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;
@@ -1,312 +1,7 @@
1
- import {layoutWorld,worldLimits,initialPose,movePose,lookPose,cameraMatrix,screenPoint,renderSize,clamp,pickWorld,fitPose,viewCamera} from './world-model.mjs'
2
- import {MOUSE,TOUCH} from 'three'
3
- import {OrbitControls} from 'three/addons/controls/OrbitControls.js'
4
- import {createGpu,worldGeometry} from './world-gpu.mjs'
5
- import {spatialPattern,spatialPatterns,spatialGroupings} from './spatial-patterns.mjs'
6
- import {objectForms,spacingPresets,objectBounds} from './object-presentation.mjs'
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,2 @@
1
+ import type { WorldContext } from './world-context.mjs';
2
+ export declare function setupWorldView(ctx: WorldContext): void;
@@ -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
+ }
@@ -1,7 +1,2 @@
1
- export function createWorldSurface(activityPresentation?: {}): {
2
- attach(host: any, options: any): {
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;
@@ -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
- for(const value of Object.values(activityPresentation))if(typeof value!=='function')throw Error('InvalidPresentationAdapter')
5
- const trustedPresentation=Object.freeze({...activityPresentation})
6
- let world=null,generation=0,closed=false
7
- return {attach(host,options){
8
- if(closed)throw Error('WorldSurfaceClosed')
9
- options={...options,activityPresentation:trustedPresentation}
10
- const lease=++generation
11
- if(world)world.mount(host,options);else world=createWorld(host,options)
12
- return {world,release(){if(lease===generation)world.park()}}
13
- },close(){if(closed)return;closed=true;++generation;world?.close();world=null}}
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
  }