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

160
web/protocol/nla.ts Normal file
View File

@@ -0,0 +1,160 @@
import type { ErrorCode } from "./error";
import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } from "./capability-gates";
export const NLA_PROTOCOL_SCHEMA = 1 as const;
export type NlaBlendMode = "REPLACE" | "ADD" | "MULTIPLY" | "COMBINE";
export type NlaExtrapolation = "NOTHING" | "HOLD" | "HOLD_FORWARD";
export interface NlaStripIR {
id: string;
actionId: string;
frameStart: number;
frameEnd: number;
actionFrameStart: number;
actionFrameEnd: number;
scale: number;
repeat: number;
blendIn: number;
blendOut: number;
influence: number;
blendMode: NlaBlendMode;
extrapolation: NlaExtrapolation;
muted: boolean;
selected: boolean;
reverse?: boolean;
useTimeWarp?: boolean;
stripType?: "CLIP" | "TRANSITION" | "META" | "SOUND" | "UNKNOWN";
unsupportedReason?: string;
}
export interface NlaTrackIR {
schemaVersion: typeof NLA_PROTOCOL_SCHEMA;
id: string;
ownerId: string;
name: string;
strips: NlaStripIR[];
muted: boolean;
solo: boolean;
selected: boolean;
}
export interface NlaValidationContext {
actionIds: ReadonlySet<string>;
actionChannelPaths?: ReadonlyMap<string, ReadonlySet<string>>;
targetChannelPaths?: ReadonlySet<string>;
ownerId?: string;
actionNlaReferences?: ReadonlyMap<string, ReadonlySet<string>>;
}
export interface NlaValidationResult {
status: "SUPPORTED" | "BLOCKED";
issues: Array<{ code: ErrorCode; message: string; path?: string }>;
}
export class NlaValidationError extends Error {
readonly code: ErrorCode;
readonly path?: string;
constructor(code: ErrorCode, message: string, path?: string) {
super(message);
this.name = "NlaValidationError";
this.code = code;
this.path = path;
}
}
function record(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function finite(value: unknown, path: string): number {
if (typeof value !== "number" || !Number.isFinite(value)) throw new NlaValidationError("NLA_INVALID_STACK", `${path} must be finite`, path);
return value;
}
function parseStrip(value: unknown, path: string): NlaStripIR {
if (!record(value)) throw new NlaValidationError("NLA_INVALID_STACK", `${path} must be an object`, path);
if (typeof value.id !== "string" || value.id.length === 0 || typeof value.actionId !== "string" || value.actionId.length === 0) throw new NlaValidationError("NLA_INVALID_STACK", `${path} requires id and actionId`, path);
const strip = value as Record<string, unknown>;
const frameStart = finite(strip.frameStart, `${path}.frameStart`);
const frameEnd = finite(strip.frameEnd, `${path}.frameEnd`);
const actionFrameStart = finite(strip.actionFrameStart, `${path}.actionFrameStart`);
const actionFrameEnd = finite(strip.actionFrameEnd, `${path}.actionFrameEnd`);
const scale = finite(strip.scale, `${path}.scale`);
const repeat = finite(strip.repeat, `${path}.repeat`);
const blendIn = finite(strip.blendIn, `${path}.blendIn`);
const blendOut = finite(strip.blendOut, `${path}.blendOut`);
const influence = finite(strip.influence, `${path}.influence`);
if (frameEnd <= frameStart || actionFrameEnd <= actionFrameStart || scale <= 0 || repeat <= 0 || blendIn < 0 || blendOut < 0 || influence < 0 || influence > 1) throw new NlaValidationError("NLA_INVALID_STACK", `${path} has an invalid range or influence`, path);
if (!["REPLACE", "ADD", "MULTIPLY", "COMBINE"].includes(strip.blendMode as string)) throw new NlaValidationError("NLA_INVALID_STACK", `${path}.blendMode is invalid`, `${path}.blendMode`);
if (!["NOTHING", "HOLD", "HOLD_FORWARD"].includes(strip.extrapolation as string)) throw new NlaValidationError("NLA_INVALID_STACK", `${path}.extrapolation is invalid`, `${path}.extrapolation`);
if (typeof strip.muted !== "boolean" || typeof strip.selected !== "boolean") throw new NlaValidationError("NLA_INVALID_STACK", `${path}.muted/selected must be boolean`, path);
if (strip.reverse !== undefined && typeof strip.reverse !== "boolean") throw new NlaValidationError("NLA_INVALID_STACK", `${path}.reverse must be boolean`, `${path}.reverse`);
if (strip.useTimeWarp !== undefined && typeof strip.useTimeWarp !== "boolean") throw new NlaValidationError("NLA_INVALID_STACK", `${path}.useTimeWarp must be boolean`, `${path}.useTimeWarp`);
if (strip.stripType !== undefined && !["CLIP", "TRANSITION", "META", "SOUND", "UNKNOWN"].includes(strip.stripType as string)) throw new NlaValidationError("NLA_INVALID_STACK", `${path}.stripType is invalid`, `${path}.stripType`);
if (strip.unsupportedReason !== undefined && (typeof strip.unsupportedReason !== "string" || strip.unsupportedReason.length === 0)) throw new NlaValidationError("NLA_INVALID_STACK", `${path}.unsupportedReason is invalid`, `${path}.unsupportedReason`);
return value as unknown as NlaStripIR;
}
export function parseNlaTracks(value: unknown): NlaTrackIR[] {
if (!Array.isArray(value)) throw new NlaValidationError("NLA_INVALID_STACK", "nlaTracks must be an array", "nlaTracks");
const tracks: NlaTrackIR[] = [];
const ids = new Set<string>();
for (const [index, item] of value.entries()) {
const path = `nlaTracks[${index}]`;
if (!record(item) || item.schemaVersion !== NLA_PROTOCOL_SCHEMA || typeof item.id !== "string" || item.id.length === 0 || typeof item.ownerId !== "string" || item.ownerId.length === 0 || typeof item.name !== "string" || item.name.length === 0 || !Array.isArray(item.strips)) throw new NlaValidationError("NLA_INVALID_STACK", `${path} is invalid`, path);
if (ids.has(item.id)) throw new NlaValidationError("NLA_INVALID_STACK", `duplicate NLA track ID: ${item.id}`, path);
ids.add(item.id);
if (typeof item.muted !== "boolean" || typeof item.solo !== "boolean" || typeof item.selected !== "boolean") throw new NlaValidationError("NLA_INVALID_STACK", `${path} track flags are invalid`, path);
tracks.push({ ...item, strips: item.strips.map((strip, stripIndex) => parseStrip(strip, `${path}.strips[${stripIndex}]`)) } as NlaTrackIR);
}
return tracks;
}
export function validateNlaTracks(tracks: readonly NlaTrackIR[], context: NlaValidationContext): NlaValidationResult {
const issues: NlaValidationResult["issues"] = [];
const stripIds = new Set<string>();
for (const [trackIndex, track] of tracks.entries()) {
if (context.ownerId !== undefined && track.ownerId !== context.ownerId) issues.push({ code: "NLA_PATH_INCOMPATIBLE", message: `NLA track owner does not match target: ${track.ownerId}`, path: `nlaTracks[${trackIndex}].ownerId` });
for (let index = 1; index < track.strips.length; index++) {
const previous = track.strips[index - 1];
const current = track.strips[index];
if (previous.frameStart > current.frameStart || (previous.frameStart === current.frameStart && previous.id.localeCompare(current.id) > 0)) {
issues.push({ code: "NLA_INVALID_STACK", message: "NLA strips must be deterministically sorted by frameStart and id", path: `nlaTracks[${trackIndex}].strips` });
break;
}
if (previous.frameEnd > current.frameStart) issues.push({ code: "NLA_INVALID_STACK", message: "NLA strips on one track cannot overlap", path: `nlaTracks[${trackIndex}].strips` });
}
for (const [stripIndex, strip] of track.strips.entries()) {
const path = `nlaTracks[${trackIndex}].strips[${stripIndex}]`;
if (stripIds.has(strip.id)) issues.push({ code: "NLA_INVALID_STACK", message: `duplicate strip ID: ${strip.id}`, path });
stripIds.add(strip.id);
if (!context.actionIds.has(strip.actionId)) issues.push({ code: "NLA_ACTION_MISSING", message: `NLA strip references missing Action: ${strip.actionId}`, path });
if (strip.useTimeWarp) issues.push({ code: "NLA_TIME_WARP_UNSUPPORTED", message: "time warp is outside the finite NLA subset", path });
if (strip.stripType !== undefined && strip.stripType !== "CLIP") issues.push({ code: "NLA_STRIP_UNSUPPORTED", message: `${strip.stripType} strips are outside the finite Action Clip subset`, path });
if (strip.unsupportedReason) issues.push({ code: "NLA_STRIP_UNSUPPORTED", message: strip.unsupportedReason, path });
const expectedDuration = (strip.actionFrameEnd - strip.actionFrameStart) * strip.scale * strip.repeat;
if (Math.abs(expectedDuration - (strip.frameEnd - strip.frameStart)) > 1e-4) issues.push({ code: "NLA_INVALID_STACK", message: "NLA strip time mapping is inconsistent with Action range, scale and repeat", path });
if (strip.blendIn + strip.blendOut > strip.frameEnd - strip.frameStart) issues.push({ code: "NLA_INVALID_STACK", message: "NLA strip blend ranges exceed its duration", path });
const paths = context.actionChannelPaths?.get(strip.actionId);
if (paths && paths.size === 0) issues.push({ code: "NLA_PATH_INCOMPATIBLE", message: `Action has no compatible FCurve paths: ${strip.actionId}`, path });
if (paths && context.targetChannelPaths && ![...paths].some((channelPath) => context.targetChannelPaths?.has(channelPath))) issues.push({ code: "NLA_PATH_INCOMPATIBLE", message: `Action paths do not target the NLA owner: ${strip.actionId}`, path });
const references = context.actionNlaReferences?.get(strip.actionId);
if (references?.has(strip.actionId)) issues.push({ code: "NLA_INVALID_STACK", message: `Action references itself through NLA: ${strip.actionId}`, path });
}
}
return { status: issues.length > 0 ? "BLOCKED" : "SUPPORTED", issues };
}
export function gateNlaTracks(value: unknown, context: NlaValidationContext): CapabilityGateResult {
try {
const tracks = parseNlaTracks(value);
const result = validateNlaTracks(tracks, context);
if (result.status === "SUPPORTED") return readyGate("N-014", "NLA_STRIP_STACK");
return blockedGate("N-014", "NLA_STRIP_STACK", result.issues.map((issue) => capabilityIssue(issue.code, issue.message, issue.path)));
}
catch (error) {
const issue = error as NlaValidationError;
return blockedGate("N-014", "NLA_STRIP_STACK", [capabilityIssue(issue.code ?? "NLA_INVALID_STACK", issue.message, issue.path)]);
}
}