@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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Drain each independently owned resource; preserve falsy failures too. */
|
|
2
|
+
export function disposeGraphics(operations) {
|
|
3
|
+
const failures = [];
|
|
4
|
+
for (const operation of operations) {
|
|
5
|
+
try {
|
|
6
|
+
operation();
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
failures.push(error);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
if (failures.length)
|
|
13
|
+
throw new AggregateError(failures, 'Graphics cleanup failed');
|
|
14
|
+
}
|
package/src/index.d.mts
CHANGED
|
@@ -1,57 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export function valueView(document: Document, value: unknown, budget?: {
|
|
13
|
-
nodes: number;
|
|
14
|
-
bytes: number;
|
|
15
|
-
}): HTMLElement;
|
|
16
|
-
/** @param {Document} document
|
|
17
|
-
* @param {import('@zixcel/interaction/input').InputDeclaration} declaration
|
|
18
|
-
* @param {(values:Record<string,string|boolean|number>)=>unknown|Promise<unknown>} onSubmit */
|
|
19
|
-
export function inputForm(document: Document, declaration: import("@zixcel/interaction/input").InputDeclaration, onSubmit: (values: Record<string, string | boolean | number>) => unknown | Promise<unknown>): {
|
|
20
|
-
form: HTMLFormElement;
|
|
21
|
-
close(): void;
|
|
22
|
-
};
|
|
23
|
-
export { createWorldSurface } from "./world-surface.mjs";
|
|
24
|
-
export { symbol } from "./symbol.mjs";
|
|
25
|
-
export { windowFrame } from "./frame.mjs";
|
|
26
|
-
export { worldControlGuide } from "./world-input.mjs";
|
|
27
|
-
export { spatialView } from "./spatial.mjs";
|
|
28
|
-
export { structureGraph } from "./structure-graph.mjs";
|
|
29
|
-
export const renderBounds: Readonly<{
|
|
30
|
-
nodes: 8192;
|
|
31
|
-
depth: 20;
|
|
32
|
-
textBytes: 524288;
|
|
33
|
-
}>;
|
|
34
|
-
export class SceneRenderer {
|
|
35
|
-
/** @param {HTMLElement} root
|
|
36
|
-
* @param {(kind:string,value:string|import('@zixcel/projection-client').ActionHandle|undefined)=>void} onEvent
|
|
37
|
-
* @param {{tools?:HTMLElement,details?:HTMLElement,actions?:HTMLElement}} [slots]
|
|
38
|
-
* @param {ReturnType<typeof createWorldSurface>|null} [surface] */
|
|
39
|
-
constructor(root: HTMLElement, onEvent: (kind: string, value: string | import("@zixcel/projection-client").ActionHandle | undefined) => void, slots?: {
|
|
40
|
-
tools?: HTMLElement;
|
|
41
|
-
details?: HTMLElement;
|
|
42
|
-
actions?: HTMLElement;
|
|
43
|
-
}, surface?: ReturnType<typeof createWorldSurface> | null);
|
|
44
|
-
/** @param {import('@zixcel/projection-client').RenderPlan} plan */
|
|
45
|
-
render(plan: import("@zixcel/projection-client").RenderPlan): void;
|
|
46
|
-
/** @param {import('@zixcel/projection-client').Camera} value */
|
|
47
|
-
camera(value: import("@zixcel/projection-client").Camera): void;
|
|
48
|
-
speaker(value: any): void;
|
|
49
|
-
garden(value: any): void;
|
|
50
|
-
activity(value: any): void;
|
|
51
|
-
/** @param {'in'|'out'|'up'|'down'|'left'|'right'|'reset'} action */
|
|
52
|
-
changeCamera(action: "in" | "out" | "up" | "down" | "left" | "right" | "reset"): void;
|
|
53
|
-
close(): void;
|
|
54
|
-
dispose(): void;
|
|
55
|
-
#private;
|
|
56
|
-
}
|
|
57
|
-
import { createWorldSurface } from './world-surface.mjs';
|
|
1
|
+
export { element, valueView, renderBounds } from './dom-elements.mjs';
|
|
2
|
+
export { inputForm } from './input-form.mjs';
|
|
3
|
+
export { SceneRenderer } from './scene-renderer.mjs';
|
|
4
|
+
export type { SceneActor, SceneGarden, SceneSlots, SceneEvents } from './scene-renderer.mjs';
|
|
5
|
+
export { createWorldSurface } from './world-surface.mjs';
|
|
6
|
+
export type { World, WorldSurface, WorldOptions, WorldSnapshot, ActivityPresentation } from './world-api.mjs';
|
|
7
|
+
export { symbol } from './symbol.mjs';
|
|
8
|
+
export { windowFrame } from './frame.mjs';
|
|
9
|
+
export { worldControlGuide } from './world-input.mjs';
|
|
10
|
+
export { spatialView } from './spatial.mjs';
|
|
11
|
+
export { structureGraph } from './structure-graph.mjs';
|
package/src/index.mjs
CHANGED
|
@@ -1,205 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export {
|
|
10
|
-
export {symbol} from './symbol.mjs'
|
|
11
|
-
export {windowFrame} from './frame.mjs'
|
|
12
|
-
export {worldControlGuide} from './world-input.mjs'
|
|
13
|
-
export {spatialView} from './spatial.mjs'
|
|
14
|
-
export {structureGraph} from './structure-graph.mjs'
|
|
15
|
-
export const renderBounds=Object.freeze({nodes:8192,depth:20,textBytes:524288})
|
|
16
|
-
const fail=code=>{throw Object.assign(Error(code),{code})}
|
|
17
|
-
/** @template {keyof HTMLElementTagNameMap} K
|
|
18
|
-
* @param {Document} document
|
|
19
|
-
* @param {K} tag
|
|
20
|
-
* @param {unknown} [text]
|
|
21
|
-
* @param {Record<string,unknown>} [attrs]
|
|
22
|
-
* @returns {HTMLElementTagNameMap[K]} */
|
|
23
|
-
export function element(document,tag,text=null,attrs={}){
|
|
24
|
-
const node=document.createElement(tag);if(text!==null)node.textContent=String(text)
|
|
25
|
-
for(const [key,value]of Object.entries(attrs))if(value!==null)node.setAttribute(key,String(value))
|
|
26
|
-
return node
|
|
27
|
-
}
|
|
28
|
-
/** @param {Document} document
|
|
29
|
-
* @param {unknown} value
|
|
30
|
-
* @param {{nodes:number,bytes:number}} [budget]
|
|
31
|
-
* @returns {HTMLElement} */
|
|
32
|
-
export function valueView(document,value,budget={nodes:0,bytes:0}){
|
|
33
|
-
const walk=(value,depth)=>{
|
|
34
|
-
if(++budget.nodes>renderBounds.nodes||depth>renderBounds.depth)fail('RenderLimitExceeded')
|
|
35
|
-
if(value===null||typeof value!=='object'){
|
|
36
|
-
const text=JSON.stringify(value);if(typeof text!=='string')fail('InvalidRenderValue')
|
|
37
|
-
budget.bytes+=new TextEncoder().encode(text).length;if(budget.bytes>renderBounds.textBytes)fail('RenderLimitExceeded')
|
|
38
|
-
return element(document,'span',text,{'data-projection-scalar':text})
|
|
39
|
-
}
|
|
40
|
-
if(Object.keys(value).length>4096)fail('RenderLimitExceeded')
|
|
41
|
-
if(!Object.keys(value).length)return element(document,'span',Array.isArray(value)?'[]':'{}',{'data-empty-value':Array.isArray(value)?'array':'object'})
|
|
42
|
-
const list=element(document,Array.isArray(value)?'ol':'dl')
|
|
43
|
-
for(const [key,child]of Object.entries(value)){
|
|
44
|
-
if(Array.isArray(value)){const item=element(document,'li');item.append(walk(child,depth+1));list.append(item)}
|
|
45
|
-
else{const name=element(document,'dt',key),item=element(document,'dd');item.append(walk(child,depth+1));list.append(name,item)}
|
|
46
|
-
}
|
|
47
|
-
return list
|
|
48
|
-
}
|
|
49
|
-
return walk(value,0)
|
|
50
|
-
}
|
|
51
|
-
export class SceneRenderer{
|
|
52
|
-
#root;#events;#abort=null;#actionHandles=new Map();#spatial=null;#slots;#pose=null;#poseKey=null;#speaker=null;#surface;#ownsSurface;#garden=null
|
|
53
|
-
/** @param {HTMLElement} root
|
|
54
|
-
* @param {(kind:string,value:string|import('@zixcel/projection-client').ActionHandle|undefined)=>void} onEvent
|
|
55
|
-
* @param {{tools?:HTMLElement,details?:HTMLElement,actions?:HTMLElement}} [slots]
|
|
56
|
-
* @param {ReturnType<typeof createWorldSurface>|null} [surface] */
|
|
57
|
-
constructor(root,onEvent,slots={},surface=null){this.#root=root;this.#events=onEvent;this.#slots=slots;this.#surface=surface??createWorldSurface();this.#ownsSurface=!surface}
|
|
58
|
-
/** @param {import('@zixcel/projection-client').RenderPlan} plan */
|
|
59
|
-
render(plan){
|
|
60
|
-
const root=this.#root,document=root.ownerDocument,focused=document.activeElement?.dataset.control
|
|
61
|
-
const previous=root.querySelector('[data-render-key]'),oldViewport=root.querySelector('.scene-viewport')
|
|
62
|
-
const scroll=previous?.dataset.renderKey===plan.projectionKey&&previous.dataset.viewMode===plan.viewMode&&oldViewport
|
|
63
|
-
?{left:oldViewport.scrollLeft,top:oldViewport.scrollTop}:null
|
|
64
|
-
if(this.#poseKey===plan.projectionKey)this.#pose=this.#spatial?.snapshot()??this.#pose
|
|
65
|
-
else this.#pose=null
|
|
66
|
-
this.#poseKey=plan.projectionKey
|
|
67
|
-
this.#spatial?.close();this.#spatial=null;this.#abort?.abort();this.#abort=new AbortController();this.#actionHandles.clear()
|
|
68
|
-
if(!['spatial','structured'].includes(plan.viewMode)||!Array.isArray(plan.relations)||plan.relations.length>512)fail('InvalidRenderPlan')
|
|
69
|
-
const section=element(document,'section',null,{'data-render-revision':plan.projectionRevision,'data-render-key':plan.projectionKey,'data-view-mode':plan.viewMode,'aria-label':'Scene content',tabindex:'-1',class:'semantic-scene'})
|
|
70
|
-
const button=(label,kind,id,attrs={})=>element(document,'button',label,{type:'button','data-kind':kind,'data-control':JSON.stringify([kind,id]),'data-target':id,...attrs})
|
|
71
|
-
const views=element(document,'div',null,{role:'group','aria-label':'Scene view',class:'scene-view-controls'})
|
|
72
|
-
for(const [mode,label]of [['spatial','Spatial view'],['structured','List view']])views.append(button(label,'view',mode,{'aria-pressed':plan.viewMode===mode}))
|
|
73
|
-
const tools=this.#slots.tools??section;tools.replaceChildren();tools.append(views)
|
|
74
|
-
const viewport=element(document,'div',null,{class:'scene-viewport',tabindex:'0','aria-label':'Explore information in this space'}),board=element(document,'div',null,{class:'spatial-board'})
|
|
75
|
-
for(const person of this.#garden?.actors??(this.#speaker?[this.#speaker]:[])){const actor=button('',this.#garden?'actor':'conversation',person.id,{'data-world-object':person.id,'data-selected':person.id===this.#speaker?.id,'data-observation-label':person.preparation?.label??''});actor.append(element(document,'strong',person.title));actor.disabled=person.status!==undefined&&person.status!=='Available';board.append(actor)}
|
|
76
|
-
const camera=element(document,'div',null,{role:'group','aria-label':'View position',class:'scene-camera'})
|
|
77
|
-
for(const [id,label,icon]of [['left','Move left','←'],['right','Move right','→'],['in','Zoom in','+'],['out','Zoom out','−'],['reset','View all','⛶']])camera.append(button(icon,'camera',id,{'aria-label':label,title:label}))
|
|
78
|
-
if(plan.viewMode==='spatial')tools.append(camera)
|
|
79
|
-
viewport.append(board);(plan.viewMode==='structured'?tools:section).append(viewport)
|
|
80
|
-
if(plan.empty)section.append(element(document,'p','No projected information is available.',{role:'status'}))
|
|
81
|
-
if(plan.truncated)section.append(element(document,'p','The source reports incomplete information.',{role:'status'}))
|
|
82
|
-
const groups=[...plan.regions,...(plan.unplaced.length?[{id:'unplaced',role:'Context',items:plan.unplaced}]:[])]
|
|
83
|
-
if(groups.length>17||plan.actions.length>64)fail('RenderLimitExceeded')
|
|
84
|
-
for(const group of groups){
|
|
85
|
-
const region=element(document,'section',null,{'data-region':group.id}),list=element(document,'ul',null,{class:'scene-items'})
|
|
86
|
-
region.append(element(document,'h2',group.role),list)
|
|
87
|
-
for(const item of group.items){
|
|
88
|
-
const row=element(document,'li',null,{'data-item-id':item.id,'data-selected':item.selected,'data-related':item.related,'data-focused':item.focused}),toolbar=element(document,'div',null,{class:'item-tools'})
|
|
89
|
-
const itemButton=(label,kind,attrs={})=>button(label,kind,item.id,{'data-control':JSON.stringify([group.id,kind,item.id]),...attrs})
|
|
90
|
-
toolbar.append(itemButton(item.display?.title??item.content.reason??'Display not configured','select',{'aria-pressed':item.selected}),itemButton('View details','inspect',{'aria-expanded':plan.inspector?.id===item.id,'aria-haspopup':'dialog'}))
|
|
91
|
-
if(item.content.reason)toolbar.append(itemButton('Review','resolve'))
|
|
92
|
-
const solid=element(document,'span',null,{class:'object-solid','aria-hidden':'true'})
|
|
93
|
-
solid.append(symbol(document,item.display?.symbol??'nodes'));row.append(solid,toolbar)
|
|
94
|
-
if(item.display?.summary)row.append(element(document,'p',item.display.summary,{'data-item-summary':item.id}))
|
|
95
|
-
const fields=element(document,'dl',null,{class:'object-fields'})
|
|
96
|
-
for(const field of item.display?.fields??[])fields.append(element(document,'dt',field.label),element(document,'dd',field.value===null?'Not set':String(field.value),{'data-projection-scalar':JSON.stringify(field.value)}))
|
|
97
|
-
row.append(fields)
|
|
98
|
-
if(!item.display)row.append(element(document,'p',item.content.reason?'This information is not yet resolved. Review the available choices or the missing requirements.':'Display information is not configured. Inspect this object in System.'))
|
|
99
|
-
list.append(row)
|
|
100
|
-
}
|
|
101
|
-
board.append(region)
|
|
102
|
-
}
|
|
103
|
-
const time=element(document,'section',null,{'aria-label':'Scene time',class:'scene-time'})
|
|
104
|
-
time.append(element(document,'h2',plan.temporal?.label??'Time'))
|
|
105
|
-
if(plan.temporal?.items.length){
|
|
106
|
-
time.append(button('All times','time','',{'aria-pressed':plan.temporal.cursor===null}))
|
|
107
|
-
for(const item of plan.temporal.items){const control=button(item.at+' · '+item.itemId,'time',item.itemId,{'aria-pressed':plan.temporal.cursor===item.itemId});time.append(control)}
|
|
108
|
-
}else time.append(element(document,'p','No event time has been declared by this source.',{role:'status'}))
|
|
109
|
-
tools.append(time)
|
|
110
|
-
if(plan.relations.length){
|
|
111
|
-
const relations=element(document,'details',null,{'aria-label':'Relationships'}),list=element(document,'ul',null,{class:'scene-relations'})
|
|
112
|
-
relations.append(element(document,'summary','Relationships ('+plan.relations.length+')'),list)
|
|
113
|
-
for(const relation of plan.relations){
|
|
114
|
-
const row=element(document,'li',null,{'data-relation-id':relation.id,'data-active':relation.active})
|
|
115
|
-
row.append(button(relation.from,'select',relation.from,{'data-control':JSON.stringify(['relation',relation.id,'from']),'aria-label':'Select related '+relation.from}),element(document,'span','→ '+relation.relationRef+' →'),button(relation.to,'select',relation.to,{'data-control':JSON.stringify(['relation',relation.id,'to']),'aria-label':'Select related '+relation.to}))
|
|
116
|
-
list.append(row)
|
|
117
|
-
}
|
|
118
|
-
tools.append(relations)
|
|
119
|
-
}
|
|
120
|
-
const actions=this.#slots.actions??tools;if(this.#slots.actions)actions.replaceChildren()
|
|
121
|
-
// Unresolved owner observations are conversation material too, including
|
|
122
|
-
// when no executable choice exists. This does not manufacture an action.
|
|
123
|
-
const pending=groups.flatMap(group=>group.items).filter(item=>item.content.reason)
|
|
124
|
-
if(pending.length){
|
|
125
|
-
const questions=element(document,'section',null,{'aria-label':'Information needing clarification'})
|
|
126
|
-
questions.append(element(document,'h3','Why this information needs review'))
|
|
127
|
-
for(const item of pending){
|
|
128
|
-
const row=element(document,'div',null,{'data-unresolved-ref':item.id})
|
|
129
|
-
const explanation=this.#slots.describeUnresolved?.(item.content.reason)??{label:String(item.content.reason),detail:'The source owner reported this unresolved reason.'}
|
|
130
|
-
row.append(button(item.display?.title??explanation.label,'resolve',item.id),element(document,'small',explanation.detail))
|
|
131
|
-
questions.append(row)
|
|
132
|
-
}
|
|
133
|
-
actions.append(questions)
|
|
134
|
-
}
|
|
135
|
-
for(const action of plan.actions){this.#actionHandles.set(action.handle.actionId,action.handle);actions.append(button(action.descriptor.label,'action',action.handle.actionId))}
|
|
136
|
-
const lastBook=(this.#slots.details??root).querySelector('[data-book-id]'),bookPosition=lastBook?{id:lastBook.dataset.bookId,revision:lastBook.dataset.bookRevision,page:Number(lastBook.dataset.bookPage)}:null
|
|
137
|
-
this.#slots.details?.replaceChildren()
|
|
138
|
-
if(plan.inspector){
|
|
139
|
-
const chosen=groups.flatMap(r=>r.items).find(i=>i.id===plan.inspector.id)
|
|
140
|
-
if(chosen)(this.#slots.details??section).append(bookReader(document,chosen,plan.projectionRevision,button('Close book','dismiss',plan.inspector.id),this.#abort.signal,bookPosition))
|
|
141
|
-
}
|
|
142
|
-
reconcileChildren(root,[section])
|
|
143
|
-
const activeViewport=plan.viewMode==='spatial'?root.querySelector('.scene-viewport'):viewport,activeBoard=activeViewport.querySelector('.spatial-board')
|
|
144
|
-
if(plan.viewMode==='spatial'||this.#garden)this.#spatial=spatialView(this.#garden?.host??activeViewport,activeBoard,plan.camera,plan.relations,this.#abort.signal,this.#pose,null,{
|
|
145
|
-
surface:this.#surface,activity:this.#garden?.activity,target:this.#garden?'garden':plan.projectionKey,context:this.#garden?plan.projectionKey:null,pattern:this.#garden?'garden':'constellation',items:new Map(groups.flatMap(region=>region.items.map(item=>[item.id,{
|
|
146
|
-
anchor:this.#speaker?.id,group:item.content.group,semanticRef:item.content.semanticRef,sourceRefs:item.content.sourceRefs,scalars:(item.display?.fields??[]).map(f=>f.value),unresolved:Boolean(item.content.reason),changeKey:JSON.stringify(item.display?.fields??[])
|
|
147
|
-
}])))
|
|
148
|
-
})
|
|
149
|
-
if(this.#garden&&plan.viewMode==='structured')activeBoard.hidden=false
|
|
150
|
-
for(const host of new Set([root,this.#slots.tools,this.#slots.details,this.#slots.actions].filter(Boolean)))host.addEventListener('click',event=>{
|
|
151
|
-
const target=event.target.closest('button[data-kind]');if(!target||!host.contains(target))return
|
|
152
|
-
if(target.dataset.kind==='camera'){this.#spatial?.change(target.dataset.target);return}
|
|
153
|
-
this.#events(target.dataset.kind,target.dataset.kind==='action'?this.#actionHandles.get(target.dataset.target):target.dataset.target)
|
|
154
|
-
},{signal:this.#abort.signal})
|
|
155
|
-
if(focused)[...root.querySelectorAll('[data-control]')].find(n=>n.dataset.control===focused)?.focus({preventScroll:true})
|
|
156
|
-
if(scroll){viewport.scrollLeft=scroll.left;viewport.scrollTop=scroll.top}
|
|
157
|
-
}
|
|
158
|
-
/** @param {import('@zixcel/projection-client').Camera} value */
|
|
159
|
-
camera(value){this.#spatial?.apply(value)}
|
|
160
|
-
speaker(value){this.#speaker=value}
|
|
161
|
-
garden(value){this.#garden=value}
|
|
162
|
-
activity(value){this.#spatial?.activity(value)}
|
|
163
|
-
/** @param {'in'|'out'|'up'|'down'|'left'|'right'|'reset'} action */
|
|
164
|
-
changeCamera(action){this.#spatial?.change(action)}
|
|
165
|
-
close(){this.#spatial?.close();this.#spatial=null;this.#abort?.abort();this.#actionHandles.clear();this.#root.replaceChildren();for(const slot of new Set([this.#slots.tools,this.#slots.details,this.#slots.actions].filter(Boolean)))slot.replaceChildren();if(this.#ownsSurface){this.#surface.close();this.#surface=createWorldSurface()}}
|
|
166
|
-
dispose(){this.close();if(this.#ownsSurface)this.#surface.close()}
|
|
167
|
-
}
|
|
168
|
-
/** @param {Document} document
|
|
169
|
-
* @param {import('@zixcel/interaction/input').InputDeclaration} declaration
|
|
170
|
-
* @param {(values:Record<string,string|boolean|number>)=>unknown|Promise<unknown>} onSubmit */
|
|
171
|
-
export function inputForm(document,declaration,onSubmit){
|
|
172
|
-
if(validateInputDeclaration(declaration).length)fail('InvalidInputDeclaration')
|
|
173
|
-
const form=element(document,'form'),fields=new Map(),abort=new AbortController(),prefix='input-'+crypto.randomUUID()
|
|
174
|
-
const fieldIds=[...new Set([...declaration.action.input_schema.required,...Object.keys(declaration.fields)])]
|
|
175
|
-
for(const id of fieldIds){
|
|
176
|
-
const field=declaration.fields[id]
|
|
177
|
-
const schema=declaration.action.input_schema.fields[id],label=element(document,'label'),name=element(document,'span',field.label)
|
|
178
|
-
let input
|
|
179
|
-
if(field.choices){input=element(document,'select');input.append(element(document,'option','Choose…',{value:''}));for(const choice of field.choices)input.append(element(document,'option',choice.label,{value:choice.value}))}
|
|
180
|
-
else if(schema.type==='string'&&!field.sensitive&&schema.max_length>255){input=element(document,'textarea');input.rows=4;input.maxLength=schema.max_length}
|
|
181
|
-
else{input=element(document,'input');input.type=schema.type==='boolean'?'checkbox':['integer','number'].includes(schema.type)?'number':field.sensitive?'password':'text'
|
|
182
|
-
if(schema.type==='string')input.maxLength=schema.max_length??4096
|
|
183
|
-
if(schema.type==='integer')input.step='1'
|
|
184
|
-
if(schema.type==='number')input.step='any'
|
|
185
|
-
if(schema.minimum!==undefined)input.min=String(schema.minimum)
|
|
186
|
-
if(schema.maximum!==undefined)input.max=String(schema.maximum)
|
|
187
|
-
}
|
|
188
|
-
input.id=prefix+'-'+fields.size;input.name=id;input.autocomplete='off';label.htmlFor=input.id;label.append(name);form.append(label,input);fields.set(id,{input,schema})
|
|
189
|
-
}
|
|
190
|
-
const submit=element(document,'button','Submit',{type:'submit'}),status=element(document,'div','',{role:'status'});form.append(submit,status)
|
|
191
|
-
form.addEventListener('submit',async event=>{
|
|
192
|
-
event.preventDefault();if(submit.disabled)return
|
|
193
|
-
const values={};for(const [id,{input,schema}]of fields){
|
|
194
|
-
// An absent optional value is not zero, null, false or an empty string.
|
|
195
|
-
if(input.value===''&&schema.type!=='boolean'&&!declaration.action.input_schema.required.includes(id))continue
|
|
196
|
-
values[id]=schema.type==='boolean'?input.checked:['number','integer'].includes(schema.type)?(input.value===''?null:Number(input.value)):input.value
|
|
197
|
-
}
|
|
198
|
-
if(validateInputValues(declaration,values).length){status.textContent='Check the declared input constraints.';return}
|
|
199
|
-
submit.disabled=true;form.setAttribute('aria-busy','true')
|
|
200
|
-
try{await onSubmit(values);if(!abort.signal.aborted)status.textContent='Submitted.'}
|
|
201
|
-
catch(error){if(!abort.signal.aborted){status.setAttribute('role','alert');status.replaceChildren(valueView(document,{code:error.code??'OperationUnavailable',...(error.ownerFailure?{ownerFailure:error.ownerFailure}:{})}))}}
|
|
202
|
-
finally{for(const {input}of fields.values())if(input.type==='password')input.value='';submit.disabled=false;form.setAttribute('aria-busy','false')}
|
|
203
|
-
},{signal:abort.signal})
|
|
204
|
-
return {form,close(){abort.abort();for(const {input}of fields.values())input.value='';form.remove()}}
|
|
205
|
-
}
|
|
1
|
+
export { element, valueView, renderBounds } from './dom-elements.mjs';
|
|
2
|
+
export { inputForm } from './input-form.mjs';
|
|
3
|
+
export { SceneRenderer } from './scene-renderer.mjs';
|
|
4
|
+
export { createWorldSurface } from './world-surface.mjs';
|
|
5
|
+
export { symbol } from './symbol.mjs';
|
|
6
|
+
export { windowFrame } from './frame.mjs';
|
|
7
|
+
export { worldControlGuide } from './world-input.mjs';
|
|
8
|
+
export { spatialView } from './spatial.mjs';
|
|
9
|
+
export { structureGraph } from './structure-graph.mjs';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { InputDeclaration } from '@zixcel/interaction/input';
|
|
2
|
+
export declare function inputForm(document: Document, declaration: InputDeclaration, onSubmit: (values: Record<string, string | boolean | number>) => unknown | Promise<unknown>): {
|
|
3
|
+
form: HTMLFormElement;
|
|
4
|
+
close(): void;
|
|
5
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { validateInputDeclaration, validateInputValues } from '@zixcel/interaction/input';
|
|
2
|
+
import { element, valueView, fail } from './dom-elements.mjs';
|
|
3
|
+
import { record } from './world-api.mjs';
|
|
4
|
+
function scalar(value) {
|
|
5
|
+
if (value.type === 'boolean')
|
|
6
|
+
return { type: 'boolean' };
|
|
7
|
+
if (value.type === 'string' || value.type === 'number' || value.type === 'integer')
|
|
8
|
+
return value;
|
|
9
|
+
return fail('InvalidInputDeclaration');
|
|
10
|
+
}
|
|
11
|
+
export function inputForm(document, declaration, onSubmit) {
|
|
12
|
+
if (validateInputDeclaration(declaration).length || declaration.action.input_schema.type !== 'object')
|
|
13
|
+
fail('InvalidInputDeclaration');
|
|
14
|
+
const contract = declaration.action.input_schema, required = contract.required ?? [];
|
|
15
|
+
const form = element(document, 'form'), fields = new Map(), abort = new AbortController(), prefix = 'input-' + crypto.randomUUID();
|
|
16
|
+
const fieldIds = [...new Set([...required, ...Object.keys(declaration.fields)])];
|
|
17
|
+
for (const id of fieldIds) {
|
|
18
|
+
const field = declaration.fields[id], schema = scalar(contract.fields[id]), label = element(document, 'label'), name = element(document, 'span', field.label);
|
|
19
|
+
let input;
|
|
20
|
+
if (field.choices) {
|
|
21
|
+
input = element(document, 'select');
|
|
22
|
+
input.append(element(document, 'option', 'Choose…', { value: '' }));
|
|
23
|
+
for (const choice of field.choices)
|
|
24
|
+
input.append(element(document, 'option', choice.label, { value: choice.value }));
|
|
25
|
+
}
|
|
26
|
+
else if (schema.type === 'string' && (schema.max_length ?? 0) > 255 && !field.sensitive) {
|
|
27
|
+
input = element(document, 'textarea');
|
|
28
|
+
input.rows = 4;
|
|
29
|
+
input.maxLength = schema.max_length ?? 4096;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
input = element(document, 'input');
|
|
33
|
+
input.type = schema.type === 'boolean' ? 'checkbox' : schema.type === 'integer' || schema.type === 'number' ? 'number' : field.sensitive ? 'password' : 'text';
|
|
34
|
+
if (schema.type === 'string')
|
|
35
|
+
input.maxLength = schema.max_length ?? 4096;
|
|
36
|
+
if (schema.type === 'integer')
|
|
37
|
+
input.step = '1';
|
|
38
|
+
if (schema.type === 'number')
|
|
39
|
+
input.step = 'any';
|
|
40
|
+
if (schema.type === 'integer' || schema.type === 'number') {
|
|
41
|
+
input.min = String(schema.minimum);
|
|
42
|
+
input.max = String(schema.maximum);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
input.id = prefix + '-' + fields.size;
|
|
46
|
+
input.name = id;
|
|
47
|
+
input.autocomplete = 'off';
|
|
48
|
+
label.htmlFor = input.id;
|
|
49
|
+
label.append(name);
|
|
50
|
+
form.append(label, input);
|
|
51
|
+
fields.set(id, { input, schema });
|
|
52
|
+
}
|
|
53
|
+
const submit = element(document, 'button', 'Submit', { type: 'submit' }), status = element(document, 'div', '', { role: 'status' });
|
|
54
|
+
form.append(submit, status);
|
|
55
|
+
form.addEventListener('submit', async (event) => {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
if (submit.disabled)
|
|
58
|
+
return;
|
|
59
|
+
const values = {};
|
|
60
|
+
for (const [id, { input, schema }] of fields) {
|
|
61
|
+
if (input.value === '' && schema.type !== 'boolean' && !required.includes(id))
|
|
62
|
+
continue;
|
|
63
|
+
if (schema.type === 'boolean')
|
|
64
|
+
values[id] = 'checked' in input ? input.checked : false;
|
|
65
|
+
else if (schema.type === 'integer' || schema.type === 'number') {
|
|
66
|
+
if (input.value === '') {
|
|
67
|
+
status.textContent = 'Check the declared input constraints.';
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
values[id] = Number(input.value);
|
|
71
|
+
}
|
|
72
|
+
else
|
|
73
|
+
values[id] = input.value;
|
|
74
|
+
}
|
|
75
|
+
if (validateInputValues(declaration, values).length) {
|
|
76
|
+
status.textContent = 'Check the declared input constraints.';
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
submit.disabled = true;
|
|
80
|
+
form.setAttribute('aria-busy', 'true');
|
|
81
|
+
try {
|
|
82
|
+
await onSubmit(values);
|
|
83
|
+
if (!abort.signal.aborted)
|
|
84
|
+
status.textContent = 'Submitted.';
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const value = record(error);
|
|
88
|
+
if (!abort.signal.aborted) {
|
|
89
|
+
status.setAttribute('role', 'alert');
|
|
90
|
+
status.replaceChildren(valueView(document, { code: value.code ?? 'OperationUnavailable', ...(value.ownerFailure ? { ownerFailure: value.ownerFailure } : {}) }));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
for (const { input } of fields.values())
|
|
95
|
+
if (input.type === 'password')
|
|
96
|
+
input.value = '';
|
|
97
|
+
submit.disabled = false;
|
|
98
|
+
form.setAttribute('aria-busy', 'false');
|
|
99
|
+
}
|
|
100
|
+
}, { signal: abort.signal });
|
|
101
|
+
return { form, close() { abort.abort(); for (const { input } of fields.values())
|
|
102
|
+
input.value = ''; form.remove(); } };
|
|
103
|
+
}
|
|
@@ -1 +1,27 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface InspectionOverview {
|
|
2
|
+
readonly title: string;
|
|
3
|
+
readonly note: string;
|
|
4
|
+
readonly modulesAvailable: boolean;
|
|
5
|
+
readonly modulesPartial?: boolean;
|
|
6
|
+
readonly groups: readonly {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly title: string;
|
|
9
|
+
readonly note: string;
|
|
10
|
+
readonly state: string;
|
|
11
|
+
readonly measures: readonly {
|
|
12
|
+
readonly unit: string;
|
|
13
|
+
readonly value: number | null;
|
|
14
|
+
readonly partial?: boolean;
|
|
15
|
+
readonly documentId?: string;
|
|
16
|
+
}[];
|
|
17
|
+
readonly values?: readonly string[];
|
|
18
|
+
}[];
|
|
19
|
+
readonly modules: readonly {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly version: string;
|
|
22
|
+
readonly terms: number;
|
|
23
|
+
readonly expressions: number;
|
|
24
|
+
readonly dependencies: readonly string[];
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
export declare function inspectionOverview(document: Document, host: HTMLElement, view: InspectionOverview | null, onSource: (id: string) => void, signal: AbortSignal): void;
|
|
@@ -1,35 +1,52 @@
|
|
|
1
1
|
// Owner-projected facts only. This renderer does not classify semantic modules.
|
|
2
|
-
import {element} from './
|
|
3
|
-
export function inspectionOverview(document,host,view,onSource,signal){
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
2
|
+
import { element } from './dom-elements.mjs';
|
|
3
|
+
export function inspectionOverview(document, host, view, onSource, signal) {
|
|
4
|
+
host.replaceChildren();
|
|
5
|
+
if (!view) {
|
|
6
|
+
host.append(element(document, 'p', 'Responsibility overview is unavailable.'));
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
host.append(element(document, 'h2', view.title), element(document, 'p', view.note, { class: 'inspection-note' }));
|
|
10
|
+
const groups = element(document, 'ul', null, { class: 'scene-items' });
|
|
11
|
+
groups.style.gridTemplateColumns = 'repeat(auto-fit,minmax(min(250px,100%),1fr))';
|
|
12
|
+
for (const group of view.groups) {
|
|
13
|
+
const section = element(document, 'li', null, { 'aria-label': group.title, 'data-responsibility': group.id });
|
|
14
|
+
const info = element(document, 'details'), description = element(document, 'summary', 'About these observations');
|
|
15
|
+
info.append(description, element(document, 'p', group.note));
|
|
16
|
+
section.append(element(document, 'h3', group.title), element(document, 'p', group.state, { class: 'inspection-state' }));
|
|
17
|
+
for (const measure of group.measures) {
|
|
18
|
+
const row = element(document, 'div', null, { class: 'item-tools', 'data-measure': measure.unit });
|
|
19
|
+
row.append(element(document, 'strong', measure.value === null ? 'Unavailable' : String(measure.value) + (measure.partial ? ' +' : '')), element(document, 'span', measure.unit));
|
|
20
|
+
if (measure.documentId) {
|
|
21
|
+
const documentId = measure.documentId;
|
|
22
|
+
const button = element(document, 'button', 'Inspect', { type: 'button', 'aria-label': 'Inspect ' + measure.unit });
|
|
23
|
+
button.addEventListener('click', () => onSource(documentId), { signal });
|
|
24
|
+
row.append(button);
|
|
25
|
+
}
|
|
26
|
+
section.append(row);
|
|
27
|
+
}
|
|
28
|
+
if (group.values?.length)
|
|
29
|
+
section.append(element(document, 'p', group.values.join(' · ')));
|
|
30
|
+
section.append(info);
|
|
31
|
+
groups.append(section);
|
|
32
|
+
}
|
|
33
|
+
host.append(groups, element(document, 'h3', 'Adopted definition modules'));
|
|
34
|
+
if (!view.modulesAvailable) {
|
|
35
|
+
host.append(element(document, 'p', 'Module data unavailable.'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!view.modules.length) {
|
|
39
|
+
host.append(element(document, 'p', 'No definition modules in the observed role.'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const list = element(document, 'ul', null, { class: 'inspection-modules' });
|
|
43
|
+
for (const module of view.modules) {
|
|
44
|
+
const item = element(document, 'li'), button = element(document, 'button', module.id, { type: 'button' });
|
|
45
|
+
button.addEventListener('click', () => onSource('definitions'), { signal });
|
|
46
|
+
item.append(button, element(document, 'p', `${module.version} · ${module.terms} terms · ${module.expressions} expressions`), element(document, 'p', module.dependencies.length ? 'Imports: ' + module.dependencies.join(', ') : 'No declared imports'));
|
|
47
|
+
list.append(item);
|
|
48
|
+
}
|
|
49
|
+
host.append(list);
|
|
50
|
+
if (view.modulesPartial)
|
|
51
|
+
host.append(element(document, 'p', 'Module inventory is incomplete.'));
|
|
35
52
|
}
|
package/src/npc-motion.d.mts
CHANGED
package/src/npc-motion.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Ambient presentation only. This never changes semantic position or Work.
|
|
2
|
-
export function npcMotion(id,milliseconds,state='Idle'){
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
export function npcMotion(id, milliseconds, state = 'Idle') {
|
|
3
|
+
const seed = [...id].reduce((n, c) => (Math.imul(n, 31) + c.charCodeAt(0)) | 0, 0) >>> 0;
|
|
4
|
+
const active = ['Runnable', 'Running', 'AwaitingResult'].includes(state);
|
|
5
|
+
const phase = milliseconds / 1000 * (active ? 2.2 : 1.1) * (0.85 + (seed % 7) / 20) + (seed % 97) * .31;
|
|
6
|
+
return { x: Math.sin(phase) * (active ? .2 : .08), y: Math.max(0, Math.sin(phase * 1.7)) * (active ? .12 : .04), swing: Math.sin(phase * 1.4) * (active ? .16 : .08) };
|
|
7
7
|
}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
export type ObjectForm = 'person' | 'organization' | 'system' | 'shelf' | 'book';
|
|
2
|
+
export type ObjectTint = 'dark' | 'accent' | 'light' | 'face' | 'warning';
|
|
3
|
+
export type ObjectPart = readonly [number, number, number, number, number, number, ObjectTint];
|
|
4
|
+
export interface PresentationInput {
|
|
5
|
+
id?: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
form?: string;
|
|
8
|
+
kind?: string;
|
|
9
|
+
scalars?: readonly unknown[];
|
|
10
|
+
unresolved?: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface ObjectPresentation {
|
|
13
|
+
form: ObjectForm;
|
|
14
|
+
dataType?: 'text' | 'number' | 'boolean' | 'record';
|
|
15
|
+
needsReview?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare const spacingPresets: Readonly<{
|
|
11
18
|
compact: {
|
|
12
19
|
person: number;
|
|
13
20
|
group: number;
|
|
@@ -24,14 +31,14 @@ export const spacingPresets: Readonly<{
|
|
|
24
31
|
member: number;
|
|
25
32
|
};
|
|
26
33
|
}>;
|
|
27
|
-
export const objectForms: Readonly<{
|
|
34
|
+
export declare const objectForms: Readonly<{
|
|
28
35
|
person: "Person";
|
|
29
36
|
organization: "Organization";
|
|
30
37
|
system: "System";
|
|
31
38
|
shelf: "Bookshelf";
|
|
32
39
|
book: "Book";
|
|
33
40
|
}>;
|
|
34
|
-
export const libraryLayout: Readonly<{
|
|
41
|
+
export declare const libraryLayout: Readonly<{
|
|
35
42
|
columns: 6;
|
|
36
43
|
rows: 4;
|
|
37
44
|
slotWidth: 0.46;
|
|
@@ -39,17 +46,6 @@ export const libraryLayout: Readonly<{
|
|
|
39
46
|
baseY: 0.32;
|
|
40
47
|
frontZ: 0.32;
|
|
41
48
|
}>;
|
|
42
|
-
export const objectBounds: Readonly<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
system: number[];
|
|
46
|
-
shelf: number[];
|
|
47
|
-
book: number[];
|
|
48
|
-
}>;
|
|
49
|
-
export const objectTemplates: Readonly<{
|
|
50
|
-
person: (string | number)[][];
|
|
51
|
-
organization: (string | number)[][];
|
|
52
|
-
system: (string | number)[][];
|
|
53
|
-
shelf: (string | number)[][];
|
|
54
|
-
book: (string | number)[][];
|
|
55
|
-
}>;
|
|
49
|
+
export declare const objectBounds: Readonly<Record<ObjectForm, readonly [number, number, number]>>;
|
|
50
|
+
export declare const objectTemplates: Readonly<Record<ObjectForm, readonly ObjectPart[]>>;
|
|
51
|
+
export declare function objectPresentation(item: PresentationInput): ObjectPresentation;
|