import {
chunksFrom,
createScene,
EMPTY_META,
findConflicts,
packScene,
planCommands,
readPackedScene,
stampVersion,
unpackScene,
type CommandBatch,
type Conflict,
type ObjectVersion,
type PackedScene,
type Scene,
type SceneMeta,
} from "@klorad/api/world";
import { WHITE_TOWER } from "./thessaloniki";
/** A small pumping station: a building, two pumps that report pressure, and a mast on its own layer. */
export function createStation(): Scene {
const scene = createScene({ id: "station", name: "Pumping station", coordinateSystem: { origin: WHITE_TOWER } });
scene.add({ id: "house", name: "Pump house", position: { east: 0, north: 0 }, class: "building", layer: "buildings", geometry: { kind: "extrusion", footprint: [[-9, -5], [9, -5], [9, 5], [-9, 5]], height: 6 } });
for (const [id, east] of [["pump-1", -4], ["pump-2", 4]] as const) {
scene.add({ id, name: id === "pump-1" ? "Pump 1" : "Pump 2", position: { east, north: 0, up: 0 }, correspondence: "twin", binding: { source: "scada", entity: id }, class: "pump", classifications: { ifc: "IfcPump" }, relations: [{ kind: "inside", to: "house" }], properties: { setPoint: 4 }, geometry: { kind: "cylinder", radius: 1.2, height: 2 } });
scene.observe({ objectId: id, quantity: "pressure", value: 3.8, unit: "bar", observedAt: Date.now(), source: "scada" });
}
scene.add({ id: "mast", name: "Light mast", position: { east: 22, north: 12 }, layer: "lighting", geometry: { kind: "cylinder", radius: 0.2, height: 10 } });
return scene;
}
export const STATION_META: SceneMeta = { ...EMPTY_META, layers: [{ id: "buildings", name: "Buildings" }, { id: "lighting", name: "Lighting" }] };
export type CommitOutcome = { readonly status: "applied"; readonly revision: number } | { readonly status: "conflict"; readonly conflicts: readonly Conflict[] } | { readonly status: "invalid"; readonly reason: string };
/**
* One writer over a scene: a batch is applied when nothing it touches changed after the
* revision its author read, field by field. The store on the server keeps the same rules.
*/
export function createWriter(scene: Scene, startAt = 1) {
let revision = startAt;
const versions = new Map<string, ObjectVersion>(scene.objects().map((o) => [o.id, { exists: true, fields: { "*": startAt } }]));
return {
scene,
revision: () => revision,
commit(batch: CommandBatch): CommitOutcome {
const conflicts = findConflicts(batch, (id) => versions.get(id));
if (conflicts.length) return { status: "conflict", conflicts };
let plan;
try {
plan = planCommands(batch.commands, scene.get, scene.coordinateSystem);
} catch (error) {
return { status: "invalid", reason: (error as Error).message };
}
revision += 1;
batch.commands.forEach((command, i) => {
const { id, before, after } = plan[i]!;
versions.set(id, stampVersion(command, revision, versions.get(id)));
if (!after) scene.remove(id);
else if (!before) scene.add(after);
else if (command.type === "update") scene.update(id, command.patch);
});
return { status: "applied", revision };
},
pack: (): Promise<PackedScene> => packScene(scene, STATION_META, { revision, chunkSize: 20 }),
};
}
export type Writer = ReturnType<typeof createWriter>;
/** A restart: the package goes through JSON, is read back (any version Klorad wrote), and opens as a scene. */
export async function reload(packed: PackedScene): Promise<Writer> {
const read = await readPackedScene(JSON.parse(JSON.stringify(packed)));
const { scene, revision } = await unpackScene(read.manifest, chunksFrom(read.chunks));
return createWriter(scene, revision);
}