Skip to content

Commit

Permalink
prototype handle tours snapshots merge
Browse files Browse the repository at this point in the history
  • Loading branch information
sdumetz committed Feb 7, 2024
1 parent a0499fc commit 021c655
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 41 deletions.
11 changes: 9 additions & 2 deletions source/server/utils/merge/merge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const thisDir = path.dirname(fileURLToPath(import.meta.url));

import { IDocument, INode } from "../schema/document.js";
import {DELETE_KEY, apply, applyDoc, diff, diffDoc} from "./index.js";
import { ISetup } from "../schema/setup.js";



Expand Down Expand Up @@ -119,7 +120,13 @@ describe("merge documents", function(){

const d = diffDoc(doc, next);
const result = applyDoc(current, d);
expect(result).to.deep.equal(next);
expect(result.setups).to.have.length(1);
const {snapshots} = (result.setups as ISetup[])[0];
expect(snapshots).to.have.property("targets").to.deep.equal([
"model/0/visible",
"node/0/position",
"node/0/scale"
]);
});

it("detects a no-op", function(){
Expand All @@ -129,6 +136,6 @@ describe("merge documents", function(){
expect(d, JSON.stringify(d)).to.deep.equal({});

const result = applyDoc(current, d);
expect(result).to.deep.equal(current);
expect(result, JSON.stringify(result)).to.deep.equal(current);
});
})
33 changes: 19 additions & 14 deletions source/server/utils/merge/pointers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { mapMeta } from "./meta.js";
import { mapModel } from "./model.js";
import { appendScene } from "./scene.js";
import { mapSetup } from "./setup.js";
import { DerefDocument, DerefMeta, DerefNode, DerefScene, DerefSetup } from "./types.js";
import { DerefDocument, DerefMeta, DerefNode, DerefScene, DerefSetup, SOURCE_INDEX } from "./types.js";



Expand Down Expand Up @@ -75,7 +75,6 @@ export function toPointers(src :IDocument) :DerefDocument {

// Dereference self-contained collections
const metas = src.metas?.map(mapMeta) ?? [];
const setups = src.setups?.map(mapSetup) ?? [];
const models = src.models?.map(mapModel)?? [];

// nodes and scenes mapping couldn't be made side-effect-free because they map to other collections
Expand All @@ -96,20 +95,22 @@ export function toPointers(src :IDocument) :DerefDocument {
node.meta = metas[iNode.meta];
if(!node.meta) throw new Error(`Invalid meta index ${iNode.meta} in node #${nodeIndex}`);
}

if(typeof iNode.model === "number"){
node.model = models[iNode.model];
if(!node.model) throw new Error(`Invalid model index ${iNode.model} in node #${nodeIndex}`);
}

if(typeof iNode.camera === "number"){
let ptr = src.cameras?.[iNode.camera];
if(!ptr) throw new Error(`Invalid camera index ${iNode.camera} in node #${nodeIndex}`);
node.camera = {...ptr, [SOURCE_INDEX]: iNode.camera};
}

for(let key of ["camera", "light"] as const){
const idx = iNode[key];
if(typeof idx == "number"){
const dict = src[key+"s" as `${typeof key}s`]
let ptr = dict? dict[idx]: undefined;
if(!ptr) throw new Error(`Invalid ${key} index ${idx} in node #${nodeIndex}`);
(node[key] as any) = ptr;
}
if(typeof iNode.light === "number"){
let ptr = src.lights?.[iNode.light];
if(!ptr) throw new Error(`Invalid light index ${iNode.light} in node #${nodeIndex}`);
node.light = {...ptr, [SOURCE_INDEX]: iNode.light};
}

//node.children will be assigned later, because children might not have been dereferenced yet
Expand Down Expand Up @@ -147,15 +148,19 @@ export function toPointers(src :IDocument) :DerefDocument {
return {...children, [node.id]: node};
}, {});
}
if(typeof iScene.setup == "number"){
if(!src.setups || !src.setups[iScene.setup]) throw new Error(`Invalid setup #${iScene.setup} in scene #${src.scene}`)
scene.setup = setups[iScene.setup];
}


if(typeof iScene.meta == "number"){
if(!src.metas || !src.metas[iScene.meta]) throw new Error(`Invalid meta #${iScene.meta} in scene #${src.scene}`)
scene.meta = metas[iScene.meta];
}

const setups = src.setups?.map(s=>mapSetup(s, nodes ??[])) ?? [];
if(typeof iScene.setup == "number"){
if(!src.setups || !src.setups[iScene.setup]) throw new Error(`Invalid setup #${iScene.setup} in scene #${src.scene}`)
scene.setup = setups[iScene.setup];
}

return {
asset: src.asset,
scene,
Expand Down
7 changes: 4 additions & 3 deletions source/server/utils/merge/pointers/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { IDocument } from "../../schema/document.js";

import { IModel } from "../../schema/model.js";

import { DerefModel, toIdMap, toUriMap } from "./types.js";
import { DerefModel, SOURCE_INDEX, toIdMap, toUriMap } from "./types.js";

export function appendModel(document :Required<IDocument>, {derivatives, annotations, ...model} :DerefModel) :number{
export function appendModel(document :Required<IDocument>, {derivatives, annotations, [SOURCE_INDEX]: src_index, ...model} :DerefModel) :number{
let iModel :IModel = {
...model,
derivatives: [],
Expand All @@ -25,11 +25,12 @@ export function appendModel(document :Required<IDocument>, {derivatives, annotat
}


export function mapModel({annotations, derivatives, ...iModel} :IModel) :DerefModel {
export function mapModel({annotations, derivatives, ...iModel} :IModel, index: number) :DerefModel {
const model = {
...iModel,
derivatives: {} as DerefModel["derivatives"],
annotations: annotations?toIdMap(annotations): undefined,
[SOURCE_INDEX]: index,
}
for(let derivative of derivatives){
const id = `${derivative.usage}/${derivative.quality}`;
Expand Down
8 changes: 5 additions & 3 deletions source/server/utils/merge/pointers/node.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IDocument, INode } from "../../schema/document.js";
import { appendMeta } from "./meta.js";
import { appendModel } from "./model.js";
import { DerefNode } from "./types.js";
import { DerefNode, SOURCE_INDEX } from "./types.js";


export function appendNode(document :Required<IDocument>, node :DerefNode){
Expand All @@ -27,10 +27,12 @@ export function appendNode(document :Required<IDocument>, node :DerefNode){
}

if(node.camera){
iNode["camera"] = document["cameras"].push(node.camera) -1;
const {[SOURCE_INDEX]:src_index, ...camera} = node.camera;
iNode["camera"] = document["cameras"].push(camera) -1;
}
if(node.light){
iNode["light"] = document["lights"].push(node.light) -1;
const {[SOURCE_INDEX]:src_index, ...light} = node.light;
iNode["light"] = document["lights"].push(light) -1;
}


Expand Down
3 changes: 2 additions & 1 deletion source/server/utils/merge/pointers/pointers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';

import { fromPointers, toPointers } from "./index.js";
import { IDocument } from '../../schema/document.js';
import { DerefNode } from './types.js';
import { DerefNode, SOURCE_INDEX } from './types.js';


const thisDir = path.dirname(fileURLToPath(import.meta.url));
Expand Down Expand Up @@ -90,6 +90,7 @@ describe("(de)reference pointers", function(){
id: "PeIZ72MDwAGH",
name: "Model",
model: {
[SOURCE_INDEX]: 0,
units: "cm",
derivatives:{
"High/Web3D":{quality: "High", usage: "Web3D", assets:{"model.gltf": {uri: "model.gltf", type:"Model"}}}
Expand Down
10 changes: 6 additions & 4 deletions source/server/utils/merge/pointers/scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ export function appendScene(document :Required<IDocument>, scene :DerefScene){
};
if(scene.name) iScene.name = scene.name;


if(scene.setup){
iScene.setup = appendSetup(document, scene.setup);
}


if(scene.meta){
iScene.meta = appendMeta(document, scene.meta);
}
const nodes = Object.values(scene.nodes ?? {});
if(nodes.length){
iScene.nodes = nodes.map((node)=>appendNode(document, node));
}

//Order is important: appendSetup relies on nodes being properly inserted.
if(scene.setup){
iScene.setup = appendSetup(document, scene.setup);
}

return document.scenes.push(iScene) - 1;
Expand Down
53 changes: 44 additions & 9 deletions source/server/utils/merge/pointers/setup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { IDocument } from "../../schema/document.js";
import { ISetup, ITour } from "../../schema/setup.js";
import uid from "../../uid.js";
import { DerefSetup, DerefSnapshots, IdMap } from "./types.js";
import { mapTarget, unmapTarget } from "./snapshot.js";
import { DerefNode, DerefSetup, DerefSnapshots, IdMap } from "./types.js";

export function appendSetup(document :Required<IDocument>, {tours: toursMap, snapshots, ...setup} :DerefSetup) :number{
let iSetup :ISetup = {...setup};
Expand All @@ -11,9 +12,17 @@ export function appendSetup(document :Required<IDocument>, {tours: toursMap, sna
iSetup.tours = tours;
}
if(snapshots){
const targetStrings = Object.keys(snapshots.targets)
.map(k=> Object.keys(snapshots.targets[k]).map(prop=>`${k}/${prop}`))
.flat()
const targets = targetStrings.map(t=>unmapTarget(t, document.nodes));
iSetup.snapshots = {
...snapshots,
states: Object.values(snapshots.states ?? {}),
targets,
states: Object.values(snapshots.states ?? {}).map(s=>({
...s,
values: targetStrings.map(t=>s.values[t])
})),
};
}

Expand All @@ -22,7 +31,7 @@ export function appendSetup(document :Required<IDocument>, {tours: toursMap, sna
}


export function mapSetup({tours, snapshots, ...iSetup} :ISetup) :DerefSetup {
export function mapSetup({tours, snapshots, ...iSetup} :ISetup, nodes :DerefNode[]) :DerefSetup {
const setup = {
...iSetup,
tours: (tours?.length ? {} : undefined) as IdMap<ITour & {id:string}>,
Expand All @@ -33,12 +42,38 @@ export function mapSetup({tours, snapshots, ...iSetup} :ISetup) :DerefSetup {
tour.id ??= uid();
setup.tours[tour.id] = tour as ITour & {id: string};
}
setup.snapshots = ((snapshots)? {
...snapshots,
states: {} as DerefSnapshots["states"],
} as DerefSnapshots : undefined as any);
for(let state of snapshots?.states??[]){
setup.snapshots.states[state.id] = state;
if(snapshots){
//Nest targets into objects to ease deep merge by node ID
const targetNames = snapshots.targets.map(t=>mapTarget(t, nodes))
const targets = targetNames.reduce((targets, t)=>{
const [root, id, ...propPath] = t.split("/");
targets[`${root}/${id}`] ??= {};
targets[`${root}/${id}`][propPath.join("/")] = true;
return targets;
}, {} as Record<string, Record<string, true>>);


setup.snapshots = {
...snapshots,
targets ,
states: {} as DerefSnapshots["states"],
} as DerefSnapshots;


for(let state of snapshots?.states??[]){
if(state.values.length != targetNames.length){
throw new Error(`Invalid snapshot states length ${state.values.length} != ${targets.length}`);
}

const values = {} as Record<string, any>;
for(let idx = 0; idx < state.values.length; idx++){
const key = targetNames[idx];
values[key] = state.values[idx];
}

setup.snapshots.states[state.id] = {...state, values};
}

}
return setup;
}
41 changes: 41 additions & 0 deletions source/server/utils/merge/pointers/snapshot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { mapTarget } from "./snapshot.js";
import { SOURCE_INDEX } from "./types.js";


describe("mapTarget()", function(){

it("Keeps identity for indices not expected to change", function(){
[
"scenes/0/setup/reader/enabled",
"scenes/0/setup/viewer/annotationsVisible",
"scenes/0/setup/reader/position",
].forEach((t)=>{
expect(mapTarget(t)).to.equal(t);
});
});

it("dereferences nodes", function(){
expect(mapTarget("node/0/position", [
{id: "foo", name: "node1"},
])).to.equal("node/foo/position");
});

it("dereferences models", function(){
expect(mapTarget("model/0/position", [
{id: "foo", name: "node1", model: {[SOURCE_INDEX]: 0} as any},
])).to.equal("model/foo/position");
});

it("derefences lights", function(){
expect(mapTarget("light/0/position", [
{id: "foo", name: "node1", light: {[SOURCE_INDEX]: 0} as any},
])).to.equal("light/foo/position");
});


it("throws an error if node is missing", function(){
expect(()=>mapTarget("light/1/position", [
{id: "foo", name: "node1", light: {[SOURCE_INDEX]: 0} as any},
])).to.throw('does not point to a valid node');
});
})
50 changes: 50 additions & 0 deletions source/server/utils/merge/pointers/snapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { INode } from "../../schema/document.js";
import { DerefNode, SOURCE_INDEX } from "./types.js";


export function mapTarget(target :string, nodes :DerefNode[] =[]){
const [root, indexString, ...propPath] = target.split("/");
const index = parseInt(indexString);
if(Number.isNaN(index)) return target;

if(root=='scenes'){
return target; //Scene index is not expected to change
}

let node :DerefNode | undefined;

if(root == "node"){
node = nodes[index];
}else if(root == "model"){
node = nodes.find(n=>n.model?.[SOURCE_INDEX] === index);
} if(root == "light"){
node = nodes.find(n=>n.light?.[SOURCE_INDEX] === index);
}

if(!node) throw new Error(`Invalid pathMap: ${target} does not point to a valid node`);
return `${root}/${node.id}/${propPath.join("/")}`;
}


export function unmapTarget(target :string, nodes :INode[]){
const [root, id, ...propPath] = target.split("/");

if(root=='scenes'){
return target; //Scene index is not expected to change
}

let index :number|undefined;
const nodeIndex = nodes.findIndex(n=>n.id === id);
if(nodeIndex === -1) throw new Error(`can't find node with id : ${id} (in ${target})`);

if(root == "node"){
index = nodeIndex;
}else if(root == "model"){
index = nodes[nodeIndex].model;
} if(root == "light"){
index = nodes[nodeIndex].model;
}

if(typeof index !== "number") throw new Error(`Invalid pathMap: ${target} does not point to a valid node`);
return `${root}/${index}/${propPath.join("/")}`;
}
Loading

0 comments on commit 021c655

Please sign in to comment.