import { Immutable, produce } from "immer"; export type Doc = Immutable<{ ops: Op[] }>; export type Op = CreateGrid | Subdivide; export type CreateGrid = Immutable<{ type: "createGrid"; gridId: string; rows: number; baseCellsPerRow: number; }>; export function createGrid(): CreateGrid { return { type: "createGrid", gridId: crypto.randomUUID(), rows: 4, baseCellsPerRow: 16, }; } export type Subdivide = Immutable<{ type: "subdivide"; gridId: string; rowIndex: number; startCellIndex: number; endCellIndex: number; subdivisions: number; }>; export function subdivide( gridId: string, rowIndex: number, startCellIndex: number, endCellIndex: number, subdivisions: number, ): Subdivide { return { type: "subdivide", gridId, rowIndex, startCellIndex, endCellIndex, subdivisions, }; } export function defaultDoc(): Doc { const ops = [createGrid()]; return { ops }; } export function apply(doc: Doc, ...ops: Op[]): Doc { return produce(doc, (doc) => { doc.ops.push(...ops); }); } export type DocIndex = Immutable<{ opsByType: Map; }>; export function indexDoc(doc: Doc): DocIndex { const opsByType = new Map(); for (const op of doc.ops) { opsByType.set(op.type, [...(opsByType.get(op.type) ?? []), op]); } return { opsByType }; } export function getOpsByType( index: DocIndex, type: T, ): Extract[] { return (index.opsByType.get(type) ?? []) as Extract[]; } export type Grid = Immutable<{ id: string; rows: Row[] }>; export type Row = Immutable<{ index: number; cells: Cell[] }>; export type Cell = Immutable<{}>; export function realizeGrids(doc: Doc): Grid[] { const index = indexDoc(doc); const createGridOps = getOpsByType(index, "createGrid"); return createGridOps.map((op) => realizeGrid(doc, index, op)); } function realizeGrid(doc: Doc, index: DocIndex, createOp: CreateGrid): Grid { const rows = []; for (let rowIndex = 0; rowIndex < createOp.rows; rowIndex++) { let cells: Cell[] = []; for (let cellIndex = 0; cellIndex < createOp.baseCellsPerRow; cellIndex++) { cells.push({ index: cellIndex }); } const subdivideOps = doc.ops.filter( (op) => op.type === "subdivide" && op.gridId === createOp.gridId && op.rowIndex === rowIndex, ) as Subdivide[]; subdivideOps.forEach((op) => { cells = [ ...cells.slice(0, op.startCellIndex), ...Array.from({ length: op.subdivisions }, () => ({})), ...cells.slice(op.endCellIndex + 1), ]; }); rows.push({ index: rowIndex, cells }); } return { id: createOp.gridId, rows, }; }