153 lines
5.3 KiB
TypeScript
153 lines
5.3 KiB
TypeScript
import type { LayoutRole } from "../../packages/definitions/index.ts";
|
|
import { getBoardLaneColumnCount, getRootSectionLane } from "../../packages/definitions/index.ts";
|
|
|
|
interface GeometryLayout {
|
|
id: string;
|
|
boardId: string;
|
|
columnCount: number;
|
|
leftGutterColumnCount: number;
|
|
rightGutterColumnCount: number;
|
|
role: LayoutRole;
|
|
}
|
|
|
|
interface GeometrySection {
|
|
id: string;
|
|
boardId: string;
|
|
kind: "empty" | "container";
|
|
xOffset: number | null;
|
|
}
|
|
|
|
interface GeometryPlacement {
|
|
layoutId: string;
|
|
sectionId: string;
|
|
xOffset: number;
|
|
yOffset: number;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
interface GeometrySectionLayout extends GeometryPlacement {
|
|
parentSectionId: string | null;
|
|
}
|
|
|
|
interface GeometryItemLayout extends GeometryPlacement {
|
|
itemId: string;
|
|
boardId: string;
|
|
}
|
|
|
|
export interface FixtureGeometry {
|
|
layouts: GeometryLayout[];
|
|
sections: GeometrySection[];
|
|
sectionLayouts: GeometrySectionLayout[];
|
|
itemLayouts: GeometryItemLayout[];
|
|
}
|
|
|
|
const placementIsValid = (placement: GeometryPlacement) =>
|
|
Number.isInteger(placement.xOffset) &&
|
|
Number.isInteger(placement.yOffset) &&
|
|
Number.isInteger(placement.width) &&
|
|
Number.isInteger(placement.height) &&
|
|
placement.xOffset >= 0 &&
|
|
placement.yOffset >= 0 &&
|
|
placement.width >= 1 &&
|
|
placement.height >= 1;
|
|
|
|
export const findFixtureGeometryErrors = (geometry: FixtureGeometry) => {
|
|
const errors: string[] = [];
|
|
const layoutsById = new Map(geometry.layouts.map((layout) => [layout.id, layout]));
|
|
const sectionsById = new Map(geometry.sections.map((section) => [section.id, section]));
|
|
const sectionLayoutsByKey = new Map<string, GeometrySectionLayout>();
|
|
|
|
for (const sectionLayout of geometry.sectionLayouts) {
|
|
const key = `${sectionLayout.layoutId}\0${sectionLayout.sectionId}`;
|
|
if (sectionLayoutsByKey.has(key)) {
|
|
errors.push(`container ${sectionLayout.sectionId} has duplicate placement in layout ${sectionLayout.layoutId}`);
|
|
continue;
|
|
}
|
|
sectionLayoutsByKey.set(key, sectionLayout);
|
|
}
|
|
|
|
const parentCapacity = (
|
|
layout: GeometryLayout,
|
|
parentSection: GeometrySection,
|
|
): { columns: number; kind: "lane" | "container" } | undefined => {
|
|
if (parentSection.kind === "empty") {
|
|
return {
|
|
columns: getBoardLaneColumnCount(layout, getRootSectionLane(parentSection.xOffset)),
|
|
kind: "lane",
|
|
};
|
|
}
|
|
|
|
const parentLayout = sectionLayoutsByKey.get(`${layout.id}\0${parentSection.id}`);
|
|
if (!parentLayout) return undefined;
|
|
return { columns: parentLayout.width, kind: "container" };
|
|
};
|
|
|
|
for (const sectionLayout of geometry.sectionLayouts) {
|
|
const layout = layoutsById.get(sectionLayout.layoutId);
|
|
const section = sectionsById.get(sectionLayout.sectionId);
|
|
const parent = sectionLayout.parentSectionId ? sectionsById.get(sectionLayout.parentSectionId) : undefined;
|
|
if (!layout || !section || !parent) {
|
|
errors.push(
|
|
`container ${sectionLayout.sectionId} in layout ${sectionLayout.layoutId} has unknown geometry references`,
|
|
);
|
|
continue;
|
|
}
|
|
if (section.kind !== "container" || section.boardId !== layout.boardId || parent.boardId !== layout.boardId) {
|
|
errors.push(`container ${section.id} in layout ${layout.id} crosses a board or root boundary`);
|
|
continue;
|
|
}
|
|
if (!placementIsValid(sectionLayout)) {
|
|
errors.push(`container ${section.id} in layout ${layout.id} has an invalid placement`);
|
|
continue;
|
|
}
|
|
|
|
const capacity = parentCapacity(layout, parent);
|
|
if (!capacity) {
|
|
errors.push(`container ${section.id} in layout ${layout.id} has an unplaced parent container`);
|
|
} else if (sectionLayout.xOffset + sectionLayout.width > capacity.columns) {
|
|
errors.push(
|
|
`container ${section.id} in layout ${layout.id} exceeds its ${capacity.columns}-column parent ${capacity.kind}`,
|
|
);
|
|
}
|
|
|
|
const visited = new Set<string>();
|
|
let currentSectionId: string | null = section.id;
|
|
while (currentSectionId) {
|
|
if (visited.has(currentSectionId)) {
|
|
errors.push(`container cycle detected in layout ${layout.id} at ${currentSectionId}`);
|
|
break;
|
|
}
|
|
visited.add(currentSectionId);
|
|
const current = sectionsById.get(currentSectionId);
|
|
if (!current || current.kind === "empty") break;
|
|
currentSectionId = sectionLayoutsByKey.get(`${layout.id}\0${currentSectionId}`)?.parentSectionId ?? null;
|
|
}
|
|
}
|
|
|
|
for (const itemLayout of geometry.itemLayouts) {
|
|
const layout = layoutsById.get(itemLayout.layoutId);
|
|
const parent = sectionsById.get(itemLayout.sectionId);
|
|
if (!layout || !parent || itemLayout.boardId !== layout.boardId || parent.boardId !== layout.boardId) {
|
|
errors.push(`item ${itemLayout.itemId} in layout ${itemLayout.layoutId} crosses a board or section boundary`);
|
|
continue;
|
|
}
|
|
if (!placementIsValid(itemLayout)) {
|
|
errors.push(`item ${itemLayout.itemId} in layout ${layout.id} has an invalid placement`);
|
|
continue;
|
|
}
|
|
const capacity = parentCapacity(layout, parent);
|
|
if (!capacity) {
|
|
errors.push(`item ${itemLayout.itemId} in layout ${layout.id} has an unplaced parent container`);
|
|
continue;
|
|
}
|
|
if (itemLayout.xOffset + itemLayout.width > capacity.columns) {
|
|
errors.push(
|
|
`item ${itemLayout.itemId} in layout ${layout.id} exceeds its ${capacity.columns}-column parent ${capacity.kind}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
};
|