@zixcel/projection-client 0.10.3 → 0.10.4
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/package.json +14 -6
- package/src/index.d.mts +39 -29
- package/src/index.mjs +121 -131
- package/src/render-plan.d.mts +5 -0
- package/src/render-plan.mjs +37 -0
- package/src/state.d.mts +9 -0
- package/src/state.mjs +16 -0
- package/src/types.d.mts +76 -0
- package/src/types.mjs +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zixcel/projection-client",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "Bounded renderer-independent projection adoption and presentation runtime",
|
|
@@ -19,14 +19,22 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@zixcel/projection-contracts": "0.10.
|
|
23
|
-
},
|
|
24
|
-
"scripts": {
|
|
25
|
-
"test": "node --test test/*.test.mjs"
|
|
22
|
+
"@zixcel/projection-contracts": "0.10.4"
|
|
26
23
|
},
|
|
27
24
|
"repository": {
|
|
28
25
|
"type": "git",
|
|
29
26
|
"url": "git+https://github.com/zixcel/data-presentation.git",
|
|
30
27
|
"directory": "packages/projection-client"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"data-presentation",
|
|
31
|
+
"projections",
|
|
32
|
+
"client",
|
|
33
|
+
"javascript"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node --test test/*.test.mts",
|
|
37
|
+
"build": "tsc -p tsconfig.build.json",
|
|
38
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit"
|
|
31
39
|
}
|
|
32
|
-
}
|
|
40
|
+
}
|
package/src/index.d.mts
CHANGED
|
@@ -1,30 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export type
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
import { renderPlan } from './render-plan.mjs';
|
|
2
|
+
export { renderPlan };
|
|
3
|
+
import { projectionIdentity } from './state.mjs';
|
|
4
|
+
export { projectionIdentity };
|
|
5
|
+
import type { SceneProjection } from '@zixcel/projection-contracts/client';
|
|
6
|
+
import type { PresentationState, PresentationChange, AdoptionContext } from './types.mjs';
|
|
7
|
+
export type * from './types.mjs';
|
|
8
|
+
export declare class ClientProjectionRuntime {
|
|
9
|
+
#private;
|
|
10
|
+
get projection(): SceneProjection | null;
|
|
11
|
+
get presentation(): PresentationState;
|
|
12
|
+
current(key: string): SceneProjection | null;
|
|
13
|
+
adopt(snapshot: unknown, context: AdoptionContext): SceneProjection;
|
|
14
|
+
input(value: unknown, context: {
|
|
15
|
+
expectedCurrent: string | null;
|
|
16
|
+
}): SceneProjection;
|
|
17
|
+
present(change: PresentationChange): PresentationState;
|
|
18
|
+
navigate(key: string): PresentationState;
|
|
19
|
+
back(): PresentationState;
|
|
20
|
+
forward(): PresentationState;
|
|
21
|
+
action(id: string): Readonly<{
|
|
22
|
+
projectionKey: string;
|
|
23
|
+
projectionRevision: string;
|
|
24
|
+
sceneId: string;
|
|
25
|
+
actionId: string;
|
|
26
|
+
}>;
|
|
27
|
+
intent(value: unknown): Readonly<{
|
|
28
|
+
projectionKey: string;
|
|
29
|
+
projectionRevision: string;
|
|
30
|
+
sceneId: string;
|
|
31
|
+
actionId: string;
|
|
32
|
+
ownerCommandDescriptor: import("@zixcel/projection-contracts/client").Action;
|
|
33
|
+
}>;
|
|
34
|
+
inspect(): {
|
|
35
|
+
keys: number;
|
|
36
|
+
snapshots: number;
|
|
37
|
+
closed: boolean;
|
|
38
|
+
};
|
|
39
|
+
close(): void;
|
|
30
40
|
}
|
package/src/index.mjs
CHANGED
|
@@ -1,138 +1,128 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const empty=()=>({activeScene:null,focus:null,selectedItems:[],expandedItems:[],activeRegion:null,
|
|
7
|
-
navigation:[],navigationIndex:-1,inspector:null,loading:false,viewMode:'spatial',camera:{x:0,y:0,z:1},timeCursor:null})
|
|
8
|
-
export const projectionIdentity=s=>frozen(copy({key:s.key,revision:s.revision,lineage:s.lineage}))
|
|
9
|
-
function sceneSnapshot(snapshot){validateSnapshot(snapshot);check(snapshot.kind==='Scene','SceneUnavailable');return frozen(copy(snapshot))}
|
|
10
|
-
function targets(snapshot){return new Set([...snapshot.data.items,...snapshot.data.unresolved].map(i=>i.id))}
|
|
11
|
-
function rebase(previous,snapshot){
|
|
12
|
-
const ids=targets(snapshot),regions=new Set(snapshot.data.regions.map(r=>r.id))
|
|
13
|
-
return frozen({...previous,activeScene:snapshot.key,focus:ids.has(previous.focus)?previous.focus:null,
|
|
14
|
-
selectedItems:previous.selectedItems.filter(id=>ids.has(id)),expandedItems:previous.expandedItems.filter(id=>ids.has(id)),
|
|
15
|
-
activeRegion:regions.has(previous.activeRegion)?previous.activeRegion:null,inspector:ids.has(previous.inspector)?previous.inspector:null,
|
|
16
|
-
timeCursor:snapshot.data.temporal?.items.some(item=>item.itemId===previous.timeCursor)?previous.timeCursor:null})
|
|
17
|
-
}
|
|
18
|
-
|
|
1
|
+
import { canonical, copy, frozen, shape, text } from '@zixcel/projection-contracts/client';
|
|
2
|
+
import { renderPlan } from './render-plan.mjs';
|
|
3
|
+
export { renderPlan };
|
|
4
|
+
import { check, empty, sceneSnapshot, targets, rebase, projectionIdentity } from './state.mjs';
|
|
5
|
+
export { projectionIdentity };
|
|
19
6
|
export class ClientProjectionRuntime {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
this.#open();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
7
|
+
#cache = new Map();
|
|
8
|
+
#presentation = frozen(empty());
|
|
9
|
+
#closed = false;
|
|
10
|
+
get projection() { return this.#cache.get(this.#presentation.activeScene ?? '')?.at(-1) ?? null; }
|
|
11
|
+
get presentation() { return this.#presentation; }
|
|
12
|
+
#open() { check(!this.#closed, 'ClientRuntimeClosed'); }
|
|
13
|
+
current(key) { this.#open(); return this.#cache.get(key)?.at(-1) ?? null; }
|
|
14
|
+
adopt(snapshot, context) {
|
|
15
|
+
this.#open();
|
|
16
|
+
shape(context, ['kind'], ['expected', 'expectedCurrent']);
|
|
17
|
+
const next = sceneSnapshot(snapshot), old = this.current(next.key);
|
|
18
|
+
if (context.kind === 'hydrate') {
|
|
19
|
+
check(canonical(projectionIdentity(next)) === canonical(context.expected), 'HydrationProjectionMismatch');
|
|
20
|
+
check(!old || old.revision === next.revision, 'HydrationProjectionMismatch');
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
check(context.kind === 'current' && Object.hasOwn(context, 'expectedCurrent'), 'ProjectionAdoptionError');
|
|
24
|
+
check((old?.revision ?? null) === context.expectedCurrent, 'StaleProjectionDelivery');
|
|
25
|
+
}
|
|
26
|
+
if (!old && this.#cache.size === 8) {
|
|
27
|
+
const evict = [...this.#cache.keys()].find(k => k !== this.#presentation.activeScene);
|
|
28
|
+
check(evict !== undefined, 'SceneUnavailable');
|
|
29
|
+
this.#cache.delete(evict);
|
|
30
|
+
}
|
|
31
|
+
const versions = this.#cache.get(next.key) ?? [];
|
|
32
|
+
const retained = versions.filter(v => v.revision !== next.revision).slice(-1);
|
|
33
|
+
this.#cache.delete(next.key);
|
|
34
|
+
this.#cache.set(next.key, [...retained, next]);
|
|
35
|
+
if (this.#presentation.activeScene === next.key)
|
|
36
|
+
this.#presentation = rebase(this.#presentation, next);
|
|
37
|
+
else if (this.#presentation.activeScene === null)
|
|
38
|
+
this.navigate(next.key);
|
|
39
|
+
return next;
|
|
34
40
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
input(value, context) {
|
|
42
|
+
this.#open();
|
|
43
|
+
shape(context, ['expectedCurrent']);
|
|
44
|
+
const input = value;
|
|
45
|
+
shape(value, ['kind', 'revision'], input?.kind === 'Snapshot' ? ['snapshot'] : []);
|
|
46
|
+
text(input.revision);
|
|
47
|
+
if (input.kind === 'Snapshot') {
|
|
48
|
+
const snapshot = input.snapshot;
|
|
49
|
+
check(snapshot?.revision === input.revision, 'ProjectionAdoptionError');
|
|
50
|
+
return this.adopt(input.snapshot, { kind: 'current', expectedCurrent: context.expectedCurrent });
|
|
51
|
+
}
|
|
52
|
+
check(input.kind === 'NoChange', 'ProjectionAdoptionError');
|
|
53
|
+
check(this.projection && this.projection.revision === input.revision && input.revision === context.expectedCurrent, 'StaleProjectionDelivery');
|
|
54
|
+
return this.projection;
|
|
38
55
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
present(change) {
|
|
57
|
+
this.#open();
|
|
58
|
+
check(this.projection, 'SceneUnavailable');
|
|
59
|
+
shape(change, [], ['focus', 'selectedItems', 'expandedItems', 'activeRegion', 'inspector', 'loading', 'viewMode', 'camera', 'timeCursor']);
|
|
60
|
+
const ids = targets(this.projection), regions = new Set(this.projection.data.regions.map(r => r.id));
|
|
61
|
+
for (const key of ['selectedItems', 'expandedItems'])
|
|
62
|
+
if (Object.hasOwn(change, key)) {
|
|
63
|
+
check(Array.isArray(change[key]) && change[key].length <= 256, 'PresentationLimitExceeded');
|
|
64
|
+
check(new Set(change[key]).size === change[key].length && change[key].every(id => ids.has(id)), 'InvalidPresentationTarget');
|
|
65
|
+
}
|
|
66
|
+
for (const key of ['focus', 'inspector'])
|
|
67
|
+
if (Object.hasOwn(change, key))
|
|
68
|
+
check(change[key] === null || ids.has(change[key] ?? ''), 'InvalidPresentationTarget');
|
|
69
|
+
if (Object.hasOwn(change, 'activeRegion'))
|
|
70
|
+
check(change.activeRegion === null || regions.has(change.activeRegion ?? ''), 'InvalidPresentationTarget');
|
|
71
|
+
if (Object.hasOwn(change, 'loading'))
|
|
72
|
+
check(typeof change.loading === 'boolean', 'InvalidPresentationTarget');
|
|
73
|
+
if (Object.hasOwn(change, 'viewMode'))
|
|
74
|
+
check(['spatial', 'structured'].includes(change.viewMode ?? ''), 'InvalidPresentationTarget');
|
|
75
|
+
if (Object.hasOwn(change, 'camera')) {
|
|
76
|
+
shape(change.camera, ['x', 'y', 'z']);
|
|
77
|
+
check(change.camera, 'InvalidPresentationTarget');
|
|
78
|
+
for (const key of ['x', 'y', 'z'])
|
|
79
|
+
check(Number.isFinite(change.camera[key]) && change.camera[key] >= (key === 'z' ? .65 : -1) && change.camera[key] <= (key === 'z' ? 1.5 : 1), 'InvalidPresentationTarget');
|
|
80
|
+
}
|
|
81
|
+
if (Object.hasOwn(change, 'timeCursor'))
|
|
82
|
+
check(change.timeCursor === null || this.projection.data.temporal?.items.some(item => item.itemId === change.timeCursor), 'InvalidPresentationTarget');
|
|
83
|
+
// Camera is bounded local presentation, not integer-only canonical source data.
|
|
84
|
+
const { camera, ...rest } = change;
|
|
85
|
+
this.#presentation = frozen({ ...this.#presentation, ...copy(rest), ...(camera ? { camera: { ...camera } } : {}) });
|
|
86
|
+
return this.#presentation;
|
|
51
87
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
check(Array.isArray(change[key])&&change[key].length<=256,'PresentationLimitExceeded')
|
|
62
|
-
check(new Set(change[key]).size===change[key].length&&change[key].every(id=>ids.has(id)),'InvalidPresentationTarget')
|
|
88
|
+
navigate(key) {
|
|
89
|
+
this.#open();
|
|
90
|
+
const next = this.current(key);
|
|
91
|
+
check(next, 'SceneUnavailable');
|
|
92
|
+
if (this.#presentation.activeScene === key)
|
|
93
|
+
return this.#presentation;
|
|
94
|
+
const navigation = [...this.#presentation.navigation.slice(0, this.#presentation.navigationIndex + 1), key].slice(-64);
|
|
95
|
+
this.#presentation = rebase({ ...empty(), navigation, navigationIndex: navigation.length - 1 }, next);
|
|
96
|
+
return this.#presentation;
|
|
63
97
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
98
|
+
#move(delta) {
|
|
99
|
+
this.#open();
|
|
100
|
+
const index = this.#presentation.navigationIndex + delta;
|
|
101
|
+
if (index < 0 || index >= this.#presentation.navigation.length)
|
|
102
|
+
return this.#presentation;
|
|
103
|
+
const next = this.current(this.#presentation.navigation[index]);
|
|
104
|
+
check(next, 'SceneUnavailable');
|
|
105
|
+
this.#presentation = rebase({ ...empty(), navigation: this.#presentation.navigation, navigationIndex: index }, next);
|
|
106
|
+
return this.#presentation;
|
|
71
107
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return this.#
|
|
91
|
-
|
|
92
|
-
back(){return this.#move(-1)}
|
|
93
|
-
forward(){return this.#move(1)}
|
|
94
|
-
action(id){
|
|
95
|
-
this.#open();check(this.projection?.data.actions.some(a=>a.id===id),'StaleSceneAction')
|
|
96
|
-
return frozen({projectionKey:this.projection.key,projectionRevision:this.projection.revision,sceneId:this.projection.key,actionId:id})
|
|
97
|
-
}
|
|
98
|
-
intent(handle){
|
|
99
|
-
this.#open();shape(handle,['projectionKey','projectionRevision','sceneId','actionId'])
|
|
100
|
-
const p=this.projection
|
|
101
|
-
check(p&&handle.projectionKey===p.key&&handle.sceneId===p.key&&handle.projectionRevision===p.revision,'StaleSceneAction')
|
|
102
|
-
const action=p.data.actions.find(a=>a.id===handle.actionId);check(action,'StaleSceneAction')
|
|
103
|
-
return frozen({...copy(handle),ownerCommandDescriptor:action})
|
|
104
|
-
}
|
|
105
|
-
inspect(){return {keys:this.#cache.size,snapshots:[...this.#cache.values()].reduce((n,v)=>n+v.length,0),closed:this.#closed}}
|
|
106
|
-
close(){this.#closed=true;this.#cache.clear();this.#presentation=frozen(empty())}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function renderPlan(projection,presentation,capabilities={roles}){
|
|
110
|
-
check(projection&&presentation.activeScene===projection.key,'RenderPlanError')
|
|
111
|
-
const values=new Map([...projection.data.items,...projection.data.unresolved].map(v=>[v.id,v]))
|
|
112
|
-
const selected=new Set(presentation.selectedItems),expanded=new Set(presentation.expandedItems)
|
|
113
|
-
const active=id=>selected.has(id)||presentation.focus===id,related=new Set()
|
|
114
|
-
const relations=projection.data.relations.map(relation=>{
|
|
115
|
-
const highlighted=active(relation.from)||active(relation.to)
|
|
116
|
-
if(highlighted){related.add(relation.from);related.add(relation.to)}
|
|
117
|
-
return {...relation,active:highlighted}
|
|
118
|
-
})
|
|
119
|
-
const item=content=>({id:content.id,content,focused:presentation.focus===content.id,
|
|
120
|
-
selected:selected.has(content.id),expanded:expanded.has(content.id),related:related.has(content.id),
|
|
121
|
-
display:projection.data.presentation?.find(binding=>binding.itemId===content.id)??null})
|
|
122
|
-
const regions=projection.data.regions.map(region=>{
|
|
123
|
-
check(capabilities.roles.includes(region.role),'UnsupportedRenderer')
|
|
124
|
-
return {...region,items:region.itemIds.map(id=>{
|
|
125
|
-
check(values.has(id),'RenderPlanError')
|
|
126
|
-
return item(values.get(id))
|
|
127
|
-
})}
|
|
128
|
-
})
|
|
129
|
-
return frozen({projectionKey:projection.key,projectionRevision:projection.revision,sceneId:projection.key,
|
|
130
|
-
purpose:projection.data.purpose,viewMode:presentation.viewMode,camera:presentation.camera,
|
|
131
|
-
temporal:projection.data.temporal?{...projection.data.temporal,cursor:presentation.timeCursor}:null,
|
|
132
|
-
regions,relations,actions:projection.data.actions.map(action=>({descriptor:action,
|
|
133
|
-
handle:{projectionKey:projection.key,projectionRevision:projection.revision,sceneId:projection.key,actionId:action.id}})),
|
|
134
|
-
inspector:presentation.inspector===null?null:values.get(presentation.inspector)??null,
|
|
135
|
-
unplaced:[...values.values()].filter(v=>!projection.data.regions.some(r=>r.itemIds.includes(v.id)))
|
|
136
|
-
.map(item),
|
|
137
|
-
empty:values.size===0,truncated:projection.data.truncated,omittedByPolicy:projection.data.omittedByPolicy})
|
|
108
|
+
back() { return this.#move(-1); }
|
|
109
|
+
forward() { return this.#move(1); }
|
|
110
|
+
action(id) {
|
|
111
|
+
this.#open();
|
|
112
|
+
const projection = this.projection;
|
|
113
|
+
check(projection && projection.data.actions.some(a => a.id === id), 'StaleSceneAction');
|
|
114
|
+
return frozen({ projectionKey: projection.key, projectionRevision: projection.revision, sceneId: projection.key, actionId: id });
|
|
115
|
+
}
|
|
116
|
+
intent(value) {
|
|
117
|
+
this.#open();
|
|
118
|
+
shape(value, ['projectionKey', 'projectionRevision', 'sceneId', 'actionId']);
|
|
119
|
+
const handle = value;
|
|
120
|
+
const p = this.projection;
|
|
121
|
+
check(p && handle.projectionKey === p.key && handle.sceneId === p.key && handle.projectionRevision === p.revision, 'StaleSceneAction');
|
|
122
|
+
const action = p.data.actions.find(a => a.id === handle.actionId);
|
|
123
|
+
check(action, 'StaleSceneAction');
|
|
124
|
+
return frozen({ projectionKey: p.key, projectionRevision: p.revision, sceneId: p.key, actionId: action.id, ownerCommandDescriptor: action });
|
|
125
|
+
}
|
|
126
|
+
inspect() { return { keys: this.#cache.size, snapshots: [...this.#cache.values()].reduce((n, v) => n + v.length, 0), closed: this.#closed }; }
|
|
127
|
+
close() { this.#closed = true; this.#cache.clear(); this.#presentation = frozen(empty()); }
|
|
138
128
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { SceneProjection } from '@zixcel/projection-contracts/client';
|
|
2
|
+
import type { PresentationState, RenderPlan } from './types.mjs';
|
|
3
|
+
export declare function renderPlan(projection: SceneProjection, presentation: PresentationState, capabilities?: {
|
|
4
|
+
roles: readonly string[];
|
|
5
|
+
}): RenderPlan;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { frozen, roles, ProjectionError } from '@zixcel/projection-contracts/client';
|
|
2
|
+
function check(value, code) { if (!value)
|
|
3
|
+
throw new ProjectionError(code); }
|
|
4
|
+
export function renderPlan(projection, presentation, capabilities = { roles }) {
|
|
5
|
+
check(projection && presentation.activeScene === projection.key, 'RenderPlanError');
|
|
6
|
+
const values = new Map([...projection.data.items, ...projection.data.unresolved].map(v => [v.id, v]));
|
|
7
|
+
const selected = new Set(presentation.selectedItems), expanded = new Set(presentation.expandedItems);
|
|
8
|
+
const active = (id) => selected.has(id) || presentation.focus === id, related = new Set();
|
|
9
|
+
const relations = projection.data.relations.map(relation => {
|
|
10
|
+
const highlighted = active(relation.from) || active(relation.to);
|
|
11
|
+
if (highlighted) {
|
|
12
|
+
related.add(relation.from);
|
|
13
|
+
related.add(relation.to);
|
|
14
|
+
}
|
|
15
|
+
return { ...relation, active: highlighted };
|
|
16
|
+
});
|
|
17
|
+
const item = (content) => ({ id: content.id, content, focused: presentation.focus === content.id,
|
|
18
|
+
selected: selected.has(content.id), expanded: expanded.has(content.id), related: related.has(content.id),
|
|
19
|
+
display: projection.data.presentation?.find(binding => binding.itemId === content.id) ?? null });
|
|
20
|
+
const regions = projection.data.regions.map(region => {
|
|
21
|
+
check(capabilities.roles.includes(region.role), 'UnsupportedRenderer');
|
|
22
|
+
return { ...region, items: region.itemIds.map(id => {
|
|
23
|
+
const content = values.get(id);
|
|
24
|
+
check(content, 'RenderPlanError');
|
|
25
|
+
return item(content);
|
|
26
|
+
}) };
|
|
27
|
+
});
|
|
28
|
+
return frozen({ projectionKey: projection.key, projectionRevision: projection.revision, sceneId: projection.key,
|
|
29
|
+
purpose: projection.data.purpose, viewMode: presentation.viewMode, camera: presentation.camera,
|
|
30
|
+
temporal: projection.data.temporal ? { ...projection.data.temporal, cursor: presentation.timeCursor } : null,
|
|
31
|
+
regions, relations, actions: projection.data.actions.map(action => ({ descriptor: action,
|
|
32
|
+
handle: { projectionKey: projection.key, projectionRevision: projection.revision, sceneId: projection.key, actionId: action.id } })),
|
|
33
|
+
inspector: presentation.inspector === null ? null : values.get(presentation.inspector) ?? null,
|
|
34
|
+
unplaced: [...values.values()].filter(v => !projection.data.regions.some(r => r.itemIds.includes(v.id)))
|
|
35
|
+
.map(item),
|
|
36
|
+
empty: values.size === 0, truncated: projection.data.truncated, omittedByPolicy: projection.data.omittedByPolicy });
|
|
37
|
+
}
|
package/src/state.d.mts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SceneProjection, ProjectionIdentity } from '@zixcel/projection-contracts/client';
|
|
2
|
+
import type { PresentationState } from './types.mjs';
|
|
3
|
+
export declare function fail(code: string): never;
|
|
4
|
+
export declare function check(value: unknown, code: string): asserts value;
|
|
5
|
+
export declare const empty: () => PresentationState;
|
|
6
|
+
export declare const projectionIdentity: (s: SceneProjection) => ProjectionIdentity;
|
|
7
|
+
export declare function sceneSnapshot(value: unknown): SceneProjection;
|
|
8
|
+
export declare function targets(snapshot: SceneProjection): Set<string>;
|
|
9
|
+
export declare function rebase(previous: PresentationState, snapshot: SceneProjection): PresentationState;
|
package/src/state.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { validateSnapshot, copy, frozen, ProjectionError } from '@zixcel/projection-contracts/client';
|
|
2
|
+
export function fail(code) { throw new ProjectionError(code); }
|
|
3
|
+
export function check(value, code) { if (!value)
|
|
4
|
+
fail(code); }
|
|
5
|
+
export const empty = () => ({ activeScene: null, focus: null, selectedItems: [], expandedItems: [], activeRegion: null,
|
|
6
|
+
navigation: [], navigationIndex: -1, inspector: null, loading: false, viewMode: 'spatial', camera: { x: 0, y: 0, z: 1 }, timeCursor: null });
|
|
7
|
+
export const projectionIdentity = (s) => frozen(copy({ key: s.key, revision: s.revision, lineage: s.lineage }));
|
|
8
|
+
export function sceneSnapshot(value) { const snapshot = validateSnapshot(value); check(snapshot.kind === 'Scene', 'SceneUnavailable'); return frozen(copy(snapshot)); }
|
|
9
|
+
export function targets(snapshot) { return new Set([...snapshot.data.items, ...snapshot.data.unresolved].map(i => i.id)); }
|
|
10
|
+
export function rebase(previous, snapshot) {
|
|
11
|
+
const ids = targets(snapshot), regions = new Set(snapshot.data.regions.map(r => r.id));
|
|
12
|
+
return frozen({ ...previous, activeScene: snapshot.key, focus: ids.has(previous.focus ?? '') ? previous.focus : null,
|
|
13
|
+
selectedItems: previous.selectedItems.filter(id => ids.has(id)), expandedItems: previous.expandedItems.filter(id => ids.has(id)),
|
|
14
|
+
activeRegion: regions.has(previous.activeRegion ?? '') ? previous.activeRegion : null, inspector: ids.has(previous.inspector ?? '') ? previous.inspector : null,
|
|
15
|
+
timeCursor: snapshot.data.temporal?.items.some(item => item.itemId === previous.timeCursor) ? previous.timeCursor : null });
|
|
16
|
+
}
|
package/src/types.d.mts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { SceneProjection, ProjectionIdentity, Action, Item, Unresolved, Relation, Region } from '@zixcel/projection-contracts/client';
|
|
2
|
+
export type { SceneProjection, ProjectionIdentity } from '@zixcel/projection-contracts/client';
|
|
3
|
+
export interface Camera {
|
|
4
|
+
readonly x: number;
|
|
5
|
+
readonly y: number;
|
|
6
|
+
readonly z: number;
|
|
7
|
+
}
|
|
8
|
+
export type DisplayBinding = NonNullable<SceneProjection['data']['presentation']>[number];
|
|
9
|
+
export type TemporalPlan = NonNullable<SceneProjection['data']['temporal']> & {
|
|
10
|
+
readonly cursor: string | null;
|
|
11
|
+
};
|
|
12
|
+
export type ViewMode = 'spatial' | 'structured';
|
|
13
|
+
export interface PresentationState {
|
|
14
|
+
readonly activeScene: string | null;
|
|
15
|
+
readonly focus: string | null;
|
|
16
|
+
readonly selectedItems: readonly string[];
|
|
17
|
+
readonly expandedItems: readonly string[];
|
|
18
|
+
readonly activeRegion: string | null;
|
|
19
|
+
readonly navigation: readonly string[];
|
|
20
|
+
readonly navigationIndex: number;
|
|
21
|
+
readonly inspector: string | null;
|
|
22
|
+
readonly loading: boolean;
|
|
23
|
+
readonly viewMode: ViewMode;
|
|
24
|
+
readonly camera: Camera;
|
|
25
|
+
readonly timeCursor: string | null;
|
|
26
|
+
}
|
|
27
|
+
export interface ActionHandle {
|
|
28
|
+
readonly projectionKey: string;
|
|
29
|
+
readonly projectionRevision: string;
|
|
30
|
+
readonly sceneId: string;
|
|
31
|
+
readonly actionId: string;
|
|
32
|
+
}
|
|
33
|
+
export interface SceneCommandIntent extends ActionHandle {
|
|
34
|
+
readonly ownerCommandDescriptor: Action;
|
|
35
|
+
}
|
|
36
|
+
export type AdoptionContext = {
|
|
37
|
+
kind: 'hydrate';
|
|
38
|
+
expected: ProjectionIdentity;
|
|
39
|
+
} | {
|
|
40
|
+
kind: 'current';
|
|
41
|
+
expectedCurrent: string | null;
|
|
42
|
+
};
|
|
43
|
+
export interface RenderItem {
|
|
44
|
+
readonly id: string;
|
|
45
|
+
readonly content: Item | Unresolved;
|
|
46
|
+
readonly focused: boolean;
|
|
47
|
+
readonly selected: boolean;
|
|
48
|
+
readonly expanded: boolean;
|
|
49
|
+
readonly related: boolean;
|
|
50
|
+
readonly display: DisplayBinding | null;
|
|
51
|
+
}
|
|
52
|
+
export interface RenderPlan {
|
|
53
|
+
readonly projectionKey: string;
|
|
54
|
+
readonly projectionRevision: string;
|
|
55
|
+
readonly sceneId: string;
|
|
56
|
+
readonly purpose: string;
|
|
57
|
+
readonly viewMode: ViewMode;
|
|
58
|
+
readonly camera: Camera;
|
|
59
|
+
readonly temporal: TemporalPlan | null;
|
|
60
|
+
readonly relations: readonly (Relation & {
|
|
61
|
+
readonly active: boolean;
|
|
62
|
+
})[];
|
|
63
|
+
readonly regions: readonly (Region & {
|
|
64
|
+
readonly items: readonly RenderItem[];
|
|
65
|
+
})[];
|
|
66
|
+
readonly actions: readonly {
|
|
67
|
+
descriptor: Action;
|
|
68
|
+
handle: ActionHandle;
|
|
69
|
+
}[];
|
|
70
|
+
readonly inspector: Item | Unresolved | null;
|
|
71
|
+
readonly unplaced: readonly RenderItem[];
|
|
72
|
+
readonly empty: boolean;
|
|
73
|
+
readonly truncated: boolean;
|
|
74
|
+
readonly omittedByPolicy: number;
|
|
75
|
+
}
|
|
76
|
+
export type PresentationChange = Partial<Pick<PresentationState, 'focus' | 'selectedItems' | 'expandedItems' | 'activeRegion' | 'inspector' | 'loading' | 'viewMode' | 'camera' | 'timeCursor'>>;
|
package/src/types.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|