Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

159
web/protocol/scene-delta.ts Normal file
View 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,
};
}