diff options
| author | Josh Kingsley <josh@joshkingsley.me> | 2025-11-12 22:09:48 +0200 |
|---|---|---|
| committer | Josh Kingsley <josh@joshkingsley.me> | 2025-11-12 22:09:48 +0200 |
| commit | bebe4efd7987676201381c9e9cb9dfb16c5adaa3 (patch) | |
| tree | bbd3f30aba33ddd89ad92f296688a08d7d2e02bb | |
| parent | 5e08e76c30c230aef3a5f1e21d14fa59a84b3c88 (diff) | |
feat(web): use immer types
| -rw-r--r-- | web/src/doc/index.test.ts | 29 | ||||
| -rw-r--r-- | web/src/doc/index.ts | 189 |
2 files changed, 117 insertions, 101 deletions
diff --git a/web/src/doc/index.test.ts b/web/src/doc/index.test.ts index 331d0a5..5f61398 100644 --- a/web/src/doc/index.test.ts +++ b/web/src/doc/index.test.ts @@ -1,15 +1,16 @@ -import { describe, expect, test } from "vitest"; -import Doc from "."; - -describe(Doc, () => { - describe(Doc.default, () => { - const doc = Doc.default(); - - test("produces valid grid data", () => { - expect(doc.grids.length).toBe(1); - expect(doc.grids[0].doc).toBe(doc); - expect(doc.grids[0].rows.length).toBe(4); - expect(doc.grids[0].rows[0].cells.length).toBe(16); - }); - }); +import { expect, test } from "vitest"; +import { apply, defaultDoc, realizeGrids, subdivide } from "."; + +test(realizeGrids, () => { + const doc = defaultDoc(); + const grids = realizeGrids(doc); + + expect(grids.length).toBe(1); + expect(grids[0].rows.length).toBe(4); + expect(grids[0].rows[0].cells.length).toBe(16); + + const doc2 = apply(doc, subdivide(grids[0].id, 0, 0, 3, 3)); + const grids2 = realizeGrids(doc2); + + expect(grids2[0].rows[0].cells.length).toBe(15); }); diff --git a/web/src/doc/index.ts b/web/src/doc/index.ts index a58faa6..ae221f0 100644 --- a/web/src/doc/index.ts +++ b/web/src/doc/index.ts @@ -1,110 +1,125 @@ -export abstract class DocOp { - abstract key: string; +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 class CreateGridOp extends DocOp { - gridId: string = crypto.randomUUID(); - rows = 4; - key = this.gridId; - 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 default class Doc { - readonly ops: DocOp[]; - - readonly opsByKey: Map<string, readonly DocOp[]>; - readonly opsByType: Map<Function, readonly DocOp[]>; - - constructor(ops: DocOp[]) { - this.ops = ops; - - const opsByKey = new Map(); - const opsByType = new Map(); - - for (const op of ops) { - const maybeKeyOps = opsByKey.get(op.key); - - if (maybeKeyOps) maybeKeyOps.push(op); - else opsByKey.set(op.key, [op]); +export function defaultDoc(): Doc { + const ops = [createGrid()]; + return { ops }; +} - const maybeTypeOps = opsByType.get(op.constructor); +export function apply(doc: Doc, ...ops: Op[]): Doc { + return produce(doc, (doc) => { + doc.ops.push(...ops); + }); +} - if (maybeTypeOps) maybeTypeOps.push(op); - else opsByType.set(op.constructor, [op]); - } +export type DocIndex = Immutable<{ + opsByType: Map<Op["type"], Op[]>; +}>; - this.opsByKey = opsByKey; - this.opsByType = opsByType; - } +export function indexDoc(doc: Doc): DocIndex { + const opsByType = new Map(); - static default(): Doc { - return new Doc([new CreateGridOp()]); + for (const op of doc.ops) { + opsByType.set(op.type, [...(opsByType.get(op.type) ?? []), op]); } - get grids(): Grid[] { - const ops = this.opsByType.get(CreateGridOp) ?? []; - return ops.map((op) => new Grid(this, op as CreateGridOp)); - } + return { opsByType }; } -export class Grid { - readonly doc: Doc; - readonly id: string; - readonly rows: readonly Row[]; +export function getOpsByType<T extends Op["type"]>( + index: DocIndex, + type: T, +): Extract<Op, { type: T }>[] { + return (index.opsByType.get(type) ?? []) as Extract<Op, { type: T }>[]; +} - constructor(doc: Doc, createOp: CreateGridOp) { - this.doc = doc; - this.id = createOp.gridId; +export type Grid = Immutable<{ id: string; rows: Row[] }>; - const rows: Row[] = []; +export type Row = Immutable<{ index: number; cells: Cell[] }>; - for (let rowIndex = 0; rowIndex < createOp.rows; rowIndex++) { - rows.push(new Row(doc, this.id, rowIndex)); - } +export type Cell = Immutable<{}>; - this.rows = rows; - } +export function realizeGrids(doc: Doc): Grid[] { + const index = indexDoc(doc); + const createGridOps = getOpsByType(index, "createGrid"); + return createGridOps.map((op) => realizeGrid(doc, index, op)); } -export class Row { - readonly doc: Doc; - readonly gridId: string; - readonly index: number; - readonly cells: readonly Cell[]; - - constructor(doc: Doc, gridId: string, index: number) { - this.doc = doc; - this.gridId = gridId; - this.index = index; - - const createGridOp = doc.opsByType - .get(CreateGridOp) - ?.find((op) => op.key === gridId)! as CreateGridOp; - - const cells: Cell[] = []; - - for ( - let cellIndex = 0; - cellIndex < createGridOp.baseCellsPerRow; - cellIndex++ - ) { - cells.push(new Cell(doc, gridId, index, cellIndex)); +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 }); } - this.cells = cells; + 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 }); } -} -export class Cell { - readonly doc: Doc; - readonly gridId: string; - readonly rowIndex: number; - readonly index: number; - - constructor(doc: Doc, gridId: string, rowIndex: number, index: number) { - this.doc = doc; - this.gridId = gridId; - this.rowIndex = rowIndex; - this.index = index; - } + return { + id: createOp.gridId, + rows, + }; } |
