Files
workinf_Blender_Wasm/web/protocol/grease-pencil-reorder.ts
mes123456 0fe8d2bb56
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

123 lines
6.0 KiB
TypeScript

import type { GreasePencilDataIR } from "./grease-pencil";
export const GREASE_PENCIL_REORDER_SCHEMA_VERSION = 1 as const;
export const GREASE_PENCIL_REORDER_BUDGET = {
maxIdBytes: 256,
minFrame: -1_000_000,
maxFrame: 1_000_000,
} as const;
export type GreasePencilLayerMoveDirection = "UP" | "DOWN" | "TOP" | "BOTTOM";
export type GreasePencilReorderCommand =
| {
type: "moveGreasePencilLayer";
schemaVersion: typeof GREASE_PENCIL_REORDER_SCHEMA_VERSION;
dataId: string;
layerId: string;
direction: GreasePencilLayerMoveDirection;
baseRevision: number;
}
| {
type: "moveGreasePencilFrame";
schemaVersion: typeof GREASE_PENCIL_REORDER_SCHEMA_VERSION;
dataId: string;
layerId: string;
frame: number;
targetFrame: number;
drawingId: string;
baseRevision: number;
};
export type GreasePencilReorderErrorCode = "GREASE_PENCIL_SCHEMA_INVALID" | "REVISION_CONFLICT";
export class GreasePencilReorderValidationError extends Error {
constructor(readonly code: GreasePencilReorderErrorCode, message: string) {
super(`${code}: ${message}`);
this.name = "GreasePencilReorderValidationError";
}
}
const encoder = new TextEncoder();
const LAYER_FIELDS = new Set(["type", "schemaVersion", "dataId", "layerId", "direction", "baseRevision"]);
const FRAME_FIELDS = new Set(["type", "schemaVersion", "dataId", "layerId", "frame", "targetFrame", "drawingId", "baseRevision"]);
function fail(code: GreasePencilReorderErrorCode, message: string): never {
throw new GreasePencilReorderValidationError(code, message);
}
function record(value: unknown): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) fail("GREASE_PENCIL_SCHEMA_INVALID", "reorder command must be an object");
return value as Record<string, unknown>;
}
function exact(value: Record<string, unknown>, fields: ReadonlySet<string>): void {
if (Object.keys(value).some((field) => !fields.has(field))) fail("GREASE_PENCIL_SCHEMA_INVALID", "reorder command contains undeclared fields");
}
function boundedId(value: unknown, prefix: string, field: string): string {
if (typeof value !== "string" || !value.startsWith(prefix) || encoder.encode(value).byteLength > GREASE_PENCIL_REORDER_BUDGET.maxIdBytes) {
fail("GREASE_PENCIL_SCHEMA_INVALID", `${field} is not a bounded ${prefix} identity`);
}
return value;
}
function integer(value: unknown, field: string): number {
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < GREASE_PENCIL_REORDER_BUDGET.minFrame || value > GREASE_PENCIL_REORDER_BUDGET.maxFrame) {
fail("GREASE_PENCIL_SCHEMA_INVALID", `${field} is outside the supported frame range`);
}
return value;
}
function revision(value: unknown): number {
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) fail("GREASE_PENCIL_SCHEMA_INVALID", "baseRevision is invalid");
return value;
}
function dataFor(command: { dataId: string; layerId: string }, currentRevision: number, data: readonly GreasePencilDataIR[]): GreasePencilDataIR {
if (!Number.isSafeInteger(currentRevision) || currentRevision < 0) fail("GREASE_PENCIL_SCHEMA_INVALID", "current revision is invalid");
const candidate = data.find((item) => item.id === command.dataId);
if (!candidate) fail("GREASE_PENCIL_SCHEMA_INVALID", "Grease Pencil data block was not found");
if (!candidate.layers.some((layer) => layer.id === command.layerId)) fail("GREASE_PENCIL_SCHEMA_INVALID", "Grease Pencil layer was not found");
return candidate;
}
export function validateGreasePencilReorderCommand(
value: unknown,
currentRevision: number,
data: readonly GreasePencilDataIR[],
): GreasePencilReorderCommand {
const command = record(value);
if (command.schemaVersion !== GREASE_PENCIL_REORDER_SCHEMA_VERSION) fail("GREASE_PENCIL_SCHEMA_INVALID", "unsupported reorder schema version");
const type = command.type;
const dataId = boundedId(command.dataId, "grease-pencil:", "dataId");
const layerId = boundedId(command.layerId, "grease-pencil-layer:", "layerId");
const baseRevision = revision(command.baseRevision);
if (baseRevision !== currentRevision) fail("REVISION_CONFLICT", "reorder command is stale");
const candidate = dataFor({ dataId, layerId }, currentRevision, data);
const layerIndex = candidate.layers.findIndex((layer) => layer.id === layerId);
if (type === "moveGreasePencilLayer") {
exact(command, LAYER_FIELDS);
const direction = command.direction;
if (direction !== "UP" && direction !== "DOWN" && direction !== "TOP" && direction !== "BOTTOM") fail("GREASE_PENCIL_SCHEMA_INVALID", "layer move direction is invalid");
const noOp = (direction === "UP" && layerIndex === candidate.layers.length - 1) ||
(direction === "DOWN" && layerIndex === 0) ||
(direction === "TOP" && layerIndex === candidate.layers.length - 1) ||
(direction === "BOTTOM" && layerIndex === 0);
if (noOp) fail("GREASE_PENCIL_SCHEMA_INVALID", "layer is already at the requested boundary");
return { type, schemaVersion: GREASE_PENCIL_REORDER_SCHEMA_VERSION, dataId, layerId, direction, baseRevision };
}
if (type !== "moveGreasePencilFrame") fail("GREASE_PENCIL_SCHEMA_INVALID", "unsupported Grease Pencil reorder command");
exact(command, FRAME_FIELDS);
const frame = integer(command.frame, "frame");
const targetFrame = integer(command.targetFrame, "targetFrame");
if (frame === targetFrame) fail("GREASE_PENCIL_SCHEMA_INVALID", "frame move must change the frame number");
const drawingId = boundedId(command.drawingId, "grease-pencil-drawing:", "drawingId");
const source = candidate.layers[layerIndex].frames.find((entry) => entry.frame === frame);
if (!source || source.drawing.id !== drawingId) fail("GREASE_PENCIL_SCHEMA_INVALID", "source frame or drawing identity was not found");
if (candidate.layers[layerIndex].frames.some((entry) => entry.frame === targetFrame)) fail("GREASE_PENCIL_SCHEMA_INVALID", "target frame already exists");
return { type, schemaVersion: GREASE_PENCIL_REORDER_SCHEMA_VERSION, dataId, layerId, frame, targetFrame, drawingId, baseRevision };
}