143 lines
6.3 KiB
JavaScript
143 lines
6.3 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import test from "node:test";
|
|
import ts from "typescript";
|
|
|
|
const repoRoot = path.resolve(import.meta.dirname, "../../..");
|
|
const sourcePath = path.join(repoRoot, "web/protocol/curve-topology-editor.ts");
|
|
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "curve-topology-editor-unit-"));
|
|
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
|
|
fileName: sourcePath,
|
|
reportDiagnostics: true,
|
|
});
|
|
assert.deepEqual(transpiled.diagnostics, []);
|
|
const modulePath = path.join(temporary, "curve-topology-editor.mjs");
|
|
fs.writeFileSync(modulePath, transpiled.outputText);
|
|
const curve = await import(pathToFileURL(modulePath));
|
|
|
|
function claim(operator, selection) {
|
|
const base = {
|
|
schemaVersion: 1,
|
|
operator,
|
|
dataId: "curve:Curve",
|
|
baseRevision: 17,
|
|
inputSplineCount: 4,
|
|
inputPointCount: 12,
|
|
selectedSplineIndices: selection === "SPLINES" || selection === "POINTS_OR_SPLINES" ? [1] : [],
|
|
selectedPointIndices: selection === "POINTS" ? [2] : [],
|
|
addedSplineCount: 0,
|
|
addedPointCount: 0,
|
|
outputSplineCount: 4,
|
|
outputPointCount: 12,
|
|
payloadBytes: 256,
|
|
};
|
|
if (operator === "ADD_SPLINE") return { ...base, addedSplineCount: 1, addedPointCount: 2, outputSplineCount: 5, outputPointCount: 14 };
|
|
if (operator === "DUPLICATE") return { ...base, addedSplineCount: 1, addedPointCount: 2, outputSplineCount: 5, outputPointCount: 14 };
|
|
if (operator === "EXTRUDE") return { ...base, addedPointCount: 1, outputPointCount: 13 };
|
|
if (operator === "SPLIT") return { ...base, addedSplineCount: 1, outputSplineCount: 5 };
|
|
if (operator === "SUBDIVIDE") return { ...base, addedPointCount: 2, outputPointCount: 14, subdivideCuts: 2 };
|
|
return base;
|
|
}
|
|
|
|
test("M9-04 freezes the Blender-sourced Curve topology allowlist and budgets", () => {
|
|
const manifest = curve.createCurveTopologyEditorManifest();
|
|
assert.equal(manifest.schemaVersion, 1);
|
|
assert.equal(manifest.sourceAuthority, "blender-5.2.0/source/blender/editors/curve/curve_ops.cc");
|
|
assert.equal(manifest.atomicMainTransaction, true);
|
|
assert.deepEqual(manifest.budget, {
|
|
maxSplines: 65_536,
|
|
maxPoints: 1_000_000,
|
|
maxSelectedElements: 100_000,
|
|
maxAddedSplinesPerOperation: 4_096,
|
|
maxAddedPointsPerOperation: 100_000,
|
|
maxPayloadBytes: 67_108_864,
|
|
maxSubdivideCuts: 64,
|
|
maxDataIdBytes: 256,
|
|
maxOperationsPerMainTransaction: 1,
|
|
});
|
|
assert.deepEqual(manifest.operators.map((operator) => operator.id), [
|
|
"ADD_SPLINE", "DECIMATE", "DELETE", "DISSOLVE_VERTICES", "DUPLICATE", "EXTRUDE",
|
|
"MAKE_SEGMENT", "SEPARATE", "SET_HANDLE_TYPE", "SET_SPLINE_TYPE", "SPLIT", "SUBDIVIDE",
|
|
"SWITCH_DIRECTION", "TOGGLE_CYCLIC",
|
|
]);
|
|
assert.deepEqual(manifest.operators.filter((operator) => operator.gate.status === "READY").map((operator) => operator.id), ["TOGGLE_CYCLIC"]);
|
|
assert.ok(manifest.operators.filter((operator) => operator.id !== "TOGGLE_CYCLIC").every((operator) => operator.gate.status === "BLOCKED" && operator.gate.reasonCode === "CURVE_TOPOLOGY_OPERATOR_NOT_VERIFIED"));
|
|
});
|
|
|
|
test("M9-04 accepts one revision-bound budget claim for every allowlisted operator", () => {
|
|
for (const descriptor of curve.CURVE_TOPOLOGY_EDITOR_OPERATORS) {
|
|
const parsed = curve.parseCurveTopologyOperationClaim(claim(descriptor.id, descriptor.selection), 17);
|
|
assert.equal(parsed.operator, descriptor.id);
|
|
assert.equal(parsed.baseRevision, 17);
|
|
}
|
|
});
|
|
|
|
test("M9-04 rejects unknown, stale, ambiguous and over-budget Curve topology claims", () => {
|
|
const valid = claim("SUBDIVIDE", "POINTS_OR_SPLINES");
|
|
const cases = [
|
|
[{ ...valid, operator: "SPIN" }, "NON_MESH_TOPOLOGY_EDIT_UNSUPPORTED"],
|
|
[valid, "REVISION_CONFLICT", 18],
|
|
[{ ...valid, selectedSplineIndices: [1, 1] }, "NON_MESH_PROPERTY_INVALID"],
|
|
[{ ...valid, selectedPointIndices: [2] }, "NON_MESH_PROPERTY_INVALID"],
|
|
[{ ...valid, dataId: "data:Curve" }, "NON_MESH_PROPERTY_INVALID"],
|
|
[{ ...claim("DELETE", "POINTS_OR_SPLINES"), inputSplineCount: 0, selectedSplineIndices: [0] }, "NON_MESH_PROPERTY_INVALID"],
|
|
[{ ...valid, outputPointCount: 15 }, "NON_MESH_PROPERTY_INVALID"],
|
|
[{ ...valid, payloadBytes: curve.CURVE_TOPOLOGY_EDITOR_BUDGET.maxPayloadBytes + 1 }, "NON_MESH_DATA_BUDGET_EXCEEDED"],
|
|
[{ ...valid, subdivideCuts: 65 }, "NON_MESH_DATA_BUDGET_EXCEEDED"],
|
|
[{ ...claim("DELETE", "POINTS_OR_SPLINES"), subdivideCuts: 1 }, "NON_MESH_PROPERTY_INVALID"],
|
|
[{ ...valid, futureField: true }, "NON_MESH_PROPERTY_INVALID"],
|
|
];
|
|
for (const [value, code, revision = 17] of cases) {
|
|
assert.throws(() => curve.parseCurveTopologyOperationClaim(value, revision), (error) => error.code === code);
|
|
}
|
|
});
|
|
|
|
test("M9-05 builds one revision-bound TOGGLE_CYCLIC Main command", () => {
|
|
const operation = curve.buildCurveToggleCyclicOperation({
|
|
schemaVersion: 1,
|
|
dataId: "curve:WebCurveData",
|
|
baseRevision: 23,
|
|
splineIndex: 1,
|
|
splineCount: 2,
|
|
pointCount: 7,
|
|
cyclicU: [false, true],
|
|
}, 23);
|
|
assert.deepEqual(operation.command, {
|
|
type: "setCurveTopology",
|
|
dataId: "curve:WebCurveData",
|
|
baseRevision: 23,
|
|
cyclicU: [false, false],
|
|
});
|
|
assert.equal(operation.claim.operator, "TOGGLE_CYCLIC");
|
|
assert.deepEqual(operation.claim.selectedSplineIndices, [1]);
|
|
assert.equal(operation.previousCyclic, true);
|
|
assert.equal(operation.nextCyclic, false);
|
|
});
|
|
|
|
test("M9-05 keeps stale and malformed TOGGLE_CYCLIC requests out of Main", () => {
|
|
const valid = {
|
|
schemaVersion: 1,
|
|
dataId: "curve:WebCurveData",
|
|
baseRevision: 23,
|
|
splineIndex: 0,
|
|
splineCount: 2,
|
|
pointCount: 7,
|
|
cyclicU: [false, false],
|
|
};
|
|
assert.throws(() => curve.buildCurveToggleCyclicOperation(valid, 24), (error) => error.code === "REVISION_CONFLICT");
|
|
for (const value of [
|
|
{ ...valid, splineIndex: 2 },
|
|
{ ...valid, splineCount: 0 },
|
|
{ ...valid, cyclicU: [false] },
|
|
{ ...valid, dataId: "surface:WebSurfaceData" },
|
|
]) {
|
|
assert.throws(() => curve.buildCurveToggleCyclicOperation(value, 23), (error) => error.code === "NON_MESH_PROPERTY_INVALID");
|
|
}
|
|
});
|
|
|
|
test.after(() => fs.rmSync(temporary, { recursive: true, force: true }));
|