Advance M8-M11 parity workflows
This commit is contained in:
122
web/protocol/grease-pencil-reorder.ts
Normal file
122
web/protocol/grease-pencil-reorder.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user