Add Chromium-only Blender WebEngine parity work
This commit is contained in:
159
web/protocol/scene-delta.ts
Normal file
159
web/protocol/scene-delta.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import type { AnimationIR, CameraIR, LightIR, MaterialIR, MeshSummaryIR, SceneNodeIR, SceneSnapshotIR } from "./scene-ir";
|
||||
|
||||
export interface SceneCollectionDelta<T extends { id: string }> {
|
||||
updated: Array<Pick<T, "id"> & Partial<T>>;
|
||||
added?: T[];
|
||||
removed?: string[];
|
||||
}
|
||||
|
||||
export interface SceneDelta {
|
||||
schemaVersion: 1;
|
||||
baseRevision: number;
|
||||
nextRevision: number;
|
||||
nodes?: {
|
||||
updated: Array<Pick<SceneNodeIR, "id"> & Partial<Pick<SceneNodeIR, "visible" | "parentId" | "localMatrix" | "transform">>>;
|
||||
added?: SceneNodeIR[];
|
||||
removed?: string[];
|
||||
};
|
||||
meshes?: SceneCollectionDelta<MeshSummaryIR>;
|
||||
materials?: SceneCollectionDelta<MaterialIR>;
|
||||
animations?: SceneCollectionDelta<AnimationIR>;
|
||||
cameras?: SceneCollectionDelta<CameraIR>;
|
||||
lights?: SceneCollectionDelta<LightIR>;
|
||||
activeObjectId?: string | null;
|
||||
frame?: SceneSnapshotIR["frame"];
|
||||
}
|
||||
|
||||
const collectionFields = ["meshes", "materials", "animations", "cameras", "lights"] as const;
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function parseSceneDelta(value: unknown): SceneDelta {
|
||||
if (!isRecord(value) || value.schemaVersion !== 1) throw new Error("Unsupported SceneDelta schema");
|
||||
if (typeof value.baseRevision !== "number" || !Number.isInteger(value.baseRevision) ||
|
||||
typeof value.nextRevision !== "number" || !Number.isInteger(value.nextRevision)) {
|
||||
throw new Error("SceneDelta revisions must be integers");
|
||||
}
|
||||
if (value.nodes !== undefined) {
|
||||
if (!isRecord(value.nodes) || !Array.isArray(value.nodes.updated)) throw new Error("SceneDelta.nodes is invalid");
|
||||
const added = value.nodes.added ?? [];
|
||||
const removed = value.nodes.removed ?? [];
|
||||
if (!Array.isArray(added) || !Array.isArray(removed) || removed.some((id) => typeof id !== "string")) {
|
||||
throw new Error("SceneDelta node additions/removals are invalid");
|
||||
}
|
||||
for (const change of value.nodes.updated) {
|
||||
if (!isRecord(change) || typeof change.id !== "string") throw new Error("SceneDelta updated node is invalid");
|
||||
}
|
||||
for (const node of added) {
|
||||
if (!isRecord(node) || typeof node.id !== "string") throw new Error("SceneDelta added node is invalid");
|
||||
}
|
||||
}
|
||||
for (const field of collectionFields) {
|
||||
const collection = value[field];
|
||||
if (collection === undefined) continue;
|
||||
if (!isRecord(collection) || !Array.isArray(collection.updated) ||
|
||||
(collection.added !== undefined && !Array.isArray(collection.added)) ||
|
||||
(collection.removed !== undefined && (!Array.isArray(collection.removed) || collection.removed.some((id) => typeof id !== "string")))) {
|
||||
throw new Error(`SceneDelta.${field} is invalid`);
|
||||
}
|
||||
}
|
||||
if (value.activeObjectId !== undefined && value.activeObjectId !== null && typeof value.activeObjectId !== "string") {
|
||||
throw new Error("SceneDelta.activeObjectId is invalid");
|
||||
}
|
||||
if (value.frame !== undefined) {
|
||||
if (!isRecord(value.frame) || typeof value.frame.current !== "number" || !Number.isFinite(value.frame.current) ||
|
||||
typeof value.frame.start !== "number" || !Number.isFinite(value.frame.start) ||
|
||||
typeof value.frame.end !== "number" || !Number.isFinite(value.frame.end)) {
|
||||
throw new Error("SceneDelta.frame is invalid");
|
||||
}
|
||||
}
|
||||
return value as unknown as SceneDelta;
|
||||
}
|
||||
|
||||
function diffCollection<T extends { id: string }>(before: readonly T[], after: readonly T[]): SceneCollectionDelta<T> | undefined {
|
||||
const oldItems = new Map(before.map((item) => [item.id, item]));
|
||||
const newItems = new Map(after.map((item) => [item.id, item]));
|
||||
const updated: Array<Pick<T, "id"> & Partial<T>> = [];
|
||||
const added: T[] = [];
|
||||
const removed: string[] = [];
|
||||
for (const item of after) {
|
||||
const previous = oldItems.get(item.id);
|
||||
if (!previous) added.push(item);
|
||||
else if (JSON.stringify(previous) !== JSON.stringify(item)) updated.push(item);
|
||||
}
|
||||
for (const item of before) if (!newItems.has(item.id)) removed.push(item.id);
|
||||
return updated.length || added.length || removed.length ? { updated, added, removed } : undefined;
|
||||
}
|
||||
|
||||
function changedNodeFields(before: SceneNodeIR, after: SceneNodeIR): { id: string } & Partial<SceneNodeIR> {
|
||||
const change: { id: string } & Partial<SceneNodeIR> = { id: after.id };
|
||||
if (before.visible !== after.visible) change.visible = after.visible;
|
||||
if (before.parentId !== after.parentId) change.parentId = after.parentId;
|
||||
if (JSON.stringify(before.localMatrix) !== JSON.stringify(after.localMatrix)) change.localMatrix = after.localMatrix;
|
||||
if (JSON.stringify(before.transform) !== JSON.stringify(after.transform)) change.transform = after.transform;
|
||||
return change;
|
||||
}
|
||||
|
||||
export function diffSceneSnapshots(before: SceneSnapshotIR, after: SceneSnapshotIR): SceneDelta {
|
||||
const oldNodes = new Map(before.nodes.map((node) => [node.id, node]));
|
||||
const newNodes = new Map(after.nodes.map((node) => [node.id, node]));
|
||||
const updated: Array<{ id: string } & Partial<SceneNodeIR>> = [];
|
||||
const added: SceneNodeIR[] = [];
|
||||
const removed: string[] = [];
|
||||
for (const node of after.nodes) {
|
||||
const previous = oldNodes.get(node.id);
|
||||
if (!previous) added.push(node);
|
||||
else if (JSON.stringify(previous) !== JSON.stringify(node)) updated.push(changedNodeFields(previous, node));
|
||||
}
|
||||
for (const node of before.nodes) if (!newNodes.has(node.id)) removed.push(node.id);
|
||||
const delta: SceneDelta = { schemaVersion: 1, baseRevision: before.revision, nextRevision: after.revision };
|
||||
if (updated.length || added.length || removed.length) delta.nodes = { updated, added, removed };
|
||||
delta.meshes = diffCollection(before.meshes, after.meshes);
|
||||
delta.materials = diffCollection(before.materials, after.materials);
|
||||
delta.animations = diffCollection(before.animations, after.animations);
|
||||
delta.cameras = diffCollection(before.cameras, after.cameras);
|
||||
delta.lights = diffCollection(before.lights, after.lights);
|
||||
if (before.activeObjectId !== after.activeObjectId) delta.activeObjectId = after.activeObjectId;
|
||||
if (JSON.stringify(before.frame) !== JSON.stringify(after.frame)) delta.frame = after.frame;
|
||||
return delta;
|
||||
}
|
||||
|
||||
function applyCollectionDelta<T extends { id: string }>(items: readonly T[], delta: SceneCollectionDelta<T> | undefined): T[] {
|
||||
if (!delta) return [...items];
|
||||
const result = new Map(items.map((item) => [item.id, item]));
|
||||
for (const id of delta.removed ?? []) result.delete(id);
|
||||
for (const item of delta.added ?? []) result.set(item.id, item);
|
||||
for (const change of delta.updated) {
|
||||
const current = result.get(change.id);
|
||||
if (!current) throw new Error(`SceneDelta item not found: ${change.id}`);
|
||||
result.set(change.id, { ...current, ...change });
|
||||
}
|
||||
return [...result.values()].sort((left, right) => left.id.localeCompare(right.id));
|
||||
}
|
||||
|
||||
export function applySceneDelta(snapshot: SceneSnapshotIR, delta: SceneDelta): SceneSnapshotIR {
|
||||
parseSceneDelta(delta);
|
||||
if (snapshot.revision !== delta.baseRevision) throw new Error(`SceneDelta revision conflict: ${snapshot.revision} != ${delta.baseRevision}`);
|
||||
const updated = new Map(snapshot.nodes.map((node) => [node.id, node]));
|
||||
for (const id of delta.nodes?.removed ?? []) updated.delete(id);
|
||||
for (const node of delta.nodes?.added ?? []) updated.set(node.id, node);
|
||||
for (const change of delta.nodes?.updated ?? []) {
|
||||
const current = updated.get(change.id);
|
||||
if (!current) throw new Error(`SceneDelta node not found: ${change.id}`);
|
||||
updated.set(change.id, { ...current, ...change });
|
||||
}
|
||||
return {
|
||||
...snapshot,
|
||||
revision: delta.nextRevision,
|
||||
nodes: [...updated.values()].sort((left, right) => left.id.localeCompare(right.id)),
|
||||
meshes: applyCollectionDelta(snapshot.meshes, delta.meshes),
|
||||
materials: applyCollectionDelta(snapshot.materials, delta.materials),
|
||||
animations: applyCollectionDelta(snapshot.animations, delta.animations),
|
||||
cameras: applyCollectionDelta(snapshot.cameras, delta.cameras),
|
||||
lights: applyCollectionDelta(snapshot.lights, delta.lights),
|
||||
activeObjectId: delta.activeObjectId === undefined ? snapshot.activeObjectId : delta.activeObjectId,
|
||||
frame: delta.frame ?? snapshot.frame,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user