forked from jappel/leistungsbilanz-ts
Add pure Revit assignment planning with room-wise socket split
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e48042bde3
commit
d4c44001e1
2 changed files with 826 additions and 0 deletions
266
tests/revit-assignment-plan.test.ts
Normal file
266
tests/revit-assignment-plan.test.ts
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
circuitRefKey,
|
||||
findRowConflicts,
|
||||
newRowChoiceKey,
|
||||
objectGroupKey,
|
||||
resolveAssignmentPlan,
|
||||
splitSocketsByRoom,
|
||||
toAssignmentPlanRequestRows,
|
||||
type AssignmentPlanCircuit,
|
||||
type AssignmentPlanObject,
|
||||
type AssignmentPlanRow,
|
||||
} from "../src/shared/revit/assignment-plan.js";
|
||||
|
||||
function socket(
|
||||
id: string,
|
||||
overrides: Partial<AssignmentPlanObject> & { quantity?: number } = {}
|
||||
): AssignmentPlanObject {
|
||||
const { quantity = 2, ...rest } = overrides;
|
||||
return {
|
||||
id,
|
||||
worksharingId: `ws-${id}`,
|
||||
familyAndType: "Steckdose: Doppel",
|
||||
selectionMarker: "SD",
|
||||
deviceTypeId: "type-socket",
|
||||
roomId: "room-1",
|
||||
roomNumber: "1.01",
|
||||
roomName: "Büro",
|
||||
linkedProjectDeviceId: null,
|
||||
isSocket: true,
|
||||
planningValues: {
|
||||
displayName: "Steckdose",
|
||||
category: "single_phase",
|
||||
connectionKind: null,
|
||||
effectiveQuantity: quantity,
|
||||
powerPerUnitW: 200,
|
||||
simultaneityFactor: 1,
|
||||
cosPhi: 0.95,
|
||||
costGroup: "440",
|
||||
remark: null,
|
||||
},
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
function row(overrides: Partial<AssignmentPlanRow> = {}): AssignmentPlanRow {
|
||||
return {
|
||||
id: "row-1",
|
||||
circuitId: "circuit-1",
|
||||
roomId: "room-1",
|
||||
displayName: "Steckdose",
|
||||
linkedProjectDeviceId: null,
|
||||
category: "single_phase",
|
||||
connectionKind: null,
|
||||
powerPerUnit: 0.2,
|
||||
simultaneityFactor: 1,
|
||||
cosPhi: 0.95,
|
||||
costGroup: "440",
|
||||
remark: null,
|
||||
linkedSelectionMarkers: ["SD"],
|
||||
linkedObjectCount: 2,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function circuit(overrides: Partial<AssignmentPlanCircuit> = {}): AssignmentPlanCircuit {
|
||||
return {
|
||||
id: "circuit-1",
|
||||
sectionId: "section-2",
|
||||
category: "single_phase",
|
||||
equipmentIdentifier: "-2F1",
|
||||
displayName: "Steckdose",
|
||||
rows: [row()],
|
||||
linkedSocketQuantity: 4,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: "section-1", category: "lighting" as const },
|
||||
{ id: "section-2", category: "single_phase" as const },
|
||||
];
|
||||
|
||||
describe("Revit assignment plan", () => {
|
||||
it("splits 8 double sockets with limit 8 into two circuits of 4 objects", () => {
|
||||
const objects = Array.from({ length: 8 }, (_, index) => socket(`s${index + 1}`));
|
||||
const result = splitSocketsByRoom(objects, 8);
|
||||
assert.deepEqual(result.circuits.map((ids) => ids.length), [4, 4]);
|
||||
assert.deepEqual(result.oversizedObjectIds, []);
|
||||
assert.deepEqual(result.circuits.flat().sort(), objects.map((object) => object.id).sort());
|
||||
});
|
||||
|
||||
it("keeps rooms together and only splits a room when it exceeds the limit", () => {
|
||||
const objects = [
|
||||
...[1, 2].map((index) => socket(`a${index}`, { roomId: "room-a", roomNumber: "1.02" })),
|
||||
...[1, 2, 3].map((index) => socket(`b${index}`, { roomId: "room-b", roomNumber: "1.10" })),
|
||||
...[1, 2].map((index) => socket(`c${index}`, { roomId: "room-c", roomNumber: "1.3" })),
|
||||
...[1, 2, 3, 4, 5].map((index) => socket(`d${index}`, { roomId: "room-d", roomNumber: "2.01" })),
|
||||
];
|
||||
// Natural order 1.02 (4) · 1.3 (4) · 1.10 (6) · 2.01 (10)
|
||||
const result = splitSocketsByRoom(objects, 8);
|
||||
assert.deepEqual(result.circuits, [
|
||||
["a1", "a2", "c1", "c2"],
|
||||
["b1", "b2", "b3"],
|
||||
["d1", "d2", "d3", "d4"],
|
||||
["d5"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("gives an oversized object its own circuit and skips non-sockets", () => {
|
||||
const result = splitSocketsByRoom(
|
||||
[
|
||||
socket("big", { quantity: 10 }),
|
||||
socket("small", { roomId: "room-2", roomNumber: "1.02" }),
|
||||
socket("light", { isSocket: false }),
|
||||
],
|
||||
8
|
||||
);
|
||||
assert.deepEqual(result.circuits, [["big"], ["small"]]);
|
||||
assert.deepEqual(result.oversizedObjectIds, ["big"]);
|
||||
assert.deepEqual(result.skippedObjectIds, ["light"]);
|
||||
assert.throws(() => splitSocketsByRoom([], 0), /positive ganze Zahl/);
|
||||
});
|
||||
|
||||
it("groups single and double sockets of a room into one row and separates rooms", () => {
|
||||
const single = socket("single", { quantity: 1 });
|
||||
assert.equal(objectGroupKey(single), objectGroupKey(socket("double")));
|
||||
const plan = resolveAssignmentPlan({
|
||||
objects: [single, socket("double"), socket("other-room", { roomId: "room-2" })],
|
||||
circuits: [],
|
||||
sections,
|
||||
plan: {
|
||||
newCircuits: [{ key: "n1", sectionId: "section-2" }],
|
||||
assignments: ["single", "double", "other-room"].map((objectId) => ({
|
||||
objectId,
|
||||
target: { kind: "new-circuit" as const, key: "n1" },
|
||||
})),
|
||||
newRowChoices: [],
|
||||
},
|
||||
maxSocketsPerCircuit: 8,
|
||||
});
|
||||
assert.equal(plan.circuits.length, 1);
|
||||
assert.deepEqual(plan.circuits[0]!.rows.map((entry) => entry.quantity), [3, 2]);
|
||||
assert.equal(plan.circuits[0]!.socketQuantityAfter, 5);
|
||||
assert.equal(plan.newCircuitCount, 1);
|
||||
assert.deepEqual(plan.issues, []);
|
||||
assert.deepEqual(toAssignmentPlanRequestRows(plan).map((entry) => entry.kind), [
|
||||
"new-row-in-new-circuit",
|
||||
"new-row-in-new-circuit",
|
||||
]);
|
||||
});
|
||||
|
||||
it("joins a matching linked row on circuit drop unless a new row is chosen", () => {
|
||||
const input = {
|
||||
objects: [socket("s1")],
|
||||
circuits: [circuit()],
|
||||
sections,
|
||||
maxSocketsPerCircuit: 8,
|
||||
};
|
||||
const joined = resolveAssignmentPlan({
|
||||
...input,
|
||||
plan: {
|
||||
newCircuits: [],
|
||||
assignments: [{ objectId: "s1", target: { kind: "circuit", circuitId: "circuit-1" } }],
|
||||
newRowChoices: [],
|
||||
},
|
||||
});
|
||||
assert.deepEqual(joined.circuits[0]!.rows[0]!.row, { kind: "existing", rowId: "row-1" });
|
||||
assert.equal(joined.circuits[0]!.socketQuantityAfter, 6);
|
||||
|
||||
const separate = resolveAssignmentPlan({
|
||||
...input,
|
||||
plan: {
|
||||
newCircuits: [],
|
||||
assignments: [{ objectId: "s1", target: { kind: "circuit", circuitId: "circuit-1" } }],
|
||||
newRowChoices: [
|
||||
newRowChoiceKey({ kind: "existing", circuitId: "circuit-1" }, objectGroupKey(socket("s1"))),
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.equal(separate.circuits[0]!.rows[0]!.row.kind, "new");
|
||||
assert.equal(separate.circuits[0]!.rows[0]!.matchingRowId, "row-1");
|
||||
|
||||
const manualOnly = resolveAssignmentPlan({
|
||||
...input,
|
||||
circuits: [circuit({ rows: [row({ linkedObjectCount: 0, linkedSelectionMarkers: [] })] })],
|
||||
plan: {
|
||||
newCircuits: [],
|
||||
assignments: [{ objectId: "s1", target: { kind: "circuit", circuitId: "circuit-1" } }],
|
||||
newRowChoices: [],
|
||||
},
|
||||
});
|
||||
assert.equal(manualOnly.circuits[0]!.rows[0]!.row.kind, "new");
|
||||
});
|
||||
|
||||
it("lists planning conflicts and blocking issues for an explicit row drop", () => {
|
||||
const differing = socket("s1", {
|
||||
planningValues: { ...socket("x").planningValues, powerPerUnitW: 250, simultaneityFactor: null },
|
||||
});
|
||||
assert.deepEqual(
|
||||
findRowConflicts(row({ simultaneityFactor: 0.5 }), differing).map((entry) => entry.field),
|
||||
["powerPerUnit", "simultaneityFactor"]
|
||||
);
|
||||
const plan = resolveAssignmentPlan({
|
||||
objects: [differing, socket("s2", { roomId: "room-2", selectionMarker: "X" })],
|
||||
circuits: [circuit()],
|
||||
sections,
|
||||
plan: {
|
||||
newCircuits: [],
|
||||
assignments: [
|
||||
{ objectId: "s1", target: { kind: "row", rowId: "row-1" } },
|
||||
{ objectId: "s2", target: { kind: "row", rowId: "row-1" } },
|
||||
],
|
||||
newRowChoices: [],
|
||||
},
|
||||
maxSocketsPerCircuit: null,
|
||||
});
|
||||
assert.deepEqual(plan.conflictObjectIds, ["s1"]);
|
||||
assert.ok(plan.issues.some((issue) => /Raum/.test(issue)));
|
||||
assert.ok(plan.issues.some((issue) => /Auswahlkenner/.test(issue)));
|
||||
assert.equal(plan.circuits[0]!.overLimit, false);
|
||||
});
|
||||
|
||||
it("marks only circuits that gain sockets above the limit and rejects wrong categories", () => {
|
||||
const plan = resolveAssignmentPlan({
|
||||
objects: [socket("s1"), socket("s2"), socket("s3", { quantity: 4 })],
|
||||
circuits: [circuit()],
|
||||
sections,
|
||||
plan: {
|
||||
newCircuits: [{ key: "light", sectionId: "section-1" }],
|
||||
assignments: [
|
||||
{ objectId: "s1", target: { kind: "circuit", circuitId: "circuit-1" } },
|
||||
{ objectId: "s2", target: { kind: "circuit", circuitId: "circuit-1" } },
|
||||
{ objectId: "s3", target: { kind: "new-circuit", key: "light" } },
|
||||
],
|
||||
newRowChoices: [],
|
||||
},
|
||||
maxSocketsPerCircuit: 6,
|
||||
});
|
||||
assert.deepEqual(plan.overLimitRefKeys, ["circuit-1"]);
|
||||
assert.equal(plan.circuits[0]!.socketQuantityAfter, 8);
|
||||
assert.equal(circuitRefKey(plan.circuits[1]!.circuit), "new:light");
|
||||
assert.ok(plan.issues.some((issue) => /Kategorie/.test(issue)));
|
||||
});
|
||||
|
||||
it("reports duplicate and unknown objects", () => {
|
||||
const plan = resolveAssignmentPlan({
|
||||
objects: [socket("s1")],
|
||||
circuits: [],
|
||||
sections,
|
||||
plan: {
|
||||
newCircuits: [{ key: "n1", sectionId: "section-2" }],
|
||||
assignments: [
|
||||
{ objectId: "s1", target: { kind: "new-circuit", key: "n1" } },
|
||||
{ objectId: "s1", target: { kind: "new-circuit", key: "n1" } },
|
||||
{ objectId: "gone", target: { kind: "new-circuit", key: "n1" } },
|
||||
],
|
||||
newRowChoices: [],
|
||||
},
|
||||
maxSocketsPerCircuit: 8,
|
||||
});
|
||||
assert.equal(plan.objectCount, 1);
|
||||
assert.equal(plan.issues.length, 2);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue