Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add shape searcher #635

Merged
merged 4 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions monosketch-svelte/src/lib/mono/shape/searcher/shape-searcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2024, tuanchauict
*/

import { Direction, type Point } from "$libs/graphics-geo/point";
import type { Rect } from "$libs/graphics-geo/rect";
import { isTransparentChar } from "$mono/common/character";
import { MonoBitmap } from "$mono/monobitmap/bitmap/monobitmap";
import { ShapeZoneAddressManager } from "$mono/shape/searcher/shape-zone-address-manager";
import { ZoneOwnersManager } from "$mono/shape/searcher/zone-address";
import type { ShapeManager } from "$mono/shape/shape-manager";
import type { AbstractShape } from "$mono/shape/shape/abstract-shape";
import { Rectangle } from "$mono/shape/shape/rectangle";

/**
* A model class which optimizes shapes retrieval from a point.
* A shape is only indexed after it's drawn onto the board. Do not use this for pre-draw check.
*/
export class ShapeSearcher {
private shapeZoneAddressManager: ShapeZoneAddressManager;
private zoneOwnersManager: ZoneOwnersManager;

constructor(
private shapeManager: ShapeManager,
private getBitmap: (shape: AbstractShape) => MonoBitmap.Bitmap | null,
) {
this.shapeZoneAddressManager = new ShapeZoneAddressManager(getBitmap);
this.zoneOwnersManager = new ZoneOwnersManager();
}

register(shape: AbstractShape) {
const addresses = this.shapeZoneAddressManager.getZoneAddresses(shape);
this.zoneOwnersManager.registerOwnerAddresses(shape.id, addresses);
}

clear(bound: Rect) {
this.zoneOwnersManager.clear(bound);
}

/**
* Returns a list of shapes which have a non-transparent pixel at [point].
* The order of the list is based on z-index which 1st item is the lowest index.
* Note: Group shape is not included in the result.
*/
getShapes(point: Point): AbstractShape[] {
return this.zoneOwnersManager.getPotentialOwners(point)
.map(ownerId => this.shapeManager.getShape(ownerId))
.filter((shape): shape is AbstractShape => !!shape)
.filter(shape => {
const position = shape.bound.position;
const bitmap = this.getBitmap(shape);
if (!bitmap) {
return false;
}
const bitmapRow = point.row - position.row;
const bitmapCol = point.column - position.column;
const isTransparent = isTransparentChar(bitmap.getVisual(bitmapRow, bitmapCol));
return !isTransparent;
});
}

getAllShapesInZone(bound: Rect): AbstractShape[] {
const zoneOwners = Array.from(this.zoneOwnersManager.getAllPotentialOwnersInZone(bound));
return zoneOwners
.map(ownerId => this.shapeManager.getShape(ownerId))
.filter((shape): shape is AbstractShape => shape !== null)
.filter(shape => shape.isOverlapped(bound));
}

/**
* Gets the edge direction of a shape having bound's edges at [point].
* The considerable shape types are shapes having static bound such as [Text], [Rectangle].
* If there are many shapes satisfying the conditions, the first one will be used.
*/
getEdgeDirection(point: Point): Direction | null {
const shape = this.zoneOwnersManager.getPotentialOwners(point)
.map(ownerId => this.shapeManager.getShape(ownerId))
.filter((shape): shape is AbstractShape => shape !== null)
.filter(shape => shape instanceof Text || shape instanceof Rectangle)
.filter(shape => shape.contains(point) && !shape.isVertex(point))
.find(shape =>
shape.bound.left === point.left ||
shape.bound.right === point.left ||
shape.bound.top === point.top ||
shape.bound.bottom === point.top,
);

if (!shape) {
return null;
}
return shape.bound.left === point.left || shape.bound.right === point.left
? Direction.VERTICAL
: Direction.HORIZONTAL;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2024, tuanchauict
*/

import { MonoBitmap } from "$mono/monobitmap/bitmap/monobitmap";
import type { AbstractShape } from '$mono/shape/shape/abstract-shape';
import { ZoneAddressFactory, type ZoneAddress } from './zone-address';

interface VersionizedZoneAddresses {
version: number;
addresses: Set<ZoneAddress>;
}

/**
* A class to convert a shape to addresses which the shape have pixels on.
* This class also cache the addresses belong to the shape along with its version.
*/
export class ShapeZoneAddressManager {
private idToZoneAddressMap: Map<string, VersionizedZoneAddresses> = new Map();

constructor(private getBitmap: (shape: AbstractShape) => MonoBitmap.Bitmap | null) {
}

getZoneAddresses(shape: AbstractShape): Set<ZoneAddress> {
const cachedAddresses = this.getCachedAddresses(shape);
if (cachedAddresses) {
return cachedAddresses;
}

const bitmap = this.getBitmap(shape);
if (!bitmap) {
this.idToZoneAddressMap.delete(shape.id);
return new Set();
}

const position = shape.bound.position;
const addresses = new Set<ZoneAddress>();
for (let ir = 0; ir < bitmap.matrix.length; ir++) {
for (const { index } of bitmap.matrix[ir].asSequence()) {
const row = ir + position.row;
const col = index + position.column;
const address = ZoneAddressFactory.toAddress(row, col);
addresses.add(address);
}
}

const versionizedZoneAddresses = { version: shape.versionCode, addresses };
this.idToZoneAddressMap.set(shape.id, versionizedZoneAddresses);
return versionizedZoneAddresses.addresses;
}

private getCachedAddresses(shape: AbstractShape): Set<ZoneAddress> | null {
const cached = this.idToZoneAddressMap.get(shape.id);
return cached && cached.version === shape.versionCode ? cached.addresses : null;
}
}
106 changes: 106 additions & 0 deletions monosketch-svelte/src/lib/mono/shape/searcher/zone-address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2024, tuanchauict
*/

import type { Point } from "$libs/graphics-geo/point";
import type { Rect } from "$libs/graphics-geo/rect";

/**
* A data class to identify address of a zone
*/
export interface ZoneAddress {
readonly row: number;
readonly column: number;
}

/**
* A factory of [ZoneAddress].
*/
export class ZoneAddressFactory {
private static addressMap: Map<number, Map<number, ZoneAddress>> = new Map();

static {
for (let left = -4; left <= 20; left++) {
const columnMap = new Map<number, ZoneAddress>();
for (let top = -4; top <= 20; top++) {
columnMap.set(top, { row: top, column: left });
}
this.addressMap.set(left, columnMap);
}
}

static toAddress(row: number, column: number): ZoneAddress {
const rowAddressIndex = toAddressIndex(row);
const colAddressIndex = toAddressIndex(column);
return this.get(rowAddressIndex, colAddressIndex);
}

static get(rowAddressIndex: number, colAddressIndex: number): ZoneAddress {
let columnMap = this.addressMap.get(rowAddressIndex);
if (!columnMap) {
columnMap = new Map<number, ZoneAddress>();
this.addressMap.set(rowAddressIndex, columnMap);
}
let address = columnMap.get(colAddressIndex);
if (!address) {
address = { row: rowAddressIndex, column: colAddressIndex };
columnMap.set(colAddressIndex, address);
}
return address;
}
}

/**
* A class to store owners of a zone whose size = 8x8 to fast identify potential candidate
* owners of a position (row, column).
* Owners of a zone will be stored in the same order as owners are added if overlapped.
*/
export class ZoneOwnersManager {
private zoneToOwnersMap: Map<ZoneAddress, string[]> = new Map();

clear(clearBound: Rect): void {
const leftIndex = toAddressIndex(clearBound.left);
const rightIndex = toAddressIndex(clearBound.right);
const topIndex = toAddressIndex(clearBound.top);
const bottomIndex = toAddressIndex(clearBound.bottom);

for (let rowIndex = topIndex; rowIndex <= bottomIndex; rowIndex++) {
for (let colIndex = leftIndex; colIndex <= rightIndex; colIndex++) {
const zone = ZoneAddressFactory.get(rowIndex, colIndex);
this.zoneToOwnersMap.get(zone)?.splice(0);
}
}
}

registerOwnerAddresses(ownerId: string, addresses: Set<ZoneAddress>): void {
for (const address of addresses) {
if (!this.zoneToOwnersMap.has(address)) {
this.zoneToOwnersMap.set(address, []);
}
this.zoneToOwnersMap.get(address)!.push(ownerId);
}
}

getPotentialOwners(point: Point): string[] {
const address = ZoneAddressFactory.toAddress(point.row, point.column);
return this.zoneToOwnersMap.get(address) || [];
}

getAllPotentialOwnersInZone(zone: Rect): Set<string> {
const address1 = ZoneAddressFactory.toAddress(zone.top, zone.left);
const address2 = ZoneAddressFactory.toAddress(zone.bottom, zone.right);
const owners = new Set<string>();

for (let addressRow = address1.row; addressRow <= address2.row; addressRow++) {
for (let addressCol = address1.column; addressCol <= address2.column; addressCol++) {
const address = ZoneAddressFactory.get(addressRow, addressCol);
const zoneOwners = this.zoneToOwnersMap.get(address) || [];
zoneOwners.forEach(owner => owners.add(owner));
}
}

return owners;
}
}

const toAddressIndex = (number: number): number => number >> 4;
Loading