diff options
Diffstat (limited to 'web/src')
33 files changed, 0 insertions, 1757 deletions
diff --git a/web/src/components/app/index.css b/web/src/components/app/index.css deleted file mode 100644 index aaf2ced..0000000 --- a/web/src/components/app/index.css +++ /dev/null @@ -1,12 +0,0 @@ -ntv-app { - display: block; - padding: 1.5rem; -} - -ntv-app > ntv-toolbar { - margin-bottom: 1.5rem; -} - -ntv-app > ntv-grid + ntv-grid { - margin-top: 1.5rem; -} diff --git a/web/src/components/app/index.ts b/web/src/components/app/index.ts deleted file mode 100644 index a2c0c9d..0000000 --- a/web/src/components/app/index.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { produce } from "immer"; -import defaultDoc from "../../defaultDoc"; -import NotiveElement, { customElement } from "../../element"; -import { - changeSelectedSubdivisions, - getSelectedSubdivisionsCount, -} from "../../grid"; -import { Doc } from "../../types"; -import ntvGrid, { NotiveGridElement } from "../grid"; -import renderGrid from "../grid/renderGrid"; -import { GridSelection } from "../grid/selection"; -import ntvToolbar from "../toolbar"; -import "./index.css"; - -@customElement("ntv-app") -export class NotiveAppElement extends NotiveElement { - doc: Doc = defaultDoc(); - - #selectedGridId?: string; - #selection?: GridSelection; - - setSelection(gridId: string, selection: GridSelection) { - const grid = this.doc.grids.find((grid) => grid.id === gridId); - if (!grid) throw new Error("Invalid grid ID"); - - this.#selectedGridId = gridId; - this.#selection = selection; - this.#updateGridSelections(); - - this.#toolbar.subdivisions = getSelectedSubdivisionsCount(grid, selection); - } - - clearSelection() { - this.#selectedGridId = undefined; - this.#selection = undefined; - this.#updateGridSelections(); - this.#toolbar.subdivisions = undefined; - } - - #updateGridSelections() { - this.querySelectorAll<NotiveGridElement>("ntv-grid").forEach((grid) => { - grid.selection = - this.#selectedGridId === grid.grid?.id ? this.#selection : undefined; - }); - } - - #toolbar = ntvToolbar({ - onsubdivisionschange: ({ subdivisions }) => { - if (!subdivisions) return; - - const gridId = this.#selectedGridId; - const selection = this.#selection; - - if (!gridId || !selection) return; - - const gridIndex = this.doc.grids.findIndex((grid) => grid.id === gridId); - - this.doc = produce(this.doc, (doc) => { - doc.grids[gridIndex] = changeSelectedSubdivisions( - this.doc.grids[gridIndex], - selection, - subdivisions, - ); - }); - - this.querySelector<NotiveGridElement>( - `ntv-grid[data-grid-id="${gridId}"]`, - )!.grid = renderGrid(this.doc.grids[gridIndex]); - - this.clearSelection(); - }, - }); - - connectedCallback() { - this.append( - this.#toolbar, - ...this.doc.grids.map((grid) => - ntvGrid({ - grid: renderGrid(grid), - dataset: { gridId: grid.id }, - ongridselectionchange: (event) => { - this.setSelection(grid.id, event.selection); - }, - oncellchange: (event) => { - console.log(event); - }, - }), - ), - ); - } -} - -export default NotiveAppElement.makeFactory(); diff --git a/web/src/components/grid/cellAtCoord.ts b/web/src/components/grid/cellAtCoord.ts deleted file mode 100644 index dd594a4..0000000 --- a/web/src/components/grid/cellAtCoord.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Coord from "../../math/Coord"; -import { CellRef } from "../../types"; -import { RenderedGrid, RenderedRow } from "./renderGrid"; - -function rowAtCoord(grid: RenderedGrid, coord: Coord): RenderedRow | undefined { - if (coord.y <= grid.rect.topLeft.y) { - return grid.renderedRows[0]; - } - - if (coord.y >= grid.rect.bottomRight.y) { - return grid.renderedRows.at(-1); - } - - return grid.renderedRows.find((row) => - row.rect.verticallyContainsCoord(coord), - ); -} - -export default function cellAtCoord( - grid: RenderedGrid, - x: number, - y: number, -): CellRef | undefined { - const coord = new Coord(x, y); - const row = rowAtCoord(grid, coord); - - if (!row) return; - - if (x <= row.rect.topLeft.x) { - return row.renderedCells[0]?.cellRef; - } - - if (x >= row.rect.bottomRight.x) { - return row.renderedCells.at(-1)?.cellRef; - } - - return row.renderedCells.find((cell) => - cell.rect.horizontallyContainsCoord(coord), - )?.cellRef; -} diff --git a/web/src/components/grid/drawGrid.ts b/web/src/components/grid/drawGrid.ts deleted file mode 100644 index da83c8e..0000000 --- a/web/src/components/grid/drawGrid.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { RangeSelection, Selection } from "../../selection"; -import { CellRef } from "../../types"; -import { getRenderedCell, RenderedCell, RenderedGrid } from "./renderGrid"; - -export interface GridStyles { - bgFill: string; - borderStroke: string; - cellStroke: string; - cellValueFont: string; - cellValueLineHeight: string; -} - -function excursion(ctx: CanvasRenderingContext2D, f: () => void) { - ctx.save(); - f(); - ctx.restore(); -} - -function fillBackground( - ctx: CanvasRenderingContext2D, - styles: GridStyles, - grid: RenderedGrid, -) { - ctx.clearRect(0, 0, grid.rect.width, grid.rect.height); - ctx.fillStyle = styles.bgFill; - ctx.fillRect(0, 0, grid.rect.width, grid.rect.height); -} - -function strokeGrid( - ctx: CanvasRenderingContext2D, - styles: GridStyles, - grid: RenderedGrid, -) { - ctx.strokeStyle = styles.borderStroke; - ctx.strokeRect(0.5, 0.5, grid.rect.width - 1, grid.rect.height - 1); -} - -function strokeGridLines( - ctx: CanvasRenderingContext2D, - styles: GridStyles, - grid: RenderedGrid, -) { - ctx.strokeStyle = styles.cellStroke; - - grid.renderedRows.forEach((row, renderedRowIndex) => { - const isLastRow = renderedRowIndex === grid.renderedRows.length - 1; - - row.renderedCells.forEach((cell, cellIndex) => { - const { topLeft, width, height } = cell.rect; - const isLastCell = cellIndex === row.renderedCells.length - 1; - - ctx.strokeRect( - topLeft.x + 0.5, - topLeft.y + 0.5, - isLastCell ? width - 1 : width, - isLastRow ? height - 1 : height, - ); - }); - }); -} - -function drawCellValues( - ctx: CanvasRenderingContext2D, - styles: GridStyles, - grid: RenderedGrid, -) { - grid.renderedRows.forEach((row) => - row.renderedCells.forEach((cell) => { - if (!cell.value) return; - ctx.fillStyle = "white"; - ctx.textAlign = "center"; - ctx.font = styles.cellValueFont; - ctx.fillText( - cell.value, - cell.rect.center.x, - cell.rect.center.y + parseInt(styles.cellValueLineHeight) / 4, - ); - }), - ); -} - -export default function drawGrid( - ctx: CanvasRenderingContext2D, - styles: GridStyles, - grid: RenderedGrid, -) { - excursion(ctx, () => fillBackground(ctx, styles, grid)); - excursion(ctx, () => strokeGridLines(ctx, styles, grid)); - excursion(ctx, () => strokeGrid(ctx, styles, grid)); - excursion(ctx, () => drawCellValues(ctx, styles, grid)); -} diff --git a/web/src/components/grid/drawSelection.ts b/web/src/components/grid/drawSelection.ts deleted file mode 100644 index 1b8c2ed..0000000 --- a/web/src/components/grid/drawSelection.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { CellRef } from "../../types"; -import excursion from "./excursion"; -import { getRenderedCell, RenderedCell, RenderedGrid } from "./renderGrid"; -import { GridSelection } from "./selection"; - -export interface SelectionStyles { - activeCellStroke: string; - selectionRangeFill: string; - selectionRangeStroke: string; -} - -function strokeActiveCell( - ctx: CanvasRenderingContext2D, - styles: SelectionStyles, - grid: RenderedGrid, - cell: RenderedCell, -) { - excursion(ctx, () => { - const isLastCell = cell.rect.bottomRight.x === grid.rect.bottomRight.x; - const isLastRow = cell.rect.bottomRight.y === grid.rect.bottomRight.y; - - ctx.strokeStyle = styles.activeCellStroke; - ctx.lineWidth = 2; - - ctx.strokeRect( - cell.rect.topLeft.x + 1, - cell.rect.topLeft.y + 1, - isLastCell ? cell.rect.width - 2 : cell.rect.width - 1, - isLastRow ? cell.rect.height - 2 : cell.rect.height - 1, - ); - }); -} - -function drawCellRange( - ctx: CanvasRenderingContext2D, - styles: SelectionStyles, - grid: RenderedGrid, - start: CellRef, - end: CellRef, - { stroke }: { stroke: boolean }, -) { - excursion(ctx, () => { - const startCell = getRenderedCell(grid, start); - const endCell = getRenderedCell(grid, end); - - if (!startCell || !endCell) return; - - const rect = startCell.rect.extend(endCell.rect); - - const isRightEdge = rect.bottomRight.x === grid.rect.bottomRight.x; - const isBottomEdge = rect.bottomRight.y === grid.rect.bottomRight.y; - - ctx.fillStyle = styles.selectionRangeFill; - - ctx.fillRect( - rect.topLeft.x + 1, - rect.topLeft.y + 1, - isRightEdge ? rect.width - 2 : rect.width - 1, - isBottomEdge ? rect.height - 2 : rect.height - 1, - ); - - if (!stroke) return; - - ctx.strokeStyle = styles.selectionRangeStroke; - - ctx.strokeRect( - rect.topLeft.x + 0.5, - rect.topLeft.y + 0.5, - isRightEdge ? rect.width - 1 : rect.width, - isBottomEdge ? rect.height - 1 : rect.height, - ); - }); -} - -export default function drawSelection( - ctx: CanvasRenderingContext2D, - styles: SelectionStyles, - grid: RenderedGrid, - selection: GridSelection | undefined, - { pending }: { pending: boolean }, -) { - ctx.clearRect(0, 0, grid.rect.width, grid.rect.height); - - if (!selection) return; - - const activeCell = getRenderedCell(grid, selection.activeCellRef); - - if (!activeCell) return; - - if (selection.range) { - drawCellRange(ctx, styles, grid, selection.range[0], selection.range[1], { - stroke: !pending, - }); - } - - strokeActiveCell(ctx, styles, grid, activeCell); -} diff --git a/web/src/components/grid/excursion.ts b/web/src/components/grid/excursion.ts deleted file mode 100644 index 7752df1..0000000 --- a/web/src/components/grid/excursion.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default function excursion( - ctx: CanvasRenderingContext2D, - f: () => void, -) { - ctx.save(); - f(); - ctx.restore(); -} diff --git a/web/src/components/grid/index.css b/web/src/components/grid/index.css deleted file mode 100644 index c29f55d..0000000 --- a/web/src/components/grid/index.css +++ /dev/null @@ -1,49 +0,0 @@ -@layer components { - ntv-grid { - display: block; - position: relative; - - --grid-bg-fill: var(--color-neutral-900); - --grid-border-stroke: var(--color-neutral-700); - --grid-cell-stroke: var(--color-neutral-800); - --grid-active-cell-stroke: var(--color-green-400); - --grid-selection-range-fill: color-mix( - in oklab, - var(--color-green-400) 10%, - transparent - ); - --grid-selection-range-stroke: var(--color-green-400); - font-size: 14px; - } - - ntv-grid > canvas { - display: block; - } - - ntv-grid > canvas[data-selection] { - position: absolute; - top: 0; - left: 0; - pointer-events: none; - } - - :has(ntv-grid:state(selecting)) - > ntv-grid:not(:state(selecting)) - > canvas[data-selection] { - display: none; - } - - ntv-grid input[data-edit] { - position: absolute; - vertical-align: baseline; - background: var(--color-neutral-800); - padding-right: 1px; - padding-bottom: 1px; - color: white; - text-align: center; - } - - ntv-grid input[data-edit]:focus-visible { - outline: none; - } -} diff --git a/web/src/components/grid/index.ts b/web/src/components/grid/index.ts deleted file mode 100644 index 3189409..0000000 --- a/web/src/components/grid/index.ts +++ /dev/null @@ -1,276 +0,0 @@ -import NotiveElement, { customElement, eventHandler } from "../../element"; -import h from "../../html"; -import { CellRef } from "../../types"; -import cellAtCoord from "./cellAtCoord"; -import drawGrid, { GridStyles } from "./drawGrid"; -import drawSelection, { SelectionStyles } from "./drawSelection"; -import "./index.css"; -import { getRenderedCell, RenderedGrid } from "./renderGrid"; -import { extendSelection, GridSelection } from "./selection"; - -@customElement("ntv-grid") -export class NotiveGridElement extends NotiveElement { - #internals: ElementInternals = this.attachInternals(); - - #grid?: RenderedGrid; - - get grid(): RenderedGrid | undefined { - return this.#grid; - } - - set grid(grid: RenderedGrid | undefined) { - this.#grid = grid; - this.draw(); - } - - #selection?: GridSelection; - - get selection() { - return this.#selection; - } - - set selection(selection: GridSelection | undefined) { - this.#selection = selection; - this.drawSelection(); - } - - @eventHandler("ntv:grid:selectionchange") - ongridselectionchange?: (event: GridSelectionChangeEvent) => any; - - @eventHandler("ntv:grid:cellchange") - oncellchange?: (event: GridCellChangeEvent) => any; - - canvas: HTMLCanvasElement = h.canvas({ - onmousedown: (event) => { - if (event.button !== 0) return; - if (!this.grid) return; - const cellRef = this.#mouseEventCellRef(event); - if (!cellRef) return; - this.startSelecting(cellRef); - }, - ondblclick: (event) => { - if (!this.grid) return; - const cellRef = this.#mouseEventCellRef(event); - if (!cellRef) return; - this.startEditing(cellRef); - }, - }); - - selectionCanvas: HTMLCanvasElement = h.canvas({ - dataset: { selection: "true" }, - }); - - connectedCallback() { - this.append(this.canvas, this.selectionCanvas); - this.draw(); - this.drawSelection(); - } - - draw() { - if (!this.grid) return; - - const ctx = this.canvas.getContext("2d"); - - if (!ctx) throw new Error("Unable to get canvas context"); - - this.canvas.setAttribute("width", this.grid.rect.width + "px"); - this.canvas.setAttribute("height", this.grid.rect.height + "px"); - - drawGrid(ctx, this.getGridStyles(), this.grid); - } - - drawSelection() { - if (!this.grid) return; - - const ctx = this.selectionCanvas.getContext("2d"); - - if (!ctx) throw new Error("Unable to get canvas context"); - - this.selectionCanvas.setAttribute("width", this.grid.rect.width + "px"); - this.selectionCanvas.setAttribute("height", this.grid.rect.height + "px"); - - drawSelection( - ctx, - this.getSelectionStyles(), - this.grid, - this.#pendingSelection ?? this.selection, - { - pending: !!this.#pendingSelection, - }, - ); - } - - getGridStyles(): GridStyles { - const style = window.getComputedStyle(this); - const val = (k: string) => style.getPropertyValue(k); - - return { - bgFill: val("--grid-bg-fill"), - borderStroke: val("--grid-border-stroke"), - cellStroke: val("--grid-cell-stroke"), - cellValueFont: val("font"), - cellValueLineHeight: val("line-height"), - }; - } - - getSelectionStyles(): SelectionStyles { - const style = window.getComputedStyle(this); - const val = (k: string) => style.getPropertyValue(k); - - return { - activeCellStroke: val("--grid-active-cell-stroke"), - selectionRangeFill: val("--grid-selection-range-fill"), - selectionRangeStroke: val("--grid-selection-range-stroke"), - }; - } - - #pendingSelection?: GridSelection; - #selectionAbortController?: AbortController; - - startSelecting(cellRef: CellRef) { - if (!this.grid || this.#pendingSelection) return; - - this.#internals.states.add("selecting"); - - this.#selectionAbortController = new AbortController(); - const { signal } = this.#selectionAbortController; - - window.addEventListener( - "mousemove", - (event) => { - const cellRef = this.#mouseEventCellRef(event); - if (!cellRef) return; - this.#pendingSelection = extendSelection( - this.#pendingSelection, - cellRef, - ); - this.drawSelection(); - }, - { signal }, - ); - - window.addEventListener("mouseup", () => this.#finishSelecting(), { - signal, - }); - - window.addEventListener( - "keydown", - (event) => { - event.preventDefault(); - if (event.key === "Escape") { - this.#pendingSelection = undefined; - this.#finishSelecting(); - } - }, - { signal }, - ); - - this.#pendingSelection = extendSelection(undefined, cellRef); - this.drawSelection(); - } - - #finishSelecting() { - this.#selectionAbortController?.abort(); - this.#selectionAbortController = undefined; - this.#internals.states.delete("selecting"); - if (this.#pendingSelection) { - this.dispatchEvent(new GridSelectionChangeEvent(this.#pendingSelection)); - } - this.#pendingSelection = undefined; - this.drawSelection(); - } - - #mouseEventCellRef( - this: NotiveGridElement, - event: MouseEvent, - ): CellRef | undefined { - if (!this.grid) return; - const clientRect = this.canvas.getBoundingClientRect(); - const x = event.x - clientRect.x; - const y = event.y - clientRect.y; - return cellAtCoord(this.grid, x, y); - } - - #editingCellRef?: CellRef; - - #editInput: HTMLInputElement = h.input({ - dataset: { edit: "true" }, - onblur: () => this.#finishEditing(), - onkeydown: (event) => { - switch (event.key) { - case "Enter": - this.#finishEditing(); - break; - - case "Escape": - this.#cancelEditing(); - break; - } - }, - }); - - startEditing(cellRef: CellRef) { - if (!this.grid) return; - - const cell = getRenderedCell(this.grid, cellRef); - - if (!cell) return; - - this.#editingCellRef = cellRef; - - this.append(this.#editInput); - - this.#editInput.value = cell.value || ""; - - Object.assign(this.#editInput.style, { - left: cell.rect.topLeft.x + 2 + "px", - top: cell.rect.topLeft.y + 2 + "px", - width: cell.rect.width - 3 + "px", - height: cell.rect.height - 3 + "px", - }); - - this.#editInput.focus(); - } - - #cancelEditing() { - this.#editInput.remove(); - } - - #finishEditing() { - this.#editInput.remove(); - - if (!this.grid || !this.#editingCellRef) return; - - this.dispatchEvent( - new GridCellChangeEvent(this.#editingCellRef, this.#editInput.value), - ); - } -} - -export default NotiveGridElement.makeFactory(); - -export class GridSelectionChangeEvent extends Event { - static readonly TYPE = "ntv:grid:selectionchange"; - - constructor(public selection: GridSelection) { - super(GridSelectionChangeEvent.TYPE); - } -} - -export class GridCellChangeEvent extends Event { - static readonly TYPE = "ntv:grid:cellchange"; - - constructor( - public cellRef: CellRef, - public value: string | undefined, - ) { - super(GridCellChangeEvent.TYPE); - } -} - -declare global { - interface HTMLElementEventMap { - [GridSelectionChangeEvent.TYPE]: GridSelectionChangeEvent; - [GridCellChangeEvent.TYPE]: GridCellChangeEvent; - } -} diff --git a/web/src/components/grid/renderGrid.ts b/web/src/components/grid/renderGrid.ts deleted file mode 100644 index 89938ec..0000000 --- a/web/src/components/grid/renderGrid.ts +++ /dev/null @@ -1,144 +0,0 @@ -import Ratio from "../../math/Ratio"; -import Rect from "../../math/Rect"; -import { Cell, CellRef, Grid, Row, RowRef } from "../../types"; - -export interface RenderedCell extends Cell { - cellRef: CellRef; - renderedRowIndex: number; - rect: Rect; - startRatio: Ratio; - endRatio: Ratio; -} - -export interface RenderedRow { - rowRef: RowRef; - rect: Rect; - renderedCells: RenderedCell[]; -} - -export interface RenderedGrid extends Grid { - rect: Rect; - renderedRows: RenderedRow[]; -} - -function renderCell( - grid: Grid, - cell: Cell, - cellRef: CellRef, - renderedRowIndex: number, - topLeftX: number, - topLeftY: number, - startRatio: Ratio, -): RenderedCell { - const width = cell.widthRatio - .divideRatio(grid.baseCellWidthRatio) - .multiplyRatio(Ratio.fromInteger(grid.baseCellSize)) - .toNumber(); - - const rect = new Rect(topLeftX, topLeftY, width, grid.baseCellSize); - - const endRatio = startRatio.add(cell.widthRatio); - - return { ...cell, cellRef, rect, renderedRowIndex, startRatio, endRatio }; -} - -function renderRow( - grid: Grid, - row: Row, - rowRef: RowRef, - renderedRowIndex: number, - topLeftY: number, -): RenderedRow { - if (row.cells.length === 0) { - return { - ...row, - rowRef, - rect: new Rect(0, topLeftY, 0, 0), - renderedCells: [], - }; - } - - let topLeftX = 0; - let startRatio = Ratio.fromInteger(0); - - const renderedCells = row.cells.map((cell, cellIndex) => { - const cellRef = { ...rowRef, cellIndex }; - - const renderedCell = renderCell( - grid, - cell, - cellRef, - renderedRowIndex, - topLeftX, - topLeftY, - startRatio, - ); - - topLeftX = renderedCell.rect.bottomRight.x; - startRatio = renderedCell.endRatio; - - return renderedCell; - }); - - const { topLeft } = renderedCells[0].rect; - const { bottomRight } = renderedCells.at(-1)!.rect; - - const rect = new Rect( - topLeft.x, - topLeft.y, - bottomRight.x - topLeft.x, - bottomRight.y - topLeft.y, - ); - - return { ...row, renderedCells, rect, rowRef }; -} - -function renderRows(grid: Grid): RenderedRow[] { - const renderedRows: RenderedRow[] = []; - - let partIndex = 0; - let rowIndex = 0; - let topLeftY = 0; - let renderedRowIndex = 0; - - while (true) { - if (!grid.parts[partIndex]?.rows[rowIndex]) break; - - const row = grid.parts[partIndex].rows[rowIndex]; - const rowRef = { partIndex, rowIndex }; - const renderedRow = renderRow( - grid, - row, - rowRef, - renderedRowIndex, - topLeftY, - ); - - topLeftY = renderedRow.rect.bottomRight.y; - renderedRows.push(renderedRow); - - if (!grid.parts[++partIndex]) { - partIndex = 0; - rowIndex++; - } - - renderedRowIndex++; - } - - return renderedRows; -} - -export default function renderGrid(grid: Grid) { - const renderedRows = renderRows(grid); - const rect = renderedRows[0].rect.extend(renderedRows.at(-1)!.rect); - return { ...grid, rect, renderedRows }; -} - -export function getRenderedCell( - grid: RenderedGrid, - cellRef: CellRef, -): RenderedCell | undefined { - const renderedRowIndex = - cellRef.rowIndex * grid.parts.length + cellRef.partIndex; - return grid.renderedRows[renderedRowIndex]?.renderedCells[cellRef.cellIndex]; -} diff --git a/web/src/components/grid/selection.ts b/web/src/components/grid/selection.ts deleted file mode 100644 index 517f8ae..0000000 --- a/web/src/components/grid/selection.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { CellRef, cellRefEquals } from "../../types"; -import { RenderedGrid } from "./renderGrid"; - -export type CellRange = [start: CellRef, end: CellRef]; - -export interface GridSelection { - activeCellRef: CellRef; - range?: CellRange; -} - -export function extendSelection( - selection: GridSelection | undefined, - cellRef: CellRef, -): GridSelection { - if (!selection || cellRefEquals(selection.activeCellRef, cellRef)) { - return { activeCellRef: cellRef }; - } - - if (selection.range) { - return { ...selection, range: [selection.range[0], cellRef] }; - } - - return { ...selection, range: [selection.activeCellRef, cellRef] }; -} - -export function getSelectionRange(selection: GridSelection): CellRange { - return selection.range ?? [selection.activeCellRef, selection.activeCellRef]; -} diff --git a/web/src/components/icons/index.ts b/web/src/components/icons/index.ts deleted file mode 100644 index 5731026..0000000 --- a/web/src/components/icons/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import plus16 from "./svgs/plus16.svg?raw"; -import minus16 from "./svgs/minus16.svg?raw"; - -function makeIconFactory(source: string) { - return (attrs?: object): SVGElement => { - const parser = new DOMParser(); - const doc = parser.parseFromString(source, "image/svg+xml"); - const svg = doc.documentElement as unknown as SVGElement; - - if (attrs) { - Object.entries(attrs).forEach(([k, v]) => svg.setAttribute(k, v)); - } - - return svg; - }; -} - -export const plus16Icon = makeIconFactory(plus16); -export const minus16Icon = makeIconFactory(minus16); diff --git a/web/src/components/icons/svgs/minus16.svg b/web/src/components/icons/svgs/minus16.svg deleted file mode 100644 index d77dcfc..0000000 --- a/web/src/components/icons/svgs/minus16.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4"> - <path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" /> -</svg> diff --git a/web/src/components/icons/svgs/plus16.svg b/web/src/components/icons/svgs/plus16.svg deleted file mode 100644 index 1d7b023..0000000 --- a/web/src/components/icons/svgs/plus16.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4"> - <path d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" /> -</svg> diff --git a/web/src/components/index.ts b/web/src/components/index.ts deleted file mode 100644 index b7f6f55..0000000 --- a/web/src/components/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import "./app"; -import "./grid"; -import "./toolbar"; diff --git a/web/src/components/toolbar/index.css b/web/src/components/toolbar/index.css deleted file mode 100644 index 653c326..0000000 --- a/web/src/components/toolbar/index.css +++ /dev/null @@ -1,48 +0,0 @@ -@layer components { - ntv-toolbar { - display: flex; - border-radius: 99999px; - background: var(--color-neutral-900); - width: min-content; - } - - ntv-toolbar > section { - display: flex; - gap: 0.25rem; - padding: 0.325rem; - } - - ntv-toolbar button { - border-radius: 99999px; - background: var(--color-neutral-800); - padding: 0 0.5rem; - height: 1.25rem; - color: white; - font-weight: 600; - font-size: 0.75rem; - } - - ntv-toolbar button:hover { - background: var(--color-green-400); - color: var(--color-neutral-900); - } - - ntv-toolbar button[data-icon] { - display: flex; - justify-content: center; - align-items: center; - aspect-ratio: 1; - height: 1.25rem; - } - - ntv-toolbar input { - border: 1px solid var(--color-neutral-700); - border-radius: 4px; - background: var(--color-neutral-900); - width: 2.5rem; - height: 1.25rem; - color: white; - font-size: 0.75rem; - text-align: center; - } -} diff --git a/web/src/components/toolbar/index.ts b/web/src/components/toolbar/index.ts deleted file mode 100644 index b8a383d..0000000 --- a/web/src/components/toolbar/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -import NotiveElement, { customElement, eventHandler } from "../../element"; -import h from "../../html"; -import { minus16Icon, plus16Icon } from "../icons"; -import "./index.css"; - -export class SubdivisionsChangeEvent extends Event { - static readonly TYPE = "ntv:toolbar:subdivisionschange"; - - constructor(public subdivisions: number | undefined) { - super(SubdivisionsChangeEvent.TYPE); - } -} - -@customElement("ntv-toolbar") -class NotiveToolbarElement extends NotiveElement { - #subdivisionsInputEl: HTMLInputElement = h.input({ - title: "Subdivisions", - disabled: true, - }); - - get subdivisions(): number | undefined { - if (this.#subdivisionsInputEl.value === "") return; - return parseInt(this.#subdivisionsInputEl.value); - } - - set subdivisions(n: number | undefined) { - const m = n && Math.max(n, 1); - this.#subdivisionsInputEl.value = m === undefined ? "" : m.toString(); - } - - @eventHandler(SubdivisionsChangeEvent.TYPE) - onsubdivisionschange?: (event: SubdivisionsChangeEvent) => any; - - connectedCallback() { - this.append( - h.section( - h.button( - { - dataset: { icon: "" }, - onclick: () => { - if (!this.subdivisions) return; - this.subdivisions = this.subdivisions - 1; - this.dispatchEvent( - new SubdivisionsChangeEvent(this.subdivisions), - ); - }, - }, - h.span(minus16Icon()), - ), - this.#subdivisionsInputEl, - h.button( - { - dataset: { icon: "" }, - onclick: () => { - if (!this.subdivisions) return; - this.subdivisions = this.subdivisions + 1; - this.dispatchEvent( - new SubdivisionsChangeEvent(this.subdivisions), - ); - }, - }, - h.span(plus16Icon()), - ), - ), - h.section(h.button("Play")), - ); - } -} - -export default NotiveToolbarElement.makeFactory(); diff --git a/web/src/defaultDoc.ts b/web/src/defaultDoc.ts deleted file mode 100644 index 0a3fbfb..0000000 --- a/web/src/defaultDoc.ts +++ /dev/null @@ -1,42 +0,0 @@ -import Ratio from "./math/Ratio"; -import { Cell, Doc } from "./types"; - -export default function defaultDoc(): Doc { - const defaultCells: Cell[] = Array.from({ length: 16 }, () => ({ - widthRatio: new Ratio(1, 16), - })); - - return { - grids: [ - { - id: globalThis.crypto.randomUUID(), - baseCellSize: 42, - baseCellWidthRatio: new Ratio(1, 16), - parts: [ - { - rows: Array.from({ length: 4 }, () => ({ - cells: [...defaultCells], - })), - }, - ], - }, - { - id: globalThis.crypto.randomUUID(), - baseCellSize: 42, - baseCellWidthRatio: new Ratio(1, 16), - parts: [ - { - rows: Array.from({ length: 2 }, () => ({ - cells: [...defaultCells], - })), - }, - { - rows: Array.from({ length: 2 }, () => ({ - cells: [...defaultCells], - })), - }, - ], - }, - ], - }; -} diff --git a/web/src/doc/index.test.ts b/web/src/doc/index.test.ts deleted file mode 100644 index 5f61398..0000000 --- a/web/src/doc/index.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index ae221f0..0000000 --- a/web/src/doc/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -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<Op["type"], Op[]>; -}>; - -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<T extends Op["type"]>( - index: DocIndex, - type: T, -): Extract<Op, { type: T }>[] { - return (index.opsByType.get(type) ?? []) as Extract<Op, { type: T }>[]; -} - -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, - }; -} diff --git a/web/src/element.ts b/web/src/element.ts deleted file mode 100644 index 6299d2f..0000000 --- a/web/src/element.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { createElement, type CreateElement } from "./html"; - -export default class NotiveElement extends HTMLElement { - static makeFactory<T extends NotiveElement>(this: { - new (): T; - }): CreateElement<T>; - - static makeFactory(): any { - throw new Error( - "Missing makeFactory implementation. Did you forget to use @customElement?", - ); - } -} - -export function customElement(tagName: string) { - return function (_value: unknown, context: ClassDecoratorContext) { - context.addInitializer(function () { - window.customElements.define(tagName, this as typeof NotiveElement); - (this as typeof NotiveElement).makeFactory = () => - ((...args: any[]) => createElement(tagName, ...args)) as CreateElement<any>; - }); - }; -} - -export function eventHandler(eventName: string) { - return function (_value: unknown, context: ClassFieldDecoratorContext) { - const privateKey = Symbol(context.name.toString()); - - context.addInitializer(function () { - Object.defineProperty(this, context.name, { - get() { - return this[privateKey]; - }, - set(handler) { - const oldHandler = this[privateKey]; - if (oldHandler) this.removeEventListener(eventName, oldHandler); - this[privateKey] = handler; - if (handler) this.addEventListener(eventName, handler); - }, - enumerable: true, - configurable: true, - }); - }); - }; -} diff --git a/web/src/favicon.ico b/web/src/favicon.ico Binary files differdeleted file mode 100644 index c10cfe9..0000000 --- a/web/src/favicon.ico +++ /dev/null diff --git a/web/src/grid.test.ts b/web/src/grid.test.ts deleted file mode 100644 index 50c0626..0000000 --- a/web/src/grid.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { expect, test } from "vitest"; -import defaultDoc from "./defaultDoc"; -import renderGrid from "./components/grid/renderGrid"; -import { changeSelectedSubdivisions } from "./grid"; -import { GridSelection } from "./components/grid/selection"; - -test("foo", () => { - const doc = defaultDoc(); - const grid = doc.grids[1]; - - const selection: GridSelection = { - activeCellRef: { partIndex: 0, rowIndex: 0, cellIndex: 0 }, - range: [ - { partIndex: 0, rowIndex: 0, cellIndex: 0 }, - { partIndex: 0, rowIndex: 0, cellIndex: 3 }, - ], - }; - - const newGrid = changeSelectedSubdivisions(grid, selection, 3); - const renderedGrid = renderGrid(newGrid); - - expect( - renderedGrid.renderedRows.map((row) => row.renderedCells.length), - ).toStrictEqual([15, 16, 16, 16]); - - expect( - newGrid.parts[0].rows[0].cells.map((cell) => cell.widthRatio.toData()), - ).toStrictEqual([ - [1, 12], - [1, 12], - [1, 12], - [1, 16], - [1, 16], - [1, 16], - [1, 16], - [1, 16], - [1, 16], - [1, 16], - [1, 16], - [1, 16], - [1, 16], - [1, 16], - [1, 16], - ]); -}); diff --git a/web/src/grid.ts b/web/src/grid.ts deleted file mode 100644 index e849803..0000000 --- a/web/src/grid.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { produce } from "immer"; -import renderGrid, { getRenderedCell } from "./components/grid/renderGrid"; -import { getSelectionRange, GridSelection } from "./components/grid/selection"; -import Ratio from "./math/Ratio"; -import { Cell, Grid, renderedRowIndexToRef } from "./types"; - -export function getSelectedSubdivisionsCount( - grid: Grid, - selection: GridSelection, -): number | undefined { - const renderedGrid = renderGrid(grid); - - const [startCellRef, endCellRef] = getSelectionRange(selection); - const startCell = getRenderedCell(renderedGrid, startCellRef); - const endCell = getRenderedCell(renderedGrid, endCellRef); - - if (!startCell || !endCell) throw new Error("Invalid cell refs"); - - const startRenderedRowIndex = Math.min( - startCell.renderedRowIndex, - endCell.renderedRowIndex, - ); - - const endRenderedRowIndex = Math.max( - startCell.renderedRowIndex, - endCell.renderedRowIndex, - ); - - const startRatio = Ratio.min(startCell.startRatio, endCell.startRatio); - const endRatio = Ratio.max(startCell.endRatio, endCell.endRatio); - - return Math.min( - ...renderedGrid.renderedRows - .slice(startRenderedRowIndex, endRenderedRowIndex + 1) - .map((row) => { - const startCellIndex = row.renderedCells.findIndex((cell) => - cell.startRatio.equals(startRatio), - ); - - const endCellIndex = row.renderedCells.findLastIndex((cell) => - cell.endRatio.equals(endRatio), - ); - - return endCellIndex - startCellIndex + 1; - }), - ); -} - -export function changeSelectedSubdivisions( - grid: Grid, - selection: GridSelection, - subdivisions: number, -): Grid { - const renderedGrid = renderGrid(grid); - const [startCellRef, endCellRef] = getSelectionRange(selection); - const startCell = getRenderedCell(renderedGrid, startCellRef); - const endCell = getRenderedCell(renderedGrid, endCellRef); - if (!startCell || !endCell) throw new Error("Invalid cell refs"); - - const startRenderedRowIndex = Math.min( - startCell.renderedRowIndex, - endCell.renderedRowIndex, - ); - - const endRenderedRowIndex = Math.max( - startCell.renderedRowIndex, - endCell.renderedRowIndex, - ); - - const startRatio = Ratio.min(startCell.startRatio, endCell.startRatio); - const endRatio = Ratio.max(startCell.endRatio, endCell.endRatio); - const selectedWidthRatio = endRatio.subtract(startRatio); - const widthRatio = selectedWidthRatio.divideRatio( - Ratio.fromInteger(subdivisions), - ); - - return produce(grid, (draft) => { - for ( - let renderedRowIndex = startRenderedRowIndex; - renderedRowIndex <= endRenderedRowIndex; - renderedRowIndex++ - ) { - const renderedRow = renderedGrid.renderedRows[renderedRowIndex]; - - const startCellIndex = renderedRow.renderedCells.findIndex((cell) => - cell.startRatio.equals(startRatio), - ); - - const endCellIndex = renderedRow.renderedCells.findLastIndex((cell) => - cell.endRatio.equals(endRatio), - ); - - const { partIndex, rowIndex } = renderedRowIndexToRef( - grid, - renderedRowIndex, - ); - - const row = draft.parts[partIndex].rows[rowIndex]; - const previousCells = row.cells.slice(0, startCellIndex); - const nextCells = row.cells.slice(endCellIndex + 1); - - const newCells: Cell[] = Array.from({ length: subdivisions }, () => ({ - widthRatio, - })); - - row.cells = [...previousCells, ...newCells, ...nextCells]; - } - }); -} diff --git a/web/src/html.ts b/web/src/html.ts deleted file mode 100644 index 3fccda3..0000000 --- a/web/src/html.ts +++ /dev/null @@ -1,50 +0,0 @@ -export function createElement<T extends HTMLElement>( - tagName: string, - ...children: (Node | string)[] -): T; - -export function createElement<T extends HTMLElement>( - tagName: string, - attrs: Partial<T>, - ...children: (Node | string)[] -): T; - -export function createElement(tagName: string, ...args: any[]) { - const el = document.createElement(tagName); - - if (args[0]?.constructor === Object) { - const { dataset, style, ...attrs } = args.shift(); - Object.assign(el, attrs); - if (dataset) Object.assign(el.dataset, dataset); - if (style) Object.assign(el.style, style); - } - - el.append(...args.flat()); - - return el; -} - -export type CreateElement<T extends HTMLElement> = { - (...children: (Node | string)[]): T; - (attrs: Partial<T>, ...children: (Node | string)[]): T; -}; - -type ElementCreator = { - [K in keyof HTMLElementTagNameMap]: CreateElement<HTMLElementTagNameMap[K]>; -}; - -const h = new Proxy({} as ElementCreator, { - get: - (_, tagName: string) => - (...args: any[]) => { - return createElement(tagName, ...args); - }, -}); - -export default h; - -export function fragment(...children: (Node | string)[]): DocumentFragment { - const fragment = document.createDocumentFragment(); - fragment.append(...children); - return fragment; -} diff --git a/web/src/index.css b/web/src/index.css deleted file mode 100644 index f100378..0000000 --- a/web/src/index.css +++ /dev/null @@ -1,10 +0,0 @@ -@import "tailwindcss"; - -body { - background: var(--color-neutral-800); - user-select: none; -} - -*:focus-visible { - outline: 2px solid var(--color-green-400); -} diff --git a/web/src/index.html b/web/src/index.html deleted file mode 100644 index 9f8bcbf..0000000 --- a/web/src/index.html +++ /dev/null @@ -1,11 +0,0 @@ -<!doctype html> -<html lang="en"> - <head> - <meta charset="utf-8" /> - <title>Notive</title> - <link rel="stylesheet" href="index.css" /> - <link rel="icon" href="favicon.ico" /> - <script type="module" src="index.ts"></script> - </head> - <body></body> -</html> diff --git a/web/src/index.ts b/web/src/index.ts deleted file mode 100644 index 7842326..0000000 --- a/web/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import ntvApp from "./components/app"; - -document.body.append(ntvApp()); diff --git a/web/src/math/Coord.ts b/web/src/math/Coord.ts deleted file mode 100644 index db7ee6d..0000000 --- a/web/src/math/Coord.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** A coord on a grid whose origin is in the top left. */ -export default class Coord { - private readonly _x: number; - private readonly _y: number; - - constructor(x: number, y: number) { - this._x = x; - this._y = y; - } - - get x(): number { - return this._x; - } - - get y(): number { - return this._y; - } - - /** Get the squared distance of this point from the origin. */ - squaredDistanceFromOrigin(): number { - return this._x * this._x + this._y * this._y; - } -} diff --git a/web/src/math/Ratio.test.ts b/web/src/math/Ratio.test.ts deleted file mode 100644 index da6fef2..0000000 --- a/web/src/math/Ratio.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, test } from "vitest"; -import Ratio from "./Ratio"; - -describe(Ratio, () => { - describe(Ratio.prototype.add, () => { - test("returns fractions in simplest form", () => { - const a = Ratio.fromInteger(0); - const b = new Ratio(1, 4); - - const c = a.add(b); - expect(c.numerator).toBe(1); - expect(c.denominator).toBe(4); - - const d = c.add(b); - expect(d.numerator).toBe(1); - expect(d.denominator).toBe(2); - - const e = d.add(b); - expect(e.numerator).toBe(3); - expect(e.denominator).toBe(4); - - const f = e.add(b); - expect(f.numerator).toBe(1); - expect(f.denominator).toBe(1); - }); - }); -}); diff --git a/web/src/math/Ratio.ts b/web/src/math/Ratio.ts deleted file mode 100644 index e2a1fbf..0000000 --- a/web/src/math/Ratio.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { gcd } from "."; - -/** Serializable representation of a ratio. */ -export type RatioData = [numerator: number, denominator: number]; - -/** Representation of a ratio for performing fractional artithmetic. */ -export default class Ratio { - readonly #numerator: number; - readonly #denominator: number; - - get numerator(): number { - return this.#numerator; - } - - get denominator(): number { - return this.#denominator; - } - - constructor(numerator: number, denominator: number) { - if (!Number.isInteger(numerator) || !Number.isInteger(denominator)) { - throw new TypeError( - `Ratio must have integer parts: ${numerator} / ${denominator}`, - ); - } - - if (denominator === 0) { - throw new RangeError("Ratio demnominator cannot be zero"); - } - - const divisor = gcd(numerator, denominator); - - this.#numerator = numerator / divisor; - this.#denominator = denominator / divisor; - } - - multiplyRatio(other: Ratio): Ratio { - return new Ratio( - this.numerator * other.numerator, - this.denominator * other.denominator, - ); - } - - divideRatio(other: Ratio): Ratio { - return new Ratio( - this.numerator * other.denominator, - this.denominator * other.numerator, - ); - } - - add(other: Ratio): Ratio { - return new Ratio( - this.numerator * other.denominator + other.numerator * this.denominator, - this.denominator * other.denominator, - ); - } - - subtract(other: Ratio): Ratio { - return new Ratio( - this.numerator * other.denominator - other.numerator * this.denominator, - this.denominator * other.denominator, - ); - } - - compare(other: Ratio): number { - const left = this.numerator * other.denominator; - const right = other.numerator * this.denominator; - return left < right ? -1 : left > right ? 1 : 0; - } - - equals(other: Ratio): boolean { - return this.compare(other) === 0; - } - - toNumber(): number { - return this.numerator / this.denominator; - } - - toString(): string { - return `${this.numerator}/${this.denominator}`; - } - - [Symbol.for("nodejs.util.inspect.custom")](): string { - return `Ratio { ${this.numerator}/${this.denominator} }`; - } - - static fromInteger(n: number): Ratio { - return new Ratio(n, 1); - } - - toData(): RatioData { - return [this.numerator, this.denominator]; - } - - static fromData(ratio: RatioData): Ratio { - return new Ratio(ratio[0], ratio[1]); - } - - static min(...ratios: Ratio[]): Ratio { - return ratios.reduce((a, b) => (a.compare(b) <= 0 ? a : b)); - } - - static max(...ratios: Ratio[]): Ratio { - return ratios.reduce((a, b) => (a.compare(b) >= 0 ? a : b)); - } -} diff --git a/web/src/math/Rect.ts b/web/src/math/Rect.ts deleted file mode 100644 index f52a2f7..0000000 --- a/web/src/math/Rect.ts +++ /dev/null @@ -1,104 +0,0 @@ -import Coord from "./Coord"; - -/** A rectangle on a grid whose origin is in the top left. */ -export default class Rect { - private readonly _topLeft: Coord; - private readonly _width: number; - private readonly _height: number; - - constructor( - topLeftX: number, - topLeftY: number, - width: number, - height: number, - ) { - this._topLeft = new Coord(topLeftX, topLeftY); - this._width = width; - this._height = height; - } - - /** Width of this rectangle. */ - get width(): number { - return this._width; - } - - /** Height of this rectangle. */ - get height(): number { - return this._height; - } - - /** Coord of the top-left point of this rectangle. */ - get topLeft(): Coord { - return this._topLeft; - } - - /** Coord of the bottom-right point of this rectangle. */ - get bottomRight(): Coord { - return new Coord( - this._topLeft.x + this._width, - this._topLeft.y + this._height, - ); - } - - get center(): Coord { - return new Coord( - this.topLeft.x + (this.bottomRight.x - this.topLeft.x) / 2, - this.topLeft.y + (this.bottomRight.y - this.topLeft.y) / 2, - ); - } - - /** Determine if this rectangle contains the point at `coord`. */ - containsCoord(coord: Coord): boolean { - return ( - this.topLeft.x <= coord.x && - coord.x <= this.bottomRight.x && - this.topLeft.y <= coord.y && - coord.y <= this.bottomRight.y - ); - } - - verticallyContainsCoord(coord: Coord): boolean { - return this.topLeft.y <= coord.y && coord.y <= this.bottomRight.y; - } - - horizontallyContainsCoord(coord: Coord): boolean { - return this.topLeft.x <= coord.x && coord.x <= this.bottomRight.x; - } - - extend(other: Rect): Rect { - const topLeftX = Math.min( - this.topLeft.x, - this.bottomRight.x, - other.topLeft.x, - other.bottomRight.x, - ); - - const topLeftY = Math.min( - this.topLeft.y, - this.bottomRight.y, - other.topLeft.y, - other.bottomRight.y, - ); - - const bottomRightX = Math.max( - this.topLeft.x, - this.bottomRight.x, - other.topLeft.x, - other.bottomRight.x, - ); - - const bottomRightY = Math.max( - this.topLeft.y, - this.bottomRight.y, - other.topLeft.y, - other.bottomRight.y, - ); - - return new Rect( - topLeftX, - topLeftY, - bottomRightX - topLeftX, - bottomRightY - topLeftY, - ); - } -} diff --git a/web/src/math/index.ts b/web/src/math/index.ts deleted file mode 100644 index 70dbb67..0000000 --- a/web/src/math/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function gcd(a: number, b: number): number { - return b === 0 ? Math.abs(a) : gcd(b, a % b); -} diff --git a/web/src/types.ts b/web/src/types.ts deleted file mode 100644 index dc26c89..0000000 --- a/web/src/types.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Immutable } from "immer"; -import Ratio from "./math/Ratio"; - -export type Cell = Immutable<{ - value?: string; - widthRatio: Ratio; -}>; - -export interface Row { - cells: Cell[]; -} - -export interface Part { - title?: string; - rows: Row[]; -} - -export interface Grid { - id: string; - baseCellSize: number; - baseCellWidthRatio: Ratio; - parts: Part[]; -} - -export interface Doc { - grids: Grid[]; -} - -export interface RowRef { - partIndex: number; - rowIndex: number; -} - -export interface CellRef { - partIndex: number; - rowIndex: number; - cellIndex: number; -} - -export function cellRefEquals(a: CellRef, b: CellRef): boolean { - return ( - a.partIndex === b.partIndex && - a.rowIndex === b.rowIndex && - a.cellIndex === b.cellIndex - ); -} - -export function renderedRowIndexToRef( - grid: Grid, - renderedRowIndex: number, -): RowRef { - const partIndex = renderedRowIndex % grid.parts.length; - const rowIndex = Math.floor(renderedRowIndex / grid.parts.length); - return { partIndex, rowIndex }; -} |
