Advance bounded editor and cache workflows
This commit is contained in:
@@ -17,7 +17,26 @@ export type GreasePencilEditorEditIR =
|
||||
| { type: "SET_FRAME"; revision: number; frame: number }
|
||||
| { type: "SET_LAYER"; revision: number; layerId: string }
|
||||
| { type: "SET_ONION"; revision: number; enabled: boolean }
|
||||
| { type: "SET_SELECTION"; revision: number; strokeIndices: number[]; points: GreasePencilPointSelectionIR[] };
|
||||
| { type: "SET_SELECTION"; revision: number; strokeIndices: number[]; points: GreasePencilPointSelectionIR[] }
|
||||
| { type: "TRANSLATE_POINTS"; revision: number; translation: [number, number, number] };
|
||||
|
||||
export interface GreasePencilEditablePointIR {
|
||||
position: [number, number, number];
|
||||
radius?: number;
|
||||
opacity?: number;
|
||||
vertexColor?: [number, number, number, number];
|
||||
}
|
||||
|
||||
export interface GreasePencilEditableStrokeIR {
|
||||
cyclic?: boolean;
|
||||
materialIndex?: number;
|
||||
points: GreasePencilEditablePointIR[];
|
||||
}
|
||||
|
||||
export interface GreasePencilPointEditResultIR {
|
||||
editor: GreasePencilEditorIR;
|
||||
strokes: GreasePencilEditableStrokeIR[];
|
||||
}
|
||||
|
||||
function fail(path: string, message: string): never { throw new Error(`GREASE_PENCIL_EDITOR_INVALID: ${path} ${message}`); }
|
||||
function record(value: unknown, path: string): Record<string, unknown> {
|
||||
@@ -32,6 +51,14 @@ function integer(value: unknown, path: string): number {
|
||||
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < -1_000_000 || value > 1_000_000) fail(path, "is outside the supported frame/index range");
|
||||
return value;
|
||||
}
|
||||
function finite(value: unknown, path: string): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > 1_000_000) fail(path, "must be finite and bounded");
|
||||
return value;
|
||||
}
|
||||
function tuple(value: unknown, length: number, path: string): number[] {
|
||||
if (!Array.isArray(value) || value.length !== length) fail(path, `must contain ${length} numbers`);
|
||||
return value.map((item, index) => finite(item, `${path}[${index}]`));
|
||||
}
|
||||
function nonnegativeIndex(value: unknown, path: string): number {
|
||||
const result = integer(value, path);
|
||||
if (result < 0) fail(path, "must be non-negative");
|
||||
@@ -87,6 +114,75 @@ export function applyGreasePencilEditorEdit(value: unknown, editValue: unknown):
|
||||
if (typeof edit.enabled !== "boolean") fail("edit.enabled", "must be boolean");
|
||||
return parseGreasePencilEditor({ ...editor, revision, onionSkinning: edit.enabled });
|
||||
case "SET_SELECTION": return parseGreasePencilEditor({ ...editor, revision, selectedStrokeIndices: uniqueIndices(edit.strokeIndices, "edit.strokeIndices"), selectedPoints: points(edit.points, "edit.points") });
|
||||
case "TRANSLATE_POINTS":
|
||||
tuple(edit.translation, 3, "edit.translation");
|
||||
if (editor.selectedPoints.length === 0) fail("editor.selectedPoints", "must contain at least one point for translation");
|
||||
return parseGreasePencilEditor({ ...editor, revision });
|
||||
default: fail("edit.type", "is unsupported");
|
||||
}
|
||||
}
|
||||
|
||||
function parseEditablePoint(value: unknown, path: string): GreasePencilEditablePointIR {
|
||||
const point = record(value, path);
|
||||
const result: GreasePencilEditablePointIR = { position: tuple(point.position, 3, `${path}.position`) as [number, number, number] };
|
||||
if (point.radius !== undefined) {
|
||||
result.radius = finite(point.radius, `${path}.radius`);
|
||||
if (result.radius < 0) fail(`${path}.radius`, "must be non-negative");
|
||||
}
|
||||
if (point.opacity !== undefined) {
|
||||
result.opacity = finite(point.opacity, `${path}.opacity`);
|
||||
if (result.opacity < 0 || result.opacity > 1) fail(`${path}.opacity`, "must be in [0,1]");
|
||||
}
|
||||
if (point.vertexColor !== undefined) {
|
||||
result.vertexColor = tuple(point.vertexColor, 4, `${path}.vertexColor`) as [number, number, number, number];
|
||||
if (result.vertexColor.some((component) => component < 0 || component > 1)) fail(`${path}.vertexColor`, "must be in [0,1]");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseEditableStrokes(value: unknown): GreasePencilEditableStrokeIR[] {
|
||||
if (!Array.isArray(value) || value.length > GREASE_PENCIL_EDITOR_BUDGET.maxSelection) fail("strokes", "exceeds the stroke budget");
|
||||
let pointCount = 0;
|
||||
return value.map((item, strokeIndex) => {
|
||||
const stroke = record(item, `strokes[${strokeIndex}]`);
|
||||
if (!Array.isArray(stroke.points) || stroke.points.length === 0) fail(`strokes[${strokeIndex}].points`, "must be a non-empty array");
|
||||
pointCount += stroke.points.length;
|
||||
if (pointCount > GREASE_PENCIL_EDITOR_BUDGET.maxSelection) fail("strokes", "exceeds the point budget");
|
||||
if (stroke.cyclic !== undefined && typeof stroke.cyclic !== "boolean") fail(`strokes[${strokeIndex}].cyclic`, "must be boolean");
|
||||
if (stroke.materialIndex !== undefined) nonnegativeIndex(stroke.materialIndex, `strokes[${strokeIndex}].materialIndex`);
|
||||
return {
|
||||
...(stroke.cyclic !== undefined ? { cyclic: stroke.cyclic } : {}),
|
||||
...(stroke.materialIndex !== undefined ? { materialIndex: stroke.materialIndex as number } : {}),
|
||||
points: stroke.points.map((point, pointIndex) => parseEditablePoint(point, `strokes[${strokeIndex}].points[${pointIndex}]`)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function applyGreasePencilPointTranslation(
|
||||
editorValue: unknown,
|
||||
strokesValue: unknown,
|
||||
editValue: unknown,
|
||||
): GreasePencilPointEditResultIR {
|
||||
const editor = parseGreasePencilEditor(editorValue);
|
||||
const strokes = parseEditableStrokes(strokesValue);
|
||||
const edit = record(editValue, "edit");
|
||||
if (edit.type !== "TRANSLATE_POINTS") fail("edit.type", "must be TRANSLATE_POINTS");
|
||||
if (edit.revision !== editor.revision) throw new Error("REVISION_CONFLICT: Grease Pencil editor state is stale");
|
||||
const translation = tuple(edit.translation, 3, "edit.translation") as [number, number, number];
|
||||
if (editor.selectedPoints.length === 0) fail("editor.selectedPoints", "must contain at least one point for translation");
|
||||
const selected = new Set(editor.selectedPoints.map((point) => `${point.strokeIndex}:${point.pointIndex}`));
|
||||
for (const point of editor.selectedPoints) {
|
||||
if (!strokes[point.strokeIndex]?.points[point.pointIndex]) fail("editor.selectedPoints", `references missing point ${point.strokeIndex}:${point.pointIndex}`);
|
||||
}
|
||||
const translated = strokes.map((stroke, strokeIndex) => ({
|
||||
...stroke,
|
||||
points: stroke.points.map((point, pointIndex) => selected.has(`${strokeIndex}:${pointIndex}`) ? {
|
||||
...point,
|
||||
position: point.position.map((component, axis) => finite(component + translation[axis], `translated[${strokeIndex}][${pointIndex}][${axis}]`)) as [number, number, number],
|
||||
} : { ...point, position: [...point.position] as [number, number, number] }),
|
||||
}));
|
||||
return {
|
||||
editor: parseGreasePencilEditor({ ...editor, revision: editor.revision + 1 }),
|
||||
strokes: translated,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user