chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import type { ErrorCode } from "./error";
|
||||
import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } from "./capability-gates";
|
||||
|
||||
export const NLA_PROTOCOL_SCHEMA = 1 as const;
|
||||
export const NLA_OBJECT_ID_TYPE = 0x424f; // DNA_ID_enums.h: MAKE_ID2('O', 'B'); assignment stays native.
|
||||
export const NLA_STACK_BUDGET = Object.freeze({
|
||||
maxTracks: 4_096,
|
||||
maxStripsPerTrack: 16_384,
|
||||
@@ -10,12 +11,13 @@ export const NLA_STACK_BUDGET = Object.freeze({
|
||||
maxNameBytes: 1_024,
|
||||
maxUnsupportedReasonBytes: 4_096,
|
||||
});
|
||||
export type NlaBlendMode = "REPLACE" | "ADD" | "MULTIPLY" | "COMBINE";
|
||||
export type NlaBlendMode = "REPLACE" | "ADD" | "SUBTRACT" | "MULTIPLY" | "COMBINE";
|
||||
export type NlaExtrapolation = "NOTHING" | "HOLD" | "HOLD_FORWARD";
|
||||
|
||||
export interface NlaStripIR {
|
||||
id: string;
|
||||
actionId: string;
|
||||
sourceActionId?: string;
|
||||
frameStart: number;
|
||||
frameEnd: number;
|
||||
actionFrameStart: number;
|
||||
@@ -31,6 +33,12 @@ export interface NlaStripIR {
|
||||
selected: boolean;
|
||||
reverse?: boolean;
|
||||
useTimeWarp?: boolean;
|
||||
useSyncLength?: boolean;
|
||||
useAnimatedInfluence?: boolean;
|
||||
useAutoBlend?: boolean;
|
||||
actionSlotHandle?: number;
|
||||
actionSlotIdentifier?: string;
|
||||
actionSlotAssigned?: boolean;
|
||||
stripType?: "CLIP" | "TRANSITION" | "META" | "SOUND" | "UNKNOWN";
|
||||
unsupportedReason?: string;
|
||||
}
|
||||
@@ -108,13 +116,15 @@ function finite(value: unknown, path: string, maximumMagnitude = 1_000_000): num
|
||||
function parseStrip(value: unknown, path: string): NlaStripIR {
|
||||
if (!record(value)) throw new NlaValidationError("NLA_INVALID_STACK", `${path} must be an object`, path);
|
||||
exactKeys(value, [
|
||||
"id", "actionId", "frameStart", "frameEnd", "actionFrameStart", "actionFrameEnd",
|
||||
"id", "actionId", "sourceActionId", "frameStart", "frameEnd", "actionFrameStart", "actionFrameEnd",
|
||||
"scale", "repeat", "blendIn", "blendOut", "influence", "blendMode", "extrapolation",
|
||||
"muted", "selected", "reverse", "useTimeWarp", "stripType", "unsupportedReason",
|
||||
"muted", "selected", "reverse", "useTimeWarp", "useSyncLength", "useAnimatedInfluence", "useAutoBlend", "actionSlotHandle", "actionSlotIdentifier", "actionSlotAssigned", "stripType", "unsupportedReason",
|
||||
], path);
|
||||
boundedText(value.id, `${path}.id`, NLA_STACK_BUDGET.maxIdentifierBytes);
|
||||
boundedText(value.actionId, `${path}.actionId`, NLA_STACK_BUDGET.maxIdentifierBytes);
|
||||
if (value.sourceActionId !== undefined) boundedText(value.sourceActionId, `${path}.sourceActionId`, NLA_STACK_BUDGET.maxIdentifierBytes);
|
||||
const strip = value as Record<string, unknown>;
|
||||
const actionClip = strip.stripType === undefined || strip.stripType === "CLIP";
|
||||
const frameStart = finite(strip.frameStart, `${path}.frameStart`);
|
||||
const frameEnd = finite(strip.frameEnd, `${path}.frameEnd`);
|
||||
const actionFrameStart = finite(strip.actionFrameStart, `${path}.actionFrameStart`);
|
||||
@@ -124,12 +134,19 @@ function parseStrip(value: unknown, path: string): NlaStripIR {
|
||||
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 (frameEnd <= frameStart || (actionClip && (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", "SUBTRACT", "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.useSyncLength !== undefined && typeof strip.useSyncLength !== "boolean") throw new NlaValidationError("NLA_INVALID_STACK", `${path}.useSyncLength must be boolean`, `${path}.useSyncLength`);
|
||||
if (strip.useAnimatedInfluence !== undefined && typeof strip.useAnimatedInfluence !== "boolean") throw new NlaValidationError("NLA_INVALID_STACK", `${path}.useAnimatedInfluence must be boolean`, `${path}.useAnimatedInfluence`);
|
||||
if (strip.useAutoBlend !== undefined && typeof strip.useAutoBlend !== "boolean") throw new NlaValidationError("NLA_INVALID_STACK", `${path}.useAutoBlend must be boolean`, `${path}.useAutoBlend`);
|
||||
const actionSlotHandle = strip.actionSlotHandle;
|
||||
if (actionSlotHandle !== undefined && actionSlotHandle !== null && (typeof actionSlotHandle !== "number" || !Number.isSafeInteger(actionSlotHandle) || actionSlotHandle < 0)) throw new NlaValidationError("NLA_INVALID_STACK", `${path}.actionSlotHandle must be a non-negative integer`, `${path}.actionSlotHandle`);
|
||||
if (strip.actionSlotIdentifier !== undefined) boundedText(strip.actionSlotIdentifier, `${path}.actionSlotIdentifier`, NLA_STACK_BUDGET.maxIdentifierBytes);
|
||||
if (strip.actionSlotAssigned !== undefined && typeof strip.actionSlotAssigned !== "boolean") throw new NlaValidationError("NLA_INVALID_STACK", `${path}.actionSlotAssigned must be boolean`, `${path}.actionSlotAssigned`);
|
||||
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) boundedText(strip.unsupportedReason, `${path}.unsupportedReason`, NLA_STACK_BUDGET.maxUnsupportedReasonBytes);
|
||||
return value as unknown as NlaStripIR;
|
||||
@@ -185,8 +202,8 @@ export function validateNlaTracks(tracks: readonly NlaTrackIR[], context: NlaVal
|
||||
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 unsupportedReason = strip.unsupportedReason ?? (strip.stripType !== undefined && strip.stripType !== "CLIP" ? `${strip.stripType} strips are outside the finite Action Clip subset` : undefined);
|
||||
if (unsupportedReason) issues.push({ code: "NLA_STRIP_UNSUPPORTED", message: 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 });
|
||||
|
||||
Reference in New Issue
Block a user