From d369b45b0dc534678493c8f803dccd8430009253 Mon Sep 17 00:00:00 2001 From: "Tuan Chau (Tuna)" Date: Wed, 21 Feb 2024 02:14:32 +0900 Subject: [PATCH 01/11] Implement pixel --- .../src/lib/mono/monobitmap/board/pixel.ts | 56 +++++++++++++++++-- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/pixel.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/pixel.ts index c6454dc4b..ea9fd73b9 100644 --- a/monosketch-svelte/src/lib/mono/monobitmap/board/pixel.ts +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/pixel.ts @@ -1,19 +1,63 @@ import type { Char } from '$libs/char'; +import { isHalfTransparentChar, isTransparentChar, TRANSPARENT_CHAR } from '$mono/common/character'; +import type { Comparable } from '$libs/comparable'; /** * An interface for the pixel of the Mono Bitmap. */ -export interface Pixel { - visualChar: Char; - directionChar: Char; - highlight: HighlightType; +export class Pixel implements Comparable { + static TRANSPARENT = new Pixel(); - isTransparent: boolean; + private visualCharInner: Char = TRANSPARENT_CHAR; + private directionCharInner: Char = TRANSPARENT_CHAR; + private highlightInner: HighlightType = HighlightType.NO; + + get isTransparent(): boolean { + const char = this.visualCharInner; + return isTransparentChar(char) || isHalfTransparentChar(char); + } + + get visualChar(): Char { + return this.visualCharInner; + } + + get directionChar(): Char { + return this.directionCharInner; + } + + get highlight(): HighlightType { + return this.highlightInner; + } + + set = (visualChar: Char, directionChar: Char, highlight: HighlightType) => { + this.visualCharInner = visualChar; + this.directionCharInner = directionChar; + this.highlightInner = highlight; + }; + + reset = () => { + this.visualCharInner = TRANSPARENT_CHAR; + this.directionCharInner = TRANSPARENT_CHAR; + this.highlightInner = HighlightType.NO; + }; + + toString = (): string => (this.isTransparent ? ' ' : this.visualChar); + + equals(other: unknown): boolean { + if (!(other instanceof Pixel)) { + return false; + } + const otherPixel = other as Pixel; + return ( + this.visualCharInner === otherPixel.visualCharInner && + this.highlightInner === otherPixel.highlightInner + ); + } } export enum HighlightType { NO, SELECTED, TEXT_EDITING, - LINE_CONNECT_FOCUSING + LINE_CONNECT_FOCUSING, } From 0fe89dad4ff65cfbeab92b83b83c9177ada13054 Mon Sep 17 00:00:00 2001 From: "Tuan Chau (Tuna)" Date: Fri, 23 Feb 2024 18:26:39 +0900 Subject: [PATCH 02/11] Add for creating list with size and values and destructuring point --- .../src/lib/libs/graphics-geo/point.ts | 8 ++++++++ monosketch-svelte/src/lib/libs/sequence.ts | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/monosketch-svelte/src/lib/libs/graphics-geo/point.ts b/monosketch-svelte/src/lib/libs/graphics-geo/point.ts index cb8c1cc78..e12f71c74 100644 --- a/monosketch-svelte/src/lib/libs/graphics-geo/point.ts +++ b/monosketch-svelte/src/lib/libs/graphics-geo/point.ts @@ -72,6 +72,14 @@ export class Point implements IPoint { return `${this.left}|${this.top}`; } + /** + * Returns an array [left, top]. + * This can be used for destructuring assignment. + */ + toArray(): [number, number] { + return [this.left, this.top]; + } + // TODO: implement serialize and deserialize } diff --git a/monosketch-svelte/src/lib/libs/sequence.ts b/monosketch-svelte/src/lib/libs/sequence.ts index 759fdc474..f3cb64a50 100644 --- a/monosketch-svelte/src/lib/libs/sequence.ts +++ b/monosketch-svelte/src/lib/libs/sequence.ts @@ -146,3 +146,16 @@ export function getOrNull(array: T[], index: number): T | null { export function getOrDefault(array: T[], index: number, defaultValue: T): T { return index >= 0 && index < array.length ? array[index] : defaultValue; } + +/** + * Create a list of the specified size with the specified value. + * @param size + * @param value + */ +export function list(size: number, value: () => T): T[] { + const result: T[] = []; + for (let i = 0; i < size; i++) { + result.push(value()); + } + return result; +} From 4bd5ba749a651563846511cfac03375e34ec30ab Mon Sep 17 00:00:00 2001 From: "Tuan Chau (Tuna)" Date: Fri, 23 Feb 2024 20:24:40 +0900 Subject: [PATCH 03/11] Replace forEachIndex with asSequence generator --- .../lib/mono/monobitmap/bitmap/monobitmap.ts | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/monosketch-svelte/src/lib/mono/monobitmap/bitmap/monobitmap.ts b/monosketch-svelte/src/lib/mono/monobitmap/bitmap/monobitmap.ts index 4e0976ded..8fb70e9f2 100644 --- a/monosketch-svelte/src/lib/mono/monobitmap/bitmap/monobitmap.ts +++ b/monosketch-svelte/src/lib/mono/monobitmap/bitmap/monobitmap.ts @@ -4,8 +4,12 @@ import { binarySearch, getOrNull, mapIndexedNotNull, zip } from '$libs/sequence' import { isHalfTransparentChar, isTransparentChar, TRANSPARENT_CHAR } from '$mono/common/character'; import { Rect } from '$libs/graphics-geo/rect'; -namespace MonoBitmap { - export class MonoBitmap { +export namespace MonoBitmap { + /** + * A model class to hold the look of a shape after drawing. + * Create new object via [Builder]. + */ + export class Bitmap { readonly size: Size; constructor(public matrix: Row[]) { @@ -71,7 +75,7 @@ namespace MonoBitmap { } } - fillBitmap(row: number, column: number, bitmap: MonoBitmap): void { + fillBitmap(row: number, column: number, bitmap: Bitmap): void { if (bitmap.isEmpty()) { return; } @@ -93,28 +97,28 @@ namespace MonoBitmap { const destVisual = this.visualMatrix[startRow + r]; const destDirection = this.directionMatrix[startRow + r]; - const updateCell = (index: number, visualChar: Char, directionChar: Char) => { + const sequence = src.asSequence(inStartCol, inStartCol + overlap.width); + + for (let [index, visual, direction] of sequence) { const destIndex = startCol + index; // visualChar from source is always not transparent (0) due to the optimization of Row - if (isApplicable(destVisual[destIndex], visualChar)) { - destVisual[startCol + index] = visualChar; + if (isApplicable(destVisual[destIndex], visual)) { + destVisual[startCol + index] = visual; } // TODO: Double check this condition - if (isApplicable(destDirection[destIndex], directionChar)) { - destDirection[startCol + index] = directionChar; + if (isApplicable(destDirection[destIndex], direction)) { + destDirection[startCol + index] = direction; } - }; - - src.forEachIndex(updateCell, inStartCol, inStartCol + overlap.width); + } } } - toBitmap(): MonoBitmap { + toBitmap(): Bitmap { const rows = this.visualMatrix.map( (chars, index) => new Row(chars, this.directionMatrix[index]), ); - return new MonoBitmap(rows); + return new Bitmap(rows); } } @@ -154,19 +158,19 @@ namespace MonoBitmap { ); } - forEachIndex( - callback: ForEachIndex, + *asSequence( fromIndex: number = 0, toExclusiveIndex: number = this.size, - ) { + ): Generator<[index: number, visual: Char, direction: Char]> { const foundLow = binarySearch(this.sortedCells, (cell) => cell.index - fromIndex); const low = foundLow >= 0 ? foundLow : -foundLow - 1; + for (let i = low; i < this.sortedCells.length; i++) { const cell = this.sortedCells[i]; if (cell.index >= toExclusiveIndex) { break; } - callback(cell.index, cell.visual, cell.direction); + yield [cell.index, cell.visual, cell.direction]; } } @@ -193,6 +197,4 @@ namespace MonoBitmap { public direction: Char, ) {} } - - type ForEachIndex = (index: number, visual: Char, direction: Char) => void; } From f39a82519121cf7d48512b992dec18f91cbc0114 Mon Sep 17 00:00:00 2001 From: "Tuan Chau (Tuna)" Date: Fri, 23 Feb 2024 20:25:18 +0900 Subject: [PATCH 04/11] Implement painter board --- .../lib/mono/monobitmap/board/cross-point.ts | 22 ++ .../monobitmap/board/crosssing-resources.ts | 8 + .../mono/monobitmap/board/painter-board.ts | 217 ++++++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 monosketch-svelte/src/lib/mono/monobitmap/board/cross-point.ts create mode 100644 monosketch-svelte/src/lib/mono/monobitmap/board/crosssing-resources.ts create mode 100644 monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/cross-point.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/cross-point.ts new file mode 100644 index 000000000..300e55666 --- /dev/null +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/cross-point.ts @@ -0,0 +1,22 @@ +import type { Char } from '$libs/char'; + +/** + * A data class that stores information of a cross point when drawing a bitmap with + * [PainterBoard]. + * CrossPoint will then be drawn to the board after non-crossing pixels are drawn. + * + * @property [boardRow] and [boardColumn] are the location of point on the board. + * @param [visualChar] is the character at the crossing point + * @param [leftChar], [rightChar], [topChar], and [bottomChar] are 4 characters around the + * crossing point + */ +export interface CrossPoint { + boardRow: number; + boardColumn: number; + visualChar: Char; + directionChar: Char; + leftChar: Char; + rightChar: Char; + topChar: Char; + bottomChar: Char; +} diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/crosssing-resources.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/crosssing-resources.ts new file mode 100644 index 000000000..8563908df --- /dev/null +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/crosssing-resources.ts @@ -0,0 +1,8 @@ +import type { Char } from '$libs/char'; + +// TODO: Implement the connectable characters. +const CONNECTABLE_CHARS: Set = new Set(); + +export const isConnectableChar = (char: string): boolean => { + return CONNECTABLE_CHARS.has(char); +} diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts new file mode 100644 index 000000000..13ff2e443 --- /dev/null +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts @@ -0,0 +1,217 @@ +import { HighlightType, Pixel } from '$mono/monobitmap/board/pixel'; +import { Rect } from '$libs/graphics-geo/rect'; +import { list } from '$libs/sequence'; +import { Size } from '$libs/graphics-geo/size'; +import type { Point } from '$libs/graphics-geo/point'; +import { MonoBitmap } from '$mono/monobitmap/bitmap/monobitmap'; +import { isHalfTransparentChar } from '$mono/common/character'; +import type { Char } from '$libs/char'; +import { isConnectableChar } from '$mono/monobitmap/board/crosssing-resources'; +import type { CrossPoint } from '$mono/monobitmap/board/cross-point'; + +/** + * A model class to manage drawn pixel. + * This is where a pixel is represented with its absolute position. + */ +class PainterBoard { + private readonly matrix: Pixel[][]; + + constructor(private bound: Rect) { + this.matrix = list(bound.height, () => list(bound.width, () => new Pixel())); + } + + clear = () => { + for (let row of this.matrix) { + for (let pixel of row) { + pixel.reset(); + } + } + }; + + /** + * Fills with another [PainterBoard]. + * If a pixel in input [PainterBoard] is transparent, the value in the current board at that + * position won't be overwritten. + */ + fill = (board: PainterBoard) => { + if (this.matrix.length == 0 || this.matrix[0].length == 0) { + return; + } + + const position = board.bound.position; + const inMatrix = board.matrix; + + const inMatrixBound = new Rect(position, Size.of(inMatrix[0].length, inMatrix.length)); + + const overlap = this.bound.getOverlappedRect(inMatrixBound); + if (!overlap) { + return; + } + const [startCol, startRow] = overlap.position.minus(this.bound.position).toArray(); + const [inStartCol, inStartRow] = overlap.position.minus(position).toArray(); + + for (let r = 0; r < overlap.height; r++) { + const src = inMatrix[inStartRow + r]; + const dest = this.matrix[startRow + r]; + + for (let c = 0; c < overlap.width; c++) { + const px = src[inStartCol + c]; + if (!px.isTransparent) { + dest[startCol + c].set(px.visualChar, px.directionChar, px.highlight); + } + } + } + }; + + /** + * Fills with a bitmap and the highlight state of that bitmap from [position] excepts crossing + * points. Connection point are the point having the char which is connectable + * ([CrossingResources.isConnectable]) and there is a character drawn at the position. + * A list of [CrossPoint] will be returned to let [MonoBoard] able to adjust and draw the + * adjusted character of the connection points. + * + * The main reason why it is required to let [MonoBoard] draws the connection points is the + * painter board cannot see the pixel outside its bound which is required to identify the final + * connection character. + * + * If a pixel in input [bitmap] is transparent, the value in the current board at that + * position won't be overwritten. + */ + fillBitmap = ( + position: Point, + bitmap: MonoBitmap.Bitmap, + highlight: HighlightType, + ): Array => { + if (bitmap.isEmpty()) { + return []; + } + const inMatrix = bitmap.matrix; + const inMatrixBound = new Rect(position, bitmap.size); + + const overlap = this.bound.getOverlappedRect(inMatrixBound); + if (!overlap) { + return []; + } + + const [startCol, startRow] = overlap.position.minus(this.bound.position).toArray(); + const [inStartCol, inStartRow] = overlap.position.minus(position).toArray(); + + const [boundRow, boundColumn] = this.bound.position.toArray(); + const crossPoints: CrossPoint[] = []; + + for (let r = 0; r < overlap.height; r++) { + const bitmapRow = inStartRow + r; + const painterRow = startRow + r; + const src = inMatrix[bitmapRow]; + const dest = this.matrix[painterRow]; + + const sequence = src.asSequence(inStartCol, inStartCol + overlap.width); + for (let [index, visual, direction] of sequence) { + const bitmapColumn = inStartCol + index; + const painterColumn = startCol + index; + const pixel = dest[painterColumn]; + + if (this.isApplicable(pixel, visual)) { + // Not drawing half transparent character + // (full transparent character is removed by bitmap) + if (!isHalfTransparentChar(visual)) { + pixel.set(visual, direction, highlight); + } + } else { + // Crossing points will be drawn after finishing drawing all pixels of the + // bitmap on the Mono Board. Each unit painter board does not have enough + // information to decide the value of the crossing point. + crossPoints.push({ + boardRow: painterRow + boundRow, + boardColumn: painterColumn + boundColumn, + visualChar: visual, + directionChar: direction, + leftChar: bitmap.getDirection(bitmapRow, bitmapColumn - 1), + rightChar: bitmap.getDirection(bitmapRow, bitmapColumn + 1), + topChar: bitmap.getDirection(bitmapRow - 1, bitmapColumn), + bottomChar: bitmap.getDirection(bitmapRow + 1, bitmapColumn), + }); + } + } + } + + return crossPoints; + }; + + /** + * Force values overlap with [rect] to be [char] regardless they are [TRANSPARENT_CHAR]. + * + * Note: This method is for testing only + */ + fillRect = (rect: Rect, char: Char, highlight: HighlightType) => { + const overlap = this.bound.getOverlappedRect(rect); + if (!overlap) { + return; + } + const [startCol, startRow] = overlap.position.minus(this.bound.position).toArray(); + + for (let r = 0; r < overlap.height; r++) { + const row = this.matrix[startRow + r]; + for (let c = 0; c < overlap.width; c++) { + const pixel = row[startCol + c]; + pixel.set(char, char, highlight); + } + } + }; + + /** + * Force value at [position] to be [char] with [highlight]. + * + * Note: This method is for testing only + */ + setPoint = (position: Point, char: Char, highlight: HighlightType) => { + const [left, top] = position.toArray(); + const columnIndex = left - this.bound.left; + const rowIndex = top - this.bound.top; + + if ( + columnIndex < 0 || + columnIndex >= this.bound.width || + rowIndex < 0 || + rowIndex >= this.bound.height + ) { + return; + } + this.matrix[rowIndex][columnIndex].set(char, char, highlight); + }; + + get = (left: number, top: number): Pixel | null => { + const columnIndex = left - this.bound.left; + const rowIndex = top - this.bound.top; + if ( + columnIndex < 0 || + columnIndex >= this.bound.width || + rowIndex < 0 || + rowIndex >= this.bound.height + ) { + return null; + } + return this.matrix[rowIndex][columnIndex]; + }; + + private isApplicable = (pixel: Pixel, visual: Char): boolean => { + if (pixel.isTransparent) { + return true; + } + if (pixel.visualChar === visual) { + return true; + } + return !isConnectableChar(pixel.visualChar); + }; + + toString = (): string => { + let result = ''; + for (let row of this.matrix) { + for (let pixel of row) { + result += pixel.toString(); + } + result += '\n'; + } + return result; + }; +} From b4e0118807657bb17638c1e31e1eea83adb3d3c2 Mon Sep 17 00:00:00 2001 From: "Tuan Chau (Tuna)" Date: Sat, 24 Feb 2024 12:52:58 +0900 Subject: [PATCH 05/11] Use object destruction instead of array destruction --- .../src/lib/libs/graphics-geo/point.ts | 8 ----- monosketch-svelte/src/lib/libs/sequence.ts | 11 ++++++ .../lib/mono/monobitmap/bitmap/monobitmap.ts | 17 +++++---- .../mono/monobitmap/board/painter-board.ts | 35 ++++++++++--------- 4 files changed, 37 insertions(+), 34 deletions(-) diff --git a/monosketch-svelte/src/lib/libs/graphics-geo/point.ts b/monosketch-svelte/src/lib/libs/graphics-geo/point.ts index e12f71c74..cb8c1cc78 100644 --- a/monosketch-svelte/src/lib/libs/graphics-geo/point.ts +++ b/monosketch-svelte/src/lib/libs/graphics-geo/point.ts @@ -72,14 +72,6 @@ export class Point implements IPoint { return `${this.left}|${this.top}`; } - /** - * Returns an array [left, top]. - * This can be used for destructuring assignment. - */ - toArray(): [number, number] { - return [this.left, this.top]; - } - // TODO: implement serialize and deserialize } diff --git a/monosketch-svelte/src/lib/libs/sequence.ts b/monosketch-svelte/src/lib/libs/sequence.ts index f3cb64a50..ffdba841b 100644 --- a/monosketch-svelte/src/lib/libs/sequence.ts +++ b/monosketch-svelte/src/lib/libs/sequence.ts @@ -159,3 +159,14 @@ export function list(size: number, value: () => T): T[] { } return result; } + +export namespace MapExt { + export function getOrPut(map: Map, key: K, value: () => V): V { + let result = map.get(key); + if (result === undefined) { + result = value(); + map.set(key, result); + } + return result; + } +} diff --git a/monosketch-svelte/src/lib/mono/monobitmap/bitmap/monobitmap.ts b/monosketch-svelte/src/lib/mono/monobitmap/bitmap/monobitmap.ts index 8fb70e9f2..7a6a47a78 100644 --- a/monosketch-svelte/src/lib/mono/monobitmap/bitmap/monobitmap.ts +++ b/monosketch-svelte/src/lib/mono/monobitmap/bitmap/monobitmap.ts @@ -97,18 +97,17 @@ export namespace MonoBitmap { const destVisual = this.visualMatrix[startRow + r]; const destDirection = this.directionMatrix[startRow + r]; - const sequence = src.asSequence(inStartCol, inStartCol + overlap.width); - - for (let [index, visual, direction] of sequence) { + for (let cell of src.asSequence(inStartCol, inStartCol + overlap.width)) { + const index = cell.index - inStartCol; const destIndex = startCol + index; // visualChar from source is always not transparent (0) due to the optimization of Row - if (isApplicable(destVisual[destIndex], visual)) { - destVisual[startCol + index] = visual; + if (isApplicable(destVisual[destIndex], cell.visual)) { + destVisual[startCol + index] = cell.visual; } // TODO: Double check this condition - if (isApplicable(destDirection[destIndex], direction)) { - destDirection[startCol + index] = direction; + if (isApplicable(destDirection[destIndex], cell.direction)) { + destDirection[startCol + index] = cell.direction; } } } @@ -161,7 +160,7 @@ export namespace MonoBitmap { *asSequence( fromIndex: number = 0, toExclusiveIndex: number = this.size, - ): Generator<[index: number, visual: Char, direction: Char]> { + ): Generator { const foundLow = binarySearch(this.sortedCells, (cell) => cell.index - fromIndex); const low = foundLow >= 0 ? foundLow : -foundLow - 1; @@ -170,7 +169,7 @@ export namespace MonoBitmap { if (cell.index >= toExclusiveIndex) { break; } - yield [cell.index, cell.visual, cell.direction]; + yield cell; } } diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts index 13ff2e443..95c188057 100644 --- a/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts @@ -13,13 +13,15 @@ import type { CrossPoint } from '$mono/monobitmap/board/cross-point'; * A model class to manage drawn pixel. * This is where a pixel is represented with its absolute position. */ -class PainterBoard { +export class PainterBoard { private readonly matrix: Pixel[][]; constructor(private bound: Rect) { this.matrix = list(bound.height, () => list(bound.width, () => new Pixel())); } + getBound = (): Rect => this.bound; + clear = () => { for (let row of this.matrix) { for (let pixel of row) { @@ -47,8 +49,8 @@ class PainterBoard { if (!overlap) { return; } - const [startCol, startRow] = overlap.position.minus(this.bound.position).toArray(); - const [inStartCol, inStartRow] = overlap.position.minus(position).toArray(); + const { left: startCol, top: startRow } = overlap.position.minus(this.bound.position); + const { left: inStartCol, top: inStartRow } = overlap.position.minus(position); for (let r = 0; r < overlap.height; r++) { const src = inMatrix[inStartRow + r]; @@ -93,10 +95,10 @@ class PainterBoard { return []; } - const [startCol, startRow] = overlap.position.minus(this.bound.position).toArray(); - const [inStartCol, inStartRow] = overlap.position.minus(position).toArray(); + const { left: startCol, top: startRow } = overlap.position.minus(this.bound.position); + const { left: inStartCol, top: inStartRow } = overlap.position.minus(position); + const { left: boundColumn, top: boundRow } = this.bound.position; - const [boundRow, boundColumn] = this.bound.position.toArray(); const crossPoints: CrossPoint[] = []; for (let r = 0; r < overlap.height; r++) { @@ -105,17 +107,17 @@ class PainterBoard { const src = inMatrix[bitmapRow]; const dest = this.matrix[painterRow]; - const sequence = src.asSequence(inStartCol, inStartCol + overlap.width); - for (let [index, visual, direction] of sequence) { + for (let cell of src.asSequence(inStartCol, inStartCol + overlap.width)) { + const index = cell.index - inStartCol; const bitmapColumn = inStartCol + index; const painterColumn = startCol + index; const pixel = dest[painterColumn]; - if (this.isApplicable(pixel, visual)) { + if (this.isApplicable(pixel, cell.visual)) { // Not drawing half transparent character // (full transparent character is removed by bitmap) - if (!isHalfTransparentChar(visual)) { - pixel.set(visual, direction, highlight); + if (!isHalfTransparentChar(cell.visual)) { + pixel.set(cell.visual, cell.direction, highlight); } } else { // Crossing points will be drawn after finishing drawing all pixels of the @@ -124,8 +126,8 @@ class PainterBoard { crossPoints.push({ boardRow: painterRow + boundRow, boardColumn: painterColumn + boundColumn, - visualChar: visual, - directionChar: direction, + visualChar: cell.visual, + directionChar: cell.direction, leftChar: bitmap.getDirection(bitmapRow, bitmapColumn - 1), rightChar: bitmap.getDirection(bitmapRow, bitmapColumn + 1), topChar: bitmap.getDirection(bitmapRow - 1, bitmapColumn), @@ -148,7 +150,7 @@ class PainterBoard { if (!overlap) { return; } - const [startCol, startRow] = overlap.position.minus(this.bound.position).toArray(); + const { left: startCol, top: startRow } = overlap.position.minus(this.bound.position); for (let r = 0; r < overlap.height; r++) { const row = this.matrix[startRow + r]; @@ -165,9 +167,8 @@ class PainterBoard { * Note: This method is for testing only */ setPoint = (position: Point, char: Char, highlight: HighlightType) => { - const [left, top] = position.toArray(); - const columnIndex = left - this.bound.left; - const rowIndex = top - this.bound.top; + const columnIndex = position.left - this.bound.left; + const rowIndex = position.top - this.bound.top; if ( columnIndex < 0 || From 1f4b9979bb620e35f87b1b5a8b96a2b3772a3fad Mon Sep 17 00:00:00 2001 From: "Tuan Chau (Tuna)" Date: Sat, 24 Feb 2024 12:57:11 +0900 Subject: [PATCH 06/11] Minor change --- monosketch-svelte/src/lib/libs/sequence.ts | 29 ++++++++++++------- .../mono/monobitmap/board/painter-board.ts | 3 +- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/monosketch-svelte/src/lib/libs/sequence.ts b/monosketch-svelte/src/lib/libs/sequence.ts index ffdba841b..138a84f6f 100644 --- a/monosketch-svelte/src/lib/libs/sequence.ts +++ b/monosketch-svelte/src/lib/libs/sequence.ts @@ -147,20 +147,29 @@ export function getOrDefault(array: T[], index: number, defaultValue: T): T { return index >= 0 && index < array.length ? array[index] : defaultValue; } -/** - * Create a list of the specified size with the specified value. - * @param size - * @param value - */ -export function list(size: number, value: () => T): T[] { - const result: T[] = []; - for (let i = 0; i < size; i++) { - result.push(value()); +export namespace ListExt { + /** + * Create a list of the specified size with the specified value. + * @param size + * @param value + */ + export function list(size: number, value: () => T): T[] { + const result: T[] = []; + for (let i = 0; i < size; i++) { + result.push(value()); + } + return result; } - return result; } export namespace MapExt { + /** + * Get the value from the map with the specified key. If the key is not found, put the value + * created by the specified function into the map and return it. + * @param map + * @param key + * @param value + */ export function getOrPut(map: Map, key: K, value: () => V): V { let result = map.get(key); if (result === undefined) { diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts index 95c188057..58091f0e4 100644 --- a/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts @@ -1,6 +1,5 @@ import { HighlightType, Pixel } from '$mono/monobitmap/board/pixel'; import { Rect } from '$libs/graphics-geo/rect'; -import { list } from '$libs/sequence'; import { Size } from '$libs/graphics-geo/size'; import type { Point } from '$libs/graphics-geo/point'; import { MonoBitmap } from '$mono/monobitmap/bitmap/monobitmap'; @@ -8,6 +7,8 @@ import { isHalfTransparentChar } from '$mono/common/character'; import type { Char } from '$libs/char'; import { isConnectableChar } from '$mono/monobitmap/board/crosssing-resources'; import type { CrossPoint } from '$mono/monobitmap/board/cross-point'; +import { ListExt } from '$libs/sequence'; +import list = ListExt.list; /** * A model class to manage drawn pixel. From 5e90cbb4113fa7554a6106d6428936b18371e881 Mon Sep 17 00:00:00 2001 From: "Tuan Chau (Tuna)" Date: Sun, 25 Feb 2024 08:09:03 +0900 Subject: [PATCH 07/11] Add crossing resources --- .../lib/mono/monobitmap/board/cross-point.ts | 6 +- .../monobitmap/board/crosssing-resources.ts | 104 +++++++++++++++++- .../mono/monobitmap/board/painter-board.ts | 3 +- 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/cross-point.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/cross-point.ts index 300e55666..be2dac25f 100644 --- a/monosketch-svelte/src/lib/mono/monobitmap/board/cross-point.ts +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/cross-point.ts @@ -5,9 +5,9 @@ import type { Char } from '$libs/char'; * [PainterBoard]. * CrossPoint will then be drawn to the board after non-crossing pixels are drawn. * - * @property [boardRow] and [boardColumn] are the location of point on the board. - * @param [visualChar] is the character at the crossing point - * @param [leftChar], [rightChar], [topChar], and [bottomChar] are 4 characters around the + * - [boardRow] and [boardColumn] are the location of point on the board. + * - [visualChar] is the character at the crossing point + * - [leftChar], [rightChar], [topChar], and [bottomChar] are 4 characters around the * crossing point */ export interface CrossPoint { diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/crosssing-resources.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/crosssing-resources.ts index 8563908df..bde148308 100644 --- a/monosketch-svelte/src/lib/mono/monobitmap/board/crosssing-resources.ts +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/crosssing-resources.ts @@ -1,8 +1,104 @@ import type { Char } from '$libs/char'; -// TODO: Implement the connectable characters. -const CONNECTABLE_CHARS: Set = new Set(); +const M_LEFT = 0b1; +const M_RIGHT = 0b10; +const M_TOP = 0b100; +const M_BOTTOM = 0b1000; +const M_VERTICAL = M_TOP | M_BOTTOM; +const M_HORIZONTAL = M_LEFT | M_RIGHT; -export const isConnectableChar = (char: string): boolean => { - return CONNECTABLE_CHARS.has(char); +const SINGLE_PAIRS: { [key: string]: string } = { + '─': '─━═', + '│': '│┃║', + '┐': '┐┓╗', + '┌': '┌┏╔', + '┘': '┘┛╝', + '└': '└┗╚', + '┬': '┬┳╦', + '┴': '┴┻╩', + '├': '├┣╠', + '┤': '┤┫╣', + '┼': '┼╋╬', +}; + +const extendChars = (key: string): string[] => + Array.from({ length: 3 }, (_, index) => getSingleKey(key, index)); + +const getSingleKey = (key: string, index: number): string => + key + .split('') + .map((char) => SINGLE_PAIRS[char]![index]) + .join(''); + +export namespace CrossingResources { + const STANDARDIZED_CHARS: { [key: string]: string } = { + '-': '─', + '|': '│', + '+': '┼', + '╮': '┐', + '╭': '┌', + '╯': '┘', + '╰': '└', + }; + + const CONNECTABLE_CHARS: Set = [...'─│┌└┐┘┬┴├┤┼'] + .map(extendChars) + .flat() + .reduce((acc, char) => new Set([...acc, char]), new Set()); + + const LEFT_IN_CHARS: Set = [...'─┌└┬┴├┼'] + .map(extendChars) + .flat() + .reduce((acc, char) => new Set([...acc, char]), new Set()); + + const RIGHT_IN_CHARS: Set = [...'─┐┘┬┴┤┼'] + .map(extendChars) + .flat() + .reduce((acc, char) => new Set([...acc, char]), new Set()); + + const TOP_IN_CHARS: Set = [...'│┌┐┬├┤┼'] + .map(extendChars) + .flat() + .reduce((acc, char) => new Set([...acc, char]), new Set()); + + const BOTTOM_IN_CHARS: Set = [...'│└┘┴├┤┼'] + .map(extendChars) + .flat() + .reduce((acc, char) => new Set([...acc, char]), new Set()); + + const standardize = (char: string): string => STANDARDIZED_CHARS[char] ?? char; + + export const isConnectable = (char: string): boolean => standardize(char) in CONNECTABLE_CHARS; + + export const hasLeft = (char: string): boolean => standardize(char) in LEFT_IN_CHARS; + + export const hasRight = (char: string): boolean => standardize(char) in RIGHT_IN_CHARS; + + export const hasTop = (char: string): boolean => standardize(char) in TOP_IN_CHARS; + + export const hasBottom = (char: string): boolean => standardize(char) in BOTTOM_IN_CHARS; + + export const isConnectableChar = (char: string): boolean => { + return CONNECTABLE_CHARS.has(char); + }; + + /** + * A utility method for creating a mark vector for in-directions. + */ + export function inDirectionMark( + hasLeft: boolean, + hasRight: boolean, + hasTop: boolean, + hasBottom: boolean, + ): number { + const leftMark = hasLeft ? M_LEFT : 0; + const rightMark = hasRight ? M_RIGHT : 0; + const topMark = hasTop ? M_TOP : 0; + const bottomMark = hasBottom ? M_BOTTOM : 0; + return leftMark | topMark | rightMark | bottomMark; + } + + const getDirectionMap = (char1: Char, char2: Char): Map | null => { + throw new Error('Not implemented'); + }; } diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts index 58091f0e4..a117ba219 100644 --- a/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts @@ -5,10 +5,11 @@ import type { Point } from '$libs/graphics-geo/point'; import { MonoBitmap } from '$mono/monobitmap/bitmap/monobitmap'; import { isHalfTransparentChar } from '$mono/common/character'; import type { Char } from '$libs/char'; -import { isConnectableChar } from '$mono/monobitmap/board/crosssing-resources'; import type { CrossPoint } from '$mono/monobitmap/board/cross-point'; import { ListExt } from '$libs/sequence'; import list = ListExt.list; +import { CrossingResources } from '$mono/monobitmap/board/crosssing-resources'; +import isConnectableChar = CrossingResources.isConnectableChar; /** * A model class to manage drawn pixel. From 2816175f2b58e44dedf3f657e171f67641c1e41a Mon Sep 17 00:00:00 2001 From: "Tuan Chau (Tuna)" Date: Sun, 25 Feb 2024 08:09:43 +0900 Subject: [PATCH 08/11] Implement MonoBoard except drawCrossingPoints --- .../src/lib/mono/monobitmap/board/board.ts | 140 +++++++++++++++++- 1 file changed, 137 insertions(+), 3 deletions(-) diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/board.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/board.ts index ef1b4122e..72a94f4c6 100644 --- a/monosketch-svelte/src/lib/mono/monobitmap/board/board.ts +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/board.ts @@ -1,8 +1,142 @@ -import type { Pixel } from '$mono/monobitmap/board/pixel'; +import { HighlightType, Pixel } from '$mono/monobitmap/board/pixel'; +import { PainterBoard } from '$mono/monobitmap/board/painter-board'; +import { Size } from '$libs/graphics-geo/size'; +import { Rect } from '$libs/graphics-geo/rect'; +import { MapExt } from '$libs/sequence'; +import { Point } from '$libs/graphics-geo/point'; +import { MonoBitmap } from '$mono/monobitmap/bitmap/monobitmap'; +import type { CrossPoint } from '$mono/monobitmap/board/cross-point'; + +const STANDARD_UNIT_SIZE = Size.of(16, 16); /** * An interface for the board of the Mono Bitmap. */ -export interface MonoBoard { - get(left: number, top: number): Pixel; +export class MonoBoard { + private readonly painterBoards: Map = new Map(); + private windowBound: Rect = Rect.ZERO; + + constructor(private readonly unitSize: Size = STANDARD_UNIT_SIZE) {} + + clearAndSetWindow = (bound: Rect) => { + this.windowBound = bound; + this.painterBoards.clear(); + const affectedBoards = this.getOrCreateOverlappedBoards(bound, false); + for (let board of affectedBoards) { + board.clear(); + } + }; + + fillBitmap = (position: Point, bitmap: MonoBitmap.Bitmap, highlight: HighlightType) => { + const rect = new Rect(position, bitmap.size); + const affectedBoards = this.getOrCreateOverlappedBoards(rect, true); + + const crossingPoints: CrossPoint[] = []; + for (const board of affectedBoards) { + crossingPoints.push(...board.fillBitmap(position, bitmap, highlight)); + } + + this.drawCrossingPoints(crossingPoints, highlight); + } + + private drawCrossingPoints = (crossingPoints: CrossPoint[], highlight: HighlightType) => { + // TODO: implement this method + } + + get(left: number, top: number): Pixel { + const address = this.toBoardAddress(left, top); + const board = this.painterBoards.get(address); + const pixel = board ? board.get(left, top) : null; + return pixel ? pixel : Pixel.TRANSPARENT; + } + + private getOrCreateOverlappedBoards = ( + rect: Rect, + isCreatedRequired: boolean, + ): PainterBoard[] => { + const affectedBoards: PainterBoard[] = []; + const { width: unitWidth, height: unitHeight } = this.unitSize; + + const leftIndex = Math.floor(rect.left / unitWidth); + const rightIndex = Math.floor(rect.right / unitWidth); + const topIndex = Math.floor(rect.top / unitHeight); + const bottomIndex = Math.floor(rect.bottom / unitHeight); + + for (let left = leftIndex; left <= rightIndex; left++) { + for (let top = topIndex; top <= bottomIndex; top++) { + const board = this.getOrCreateBoard( + left * unitWidth, + top * unitHeight, + isCreatedRequired, + ); + + if (board) { + affectedBoards.push(board); + } + } + } + + return affectedBoards; + }; + + private getOrCreateBoard = ( + left: number, + top: number, + isCreatedRequired: boolean, + ): PainterBoard | null => { + const address = this.toBoardAddress(left, top); + const board = isCreatedRequired + ? MapExt.getOrPut(this.painterBoards, address, () => this.createNewBoard(address)) + : this.painterBoards.get(address); + if (!board) { + return null; + } + return this.windowBound.isOverlapped(board.getBound()) ? board : null; + }; + + private createNewBoard = (address: BoardAddress): PainterBoard => { + const newBoardPosition = new Point( + address.columnIndex * this.unitSize.width, + address.rowIndex * this.unitSize.height, + ); + const bound = new Rect(newBoardPosition, this.unitSize); + return new PainterBoard(bound); + }; + + private toBoardAddress = (left: number, top: number): BoardAddress => { + const rowIndex = Math.floor(top / this.unitSize.height); + const columnIndex = Math.floor(left / this.unitSize.width); + return BoardAddressManager.get(rowIndex, columnIndex); + }; +} + +type BoardAddress = { + rowIndex: number; + columnIndex: number; +}; + +class BoardAddressManager { + private static readonly addressMap = new Map>(); + + static { + const addressMap = BoardAddressManager.addressMap; + for (let rowIndex = -4; rowIndex < 10; rowIndex++) { + addressMap.set(rowIndex, new Map()); + for (let columnIndex = -4; columnIndex < 16; columnIndex++) { + addressMap.get(rowIndex)!!.set(columnIndex, { rowIndex, columnIndex }); + } + } + } + + static get(rowIndex: number, columnIndex: number): BoardAddress { + const addressMap = BoardAddressManager.addressMap; + if (!addressMap.has(rowIndex)) { + addressMap.set(rowIndex, new Map()); + } + const row = addressMap.get(rowIndex)!!; + if (!row.has(columnIndex)) { + row.set(columnIndex, { rowIndex, columnIndex }); + } + return row.get(columnIndex)!!; + } } From 4c27ce8388090f431850658b4b8fff993b9e0ee3 Mon Sep 17 00:00:00 2001 From: "Tuan Chau (Tuna)" Date: Sun, 25 Feb 2024 13:15:22 +0900 Subject: [PATCH 09/11] Add test for painter boards and fix bitmap toString --- .../lib/mono/monobitmap/bitmap/monobitmap.ts | 6 ++-- .../mono/monobitmap/board/painter-board.ts | 13 +++---- .../monobitmap/board/painter-boards.test.ts | 34 +++++++++++++++++++ 3 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 monosketch-svelte/src/lib/mono/monobitmap/board/painter-boards.test.ts diff --git a/monosketch-svelte/src/lib/mono/monobitmap/bitmap/monobitmap.ts b/monosketch-svelte/src/lib/mono/monobitmap/bitmap/monobitmap.ts index 7a6a47a78..843d9fa1c 100644 --- a/monosketch-svelte/src/lib/mono/monobitmap/bitmap/monobitmap.ts +++ b/monosketch-svelte/src/lib/mono/monobitmap/bitmap/monobitmap.ts @@ -158,9 +158,11 @@ export namespace MonoBitmap { } *asSequence( - fromIndex: number = 0, - toExclusiveIndex: number = this.size, + fromIndexOptional?: number, + toExclusiveIndexOptional?: number, ): Generator { + const fromIndex = fromIndexOptional === undefined ? 0 : fromIndexOptional; + const toExclusiveIndex = toExclusiveIndexOptional === undefined ? this.size : toExclusiveIndexOptional; const foundLow = binarySearch(this.sortedCells, (cell) => cell.index - fromIndex); const low = foundLow >= 0 ? foundLow : -foundLow - 1; diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts index a117ba219..14781f0c6 100644 --- a/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/painter-board.ts @@ -208,13 +208,10 @@ export class PainterBoard { }; toString = (): string => { - let result = ''; - for (let row of this.matrix) { - for (let pixel of row) { - result += pixel.toString(); - } - result += '\n'; - } - return result; + return this.matrix.map(this.toRowString).join('\n'); }; + + private toRowString(row: Pixel[]): string { + return row.map((pixel) => pixel.toString()).join(''); + } } diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/painter-boards.test.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/painter-boards.test.ts new file mode 100644 index 000000000..0c11aad92 --- /dev/null +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/painter-boards.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from 'vitest'; +import { PainterBoard } from '$mono/monobitmap/board/painter-board'; +import { Rect } from '$libs/graphics-geo/rect'; +import { HighlightType } from '$mono/monobitmap/board/pixel'; +import { Point } from '$libs/graphics-geo/point'; +import { TRANSPARENT_CHAR } from '$mono/common/character'; + +describe('test painter-boards', () => { + test('fill inside a board', () => { + const board = new PainterBoard(Rect.byLTWH(0, 0, 4, 4)); + board.fillRect(Rect.byLTWH(1, 2, 3, 4), '#', HighlightType.NO); + expect(board.toString()).toEqual(' \n \n ###\n ###'); + }); + + test('fill outside a board', () => { + const board = new PainterBoard(Rect.byLTWH(1, 1, 4, 4)); + board.fillRect(Rect.byLTWH(0, 0, 5, 5), '#', HighlightType.NO); + expect(board.toString()).toEqual('####\n####\n####\n####'); + }); + + test('fill board', () => { + const board1 = new PainterBoard(Rect.byLTWH(0, 0, 4, 4)); + board1.fillRect(Rect.byLTWH(0, 0, 2, 2), 'a', HighlightType.NO); + board1.fillRect(Rect.byLTWH(0, 2, 2, 2), 'b', HighlightType.NO); + board1.fillRect(Rect.byLTWH(2, 0, 2, 2), 'c', HighlightType.NO); + board1.fillRect(Rect.byLTWH(2, 2, 2, 2), 'd', HighlightType.NO); + board1.setPoint(new Point(2, 1), TRANSPARENT_CHAR, HighlightType.NO); + + const board2 = new PainterBoard(Rect.byLTWH(1, 1, 3, 2)); + board2.setPoint(new Point(2, 1), 'x', HighlightType.NO); + board2.fill(board1); + expect(board2.toString()).toBe('axc\nbdd'); + }); +}); From 7b0f6b08cb4dbca3d447902174b0c74c28999b9e Mon Sep 17 00:00:00 2001 From: "Tuan Chau (Tuna)" Date: Sun, 25 Feb 2024 14:21:33 +0900 Subject: [PATCH 10/11] Add unit test for board --- .../src/lib/libs/graphics-geo/point.ts | 2 +- monosketch-svelte/src/lib/libs/string.test.ts | 24 +++ monosketch-svelte/src/lib/libs/string.ts | 27 ++++ .../lib/mono/monobitmap/board/board.test.ts | 143 ++++++++++++++++++ .../src/lib/mono/monobitmap/board/board.ts | 58 ++++++- .../src/lib/mono/monobitmap/board/index.ts | 2 + 6 files changed, 251 insertions(+), 5 deletions(-) create mode 100644 monosketch-svelte/src/lib/libs/string.test.ts create mode 100644 monosketch-svelte/src/lib/libs/string.ts create mode 100644 monosketch-svelte/src/lib/mono/monobitmap/board/board.test.ts create mode 100644 monosketch-svelte/src/lib/mono/monobitmap/board/index.ts diff --git a/monosketch-svelte/src/lib/libs/graphics-geo/point.ts b/monosketch-svelte/src/lib/libs/graphics-geo/point.ts index cb8c1cc78..a11635558 100644 --- a/monosketch-svelte/src/lib/libs/graphics-geo/point.ts +++ b/monosketch-svelte/src/lib/libs/graphics-geo/point.ts @@ -34,7 +34,7 @@ export class Point implements IPoint { public readonly top: number, ) { if (!(Number.isInteger(left) && Number.isInteger(top))) { - throw Error('location must be integer'); + throw Error(`location must be integer ${left} ${top}`); } } diff --git a/monosketch-svelte/src/lib/libs/string.test.ts b/monosketch-svelte/src/lib/libs/string.test.ts new file mode 100644 index 000000000..aeb81e488 --- /dev/null +++ b/monosketch-svelte/src/lib/libs/string.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from 'vitest'; +import { StringExt } from '$libs/string'; + +describe('StringExt', () => { + test('trimMargin', () => { + const input = ` + | 1 | + | 2 | + | 3 | + `; + const expected = ` 1 \n 2 \n 3 `; + expect(StringExt.trimMargin(input)).toStrictEqual(expected); + }); + + test('trimMargin with custom prefix', () => { + const input = ` + # 1 | + # 2 | + # 3 | + `; + const expected = ` 1 \n 2 \n 3 `; + expect(StringExt.trimMargin(input, '#')).toStrictEqual(expected); + }); +}); diff --git a/monosketch-svelte/src/lib/libs/string.ts b/monosketch-svelte/src/lib/libs/string.ts new file mode 100644 index 000000000..f4fbf025c --- /dev/null +++ b/monosketch-svelte/src/lib/libs/string.ts @@ -0,0 +1,27 @@ +export namespace StringExt { + /** + * Trims the margin of a string. + * + * The margin is defined by the first line that starts with a margin prefix. + * @param input + * @param marginPrefix The prefix of the margin. Default is `|`. + * @param trimEnd The end of the line. Default is true. + */ + export const trimMargin = ( + input: string, + marginPrefix: string = '|', + trimEnd: boolean = true, + ): string => { + const lines = input.split('\n'); + + const trimmedLines = lines.map((line) => { + const index = line.indexOf(marginPrefix); + if (index === -1) { + return null; + } + const endExcluded = trimEnd ? line.length - 1 : line.length; + return line.slice(index + marginPrefix.length, endExcluded); + }); + return trimmedLines.filter((line) => line !== null).join('\n'); + }; +} diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/board.test.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/board.test.ts new file mode 100644 index 000000000..4522517e5 --- /dev/null +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/board.test.ts @@ -0,0 +1,143 @@ +import { beforeEach, describe, expect, test } from 'vitest'; +import { MonoBoard } from '$mono/monobitmap/board/board'; +import { HighlightType, Pixel } from '$mono/monobitmap/board/pixel'; +import { Point } from '$libs/graphics-geo/point'; +import { Rect } from '$libs/graphics-geo/rect'; +import { StringExt } from '$libs/string'; +import trimMargin = StringExt.trimMargin; + +describe('MonoBoard', () => { + let target: MonoBoard; + + beforeEach(() => { + target = new MonoBoard(); + target.clearAndSetWindow(Rect.byLTWH(-100, -100, 200, 200)); + }); + + test('getSet', () => { + const points = [-48, -32, -18, -16, 0, 16, 18, 32, 48].map( + (value) => new Point(value, value), + ); + + points.forEach((point) => { + expect(target.get(point.left, point.top)).toBe(Pixel.TRANSPARENT); + }); + + const chars = '012345678'; + chars.split('').forEach((char, index) => { + target.setPoint(points[index], char, HighlightType.NO); + }); + chars.split('').forEach((char, index) => { + expect(target.getPoint(points[index]).visualChar).toBe(char); + }); + + expect(target.boardCount).toBe(7); + }); + + test('fillRect', () => { + target.fillRect(Rect.byLTWH(1, 1, 3, 3), 'A', HighlightType.NO); + + expect(target.toString()).toStrictEqual( + trimMargin(` + | | + | AAA | + | AAA | + | AAA | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + `), + ); + + expect(target.boardCount).toBe(1); + + target.fillRect(Rect.byLTWH(-3, -3, 3, 3), 'B', HighlightType.NO); + expect(target.toString()).toStrictEqual( + trimMargin(` + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | BBB | + | BBB | + | BBB | + | | + | AAA | + | AAA | + | AAA | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + `), + ); + + expect(target.boardCount).toBe(2); + + target.fillRect(Rect.byLTWH(-1, 0, 3, 1), 'C', HighlightType.NO); + expect(target.toString()).toStrictEqual( + trimMargin(` + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | BBB | + | BBB | + | BBB | + | CCC | + | AAA | + | AAA | + | AAA | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + `), + ); + + expect(target.boardCount).toBe(3); + }); +}); diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/board.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/board.ts index 72a94f4c6..8e0216c14 100644 --- a/monosketch-svelte/src/lib/mono/monobitmap/board/board.ts +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/board.ts @@ -6,6 +6,7 @@ import { MapExt } from '$libs/sequence'; import { Point } from '$libs/graphics-geo/point'; import { MonoBitmap } from '$mono/monobitmap/bitmap/monobitmap'; import type { CrossPoint } from '$mono/monobitmap/board/cross-point'; +import type { Char } from '$libs/char'; const STANDARD_UNIT_SIZE = Size.of(16, 16); @@ -16,6 +17,10 @@ export class MonoBoard { private readonly painterBoards: Map = new Map(); private windowBound: Rect = Rect.ZERO; + get boardCount(): number { + return this.painterBoards.size; + } + constructor(private readonly unitSize: Size = STANDARD_UNIT_SIZE) {} clearAndSetWindow = (bound: Rect) => { @@ -37,18 +42,38 @@ export class MonoBoard { } this.drawCrossingPoints(crossingPoints, highlight); - } + }; + + // This method is for testing only + fillRect = (rect: Rect, char: Char, highlight: HighlightType) => { + const affectedBoards = this.getOrCreateOverlappedBoards(rect, true); + for (const board of affectedBoards) { + board.fillRect(rect, char, highlight); + } + }; private drawCrossingPoints = (crossingPoints: CrossPoint[], highlight: HighlightType) => { // TODO: implement this method - } + }; + + getPoint = (position: Point): Pixel => { + return this.get(position.left, position.top); + }; - get(left: number, top: number): Pixel { + get = (left: number, top: number): Pixel => { const address = this.toBoardAddress(left, top); const board = this.painterBoards.get(address); const pixel = board ? board.get(left, top) : null; return pixel ? pixel : Pixel.TRANSPARENT; - } + }; + + set = (left: number, top: number, char: Char, highlight: HighlightType) => { + this.getOrCreateBoard(left, top, true)?.setPoint(new Point(left, top), char, highlight); + }; + + setPoint = (position: Point, char: Char, highlight: HighlightType) => { + this.set(position.left, position.top, char, highlight); + }; private getOrCreateOverlappedBoards = ( rect: Rect, @@ -108,6 +133,31 @@ export class MonoBoard { const columnIndex = Math.floor(left / this.unitSize.width); return BoardAddressManager.get(rowIndex, columnIndex); }; + + toString = (): string => { + if (this.painterBoards.size === 0) { + return ''; + } + const left = Math.min( + ...Array.from(this.painterBoards.keys(), (point) => point.columnIndex), + ); + const right = + Math.max(...Array.from(this.painterBoards.keys(), (point) => point.columnIndex)) + 1; + const top = Math.min(...Array.from(this.painterBoards.keys(), (point) => point.rowIndex)); + const bottom = + Math.max(...Array.from(this.painterBoards.keys(), (point) => point.rowIndex)) + 1; + const rect = Rect.byLTWH( + left * this.unitSize.width, + top * this.unitSize.height, + (right - left) * this.unitSize.width, + (bottom - top) * this.unitSize.height, + ); + const painterBoard = new PainterBoard(rect); + + Array.from(this.painterBoards.values()).forEach((pb) => painterBoard.fill(pb)); + + return painterBoard.toString(); + }; } type BoardAddress = { diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/index.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/index.ts new file mode 100644 index 000000000..ce38481ee --- /dev/null +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/index.ts @@ -0,0 +1,2 @@ +export { MonoBoard } from './board'; +export { Pixel } from './pixel'; From 4a8115fc7beda49f23ba60599e44c54fff51e5a9 Mon Sep 17 00:00:00 2001 From: "Tuan Chau (Tuna)" Date: Sun, 25 Feb 2024 15:08:47 +0900 Subject: [PATCH 11/11] Add a demo for new crossing point approach --- .../board/crossing-resources.test.ts | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 monosketch-svelte/src/lib/mono/monobitmap/board/crossing-resources.test.ts diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/crossing-resources.test.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/crossing-resources.test.ts new file mode 100644 index 000000000..cd1f2f7ba --- /dev/null +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/crossing-resources.test.ts @@ -0,0 +1,101 @@ +import { describe, test } from 'vitest'; + +// This file is for testing a new idea for identifying the crossing point. +// Currently, the crossing point is identified by a Database of characters but this is hard to +// maintain. +// The new idea uses masks to identify the crossing point. +// The mask is a 4-bit number that represents the direction of the crossing point and its 4 sides. +// 1 crossing point is created by 2 overlapping characters using `bit-or` operator. +// 4 sides identifies a mask with bit-or operator. +// The mask is created by the 2 characters `bit-and` their 4 surrounding characters. + +const surroundLeftChars = '─┌└┬┴├┼'; +const surroundRightChars = '─┐┘┬┴┤┼'; +const surroundTopChars = '│┌┐┬├┼'; +const surroundBottomChars = '│└┘┴┤┼'; + +const M_LEFT = 0b1; +const M_RIGHT = 0b10; +const M_TOP = 0b100; +const M_BOTTOM = 0b1000; +const M_VERTICAL = M_TOP | M_BOTTOM; +const M_HORIZONTAL = M_LEFT | M_RIGHT; + +const MASK_TO_CHAR: Map = (() => { + const result = new Map(); + result.set(M_HORIZONTAL, '─'); + result.set(M_VERTICAL, '│'); + result.set(M_LEFT | M_VERTICAL, '┤'); + result.set(M_RIGHT | M_VERTICAL, '├'); + result.set(M_HORIZONTAL | M_TOP, '┴'); + result.set(M_HORIZONTAL | M_BOTTOM, '┬'); + result.set(M_LEFT | M_TOP, '┘'); + result.set(M_LEFT | M_BOTTOM, '┐'); + result.set(M_RIGHT | M_TOP, '└'); + result.set(M_RIGHT | M_BOTTOM, '┌'); + result.set(M_HORIZONTAL | M_VERTICAL, '┼'); + return result; +})(); + +const identifySurroundMask = (left: string, right: string, top: string, bottom: string): number => { + let result = 0; + if (surroundLeftChars.includes(left)) { + result |= M_LEFT; + } + if (surroundRightChars.includes(right)) { + result |= M_RIGHT; + } + if (surroundTopChars.includes(top)) { + result |= M_TOP; + } + if (surroundBottomChars.includes(bottom)) { + result |= M_BOTTOM; + } + return result; +} + +const identifyInnerMask = (char: string): number => { + let result = 0; + // inner opposite to surround + if (surroundLeftChars.includes(char)) { + result |= M_RIGHT; + } + // inner opposite to surround + if (surroundRightChars.includes(char)) { + result |= M_LEFT; + } + // inner opposite to surround + if (surroundTopChars.includes(char)) { + result |= M_BOTTOM; + } + // inner opposite to surround + if (surroundBottomChars.includes(char)) { + result |= M_TOP; + } + return result; +} + +const directionMask = (char1: string, char2: string, left: string, right: string, top: string, bottom: string): number => { + const mask1 = identifyInnerMask(char1); + const mask2 = identifyInnerMask(char2); + console.log(toBinaryString(mask1), MASK_TO_CHAR.get(mask1), toBinaryString(mask2), MASK_TO_CHAR.get(mask2)); + const inner = mask1 | mask2; + const surround = identifySurroundMask(left, right, top, bottom); + console.log(toBinaryString(inner), MASK_TO_CHAR.get(inner), toBinaryString(surround)); + const result = inner & surround; + console.log(toBinaryString(result)); + return result; +} + +function toBinaryString(num: number): string { + const text = num.toString(2); + const missing = 8 - text.length; + return '0'.repeat(missing) + text; +} + +describe('CrossingResources', () => { + test('try new approach', () => { + const res = directionMask('─', '│', '─', '─', '│', '│'); + console.log(MASK_TO_CHAR.get(res)); + }); +});