Skip to content

Commit

Permalink
Merge pull request #614 from tuanchauict/group-js
Browse files Browse the repository at this point in the history
Port Group to TS
  • Loading branch information
tuanchauict authored Dec 9, 2024
2 parents 72fab44 + 5719e96 commit 2d52b4d
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 14 deletions.
20 changes: 10 additions & 10 deletions monosketch-svelte/src/lib/mono/shape/collection/quick-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ export class AddPosition {
*/
export class QuickList<T extends Identifier> implements Iterable<T> {
private linkedList: DoubleLinkedList<T> = new DoubleLinkedList<T>();
private map: Map<string, Node<T>> = new Map<string, Node<T>>();
private idToNodeMap: Map<string, Node<T>> = new Map<string, Node<T>>();

get size(): number {
return this.map.size;
return this.idToNodeMap.size;
}

contains(element: T): boolean {
return this.map.has(element.id);
return this.idToNodeMap.has(element.id);
}

containsAll(elements: Iterable<T>): boolean {
Expand Down Expand Up @@ -66,7 +66,7 @@ export class QuickList<T extends Identifier> implements Iterable<T> {
case AddPosition.First:
return this.linkedList.head;
default: // AddPosition after
return this.map.get(position.identifier!.id);
return this.idToNodeMap.get(position.identifier!.id);
}
})();

Expand All @@ -75,7 +75,7 @@ export class QuickList<T extends Identifier> implements Iterable<T> {
}

const node = this.linkedList.add(element, preNode);
this.map.set(element.id, node);
this.idToNodeMap.set(element.id, node);

return true;
}
Expand All @@ -89,32 +89,32 @@ export class QuickList<T extends Identifier> implements Iterable<T> {
}

remove(identifier: Identifier): T | null {
const node = this.map.get(identifier.id);
const node = this.idToNodeMap.get(identifier.id);
if (!node) {
return null;
}
this.map.delete(identifier.id);
this.idToNodeMap.delete(identifier.id);
this.linkedList.remove(node);
return node.value;
}

removeAll(): T[] {
const result = Array.from(this);
this.linkedList.clear();
this.map.clear();
this.idToNodeMap.clear();
return result;
}

get(id: string): T | null {
return this.map.get(id)?.value || null;
return this.idToNodeMap.get(id)?.value || null;
}

move(identifier: Identifier, moveActionType: MoveActionType): boolean {
if (this.size < 2) {
return false;
}

const node = this.map.get(identifier.id);
const node = this.idToNodeMap.get(identifier.id);
if (!node) {
return false;
}
Expand Down
119 changes: 119 additions & 0 deletions monosketch-svelte/src/lib/mono/shape/shape/group.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright (c) 2024, tuanchauict
*/

import { describe, it, expect, beforeEach } from 'vitest';
import { Rect } from '$libs/graphics-geo/rect';
import { AddPosition, MoveActionType } from '$mono/shape/collection/quick-list';
import { Group } from '$mono/shape/shape/group';
import { Rectangle } from '$mono/shape/shape/rectangle';

describe('Group', () => {
const PARENT_ID = "100";
let target: Group;

beforeEach(() => {
target = new Group(null, PARENT_ID);
});

it('testAdd', () => {
expect(target.itemCount).toBe(0);
const invalidShape = Rectangle.fromRect({ rect: Rect.ZERO, parentId: "10000" });
const validShape1 = Rectangle.fromRect({ rect: Rect.ZERO });
const validShape2 = Rectangle.fromRect({ rect: Rect.ZERO, parentId: target.id });
const validShape3 = Rectangle.fromRect({ rect: Rect.ZERO });

target.add(invalidShape);
expect(target.itemCount).toBe(0);
expect(Array.from(target.items)).toEqual([]);

target.add(validShape1);
expect(target.itemCount).toBe(1);
expect(validShape1.parentId).toBe(target.id);
expect(Array.from(target.items)).toEqual([validShape1]);

// Repeat adding existing object
target.add(validShape1);
expect(target.itemCount).toBe(1);
expect(validShape1.parentId).toBe(target.id);
expect(Array.from(target.items)).toEqual([validShape1]);

target.add(validShape2, AddPosition.First);
expect(target.itemCount).toBe(2);
expect(validShape2.parentId).toBe(target.id);
expect(Array.from(target.items)).toEqual([validShape2, validShape1]);

target.add(validShape3, AddPosition.After(validShape2));
expect(target.itemCount).toBe(3);
expect(validShape3.parentId).toBe(target.id);
expect(Array.from(target.items)).toEqual([validShape2, validShape3, validShape1]);
});

it('testRemove', () => {
const shape1 = new Rectangle(Rect.ZERO);
const shape2 = new Rectangle(Rect.ZERO);

target.add(shape1);
target.add(shape2);

target.remove(shape1);
expect(target.itemCount).toBe(1);
expect(Array.from(target.items)).toEqual([shape2]);

target.remove(shape2);
expect(target.itemCount).toBe(0);
expect(Array.from(target.items)).toEqual([]);
});

it('testMove_up', () => {
const shape1 = new Rectangle(Rect.ZERO);
const shape2 = new Rectangle(Rect.ZERO);
const shape3 = new Rectangle(Rect.ZERO);

target.add(shape1);
target.add(shape2);
target.add(shape3);

target.changeOrder(shape1, MoveActionType.UP);
expect(Array.from(target.items)).toEqual([shape2, shape1, shape3]);
});

it('testMove_down', () => {
const shape1 = new Rectangle(Rect.ZERO);
const shape2 = new Rectangle(Rect.ZERO);
const shape3 = new Rectangle(Rect.ZERO);

target.add(shape1);
target.add(shape2);
target.add(shape3);

target.changeOrder(shape3, MoveActionType.DOWN);
expect(Array.from(target.items)).toEqual([shape1, shape3, shape2]);
});

it('testMove_top', () => {
const shape1 = new Rectangle(Rect.ZERO);
const shape2 = new Rectangle(Rect.ZERO);
const shape3 = new Rectangle(Rect.ZERO);

target.add(shape1);
target.add(shape2);
target.add(shape3);

target.changeOrder(shape1, MoveActionType.TOP);
expect(Array.from(target.items)).toEqual([shape2, shape3, shape1]);
});

it('testMove_bottom', () => {
const shape1 = new Rectangle(Rect.ZERO);
const shape2 = new Rectangle(Rect.ZERO);
const shape3 = new Rectangle(Rect.ZERO);

target.add(shape1);
target.add(shape2);
target.add(shape3);

target.changeOrder(shape3, MoveActionType.BOTTOM);
expect(Array.from(target.items)).toEqual([shape3, shape1, shape2]);
});
});
112 changes: 112 additions & 0 deletions monosketch-svelte/src/lib/mono/shape/shape/group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright (c) 2024, tuanchauict
*/

import { Rect } from "$libs/graphics-geo/rect";
import { AddPosition, MoveActionType, QuickList } from "$mono/shape/collection/quick-list";
import {
AbstractSerializableShape,
SerializableGroup,
SerializableLine,
SerializableRectangle,
SerializableText,
} from "$mono/shape/serialization/serializable-shape";
import { AbstractShape } from "$mono/shape/shape/abstract-shape";
import { Line } from "$mono/shape/shape/line";
import { Rectangle } from "$mono/shape/shape/rectangle";
import { Text } from "$mono/shape/shape/text";

/**
* A special shape which manages a collection of shapes.
*/
export class Group extends AbstractShape {
private quickList: QuickList<AbstractShape> = new QuickList();
items: Iterable<AbstractShape> = this.quickList;

get itemCount(): number {
return this.quickList.size;
}

constructor(id: string | null = null, parentId: string | null = null) {
super(id, parentId);
}

get bound(): Rect {
if (this.quickList.isEmpty()) {
return Rect.ZERO;
}
let left = Infinity;
let right = -Infinity;
let top = Infinity;
let bottom = -Infinity;
for (const item of this.quickList) {
left = Math.min(left, item.bound.left);
right = Math.max(right, item.bound.right);
top = Math.min(top, item.bound.top);
bottom = Math.max(bottom, item.bound.bottom);
}
return Rect.byLTRB(left, top, right, bottom);
}

static fromSerializable(serializableGroup: SerializableGroup, parentId: string | null = null): Group {
const group = new Group(serializableGroup.actualId, parentId);
for (const serializableShape of serializableGroup.shapes) {
group.addInternal(Group.toShape(group.id, serializableShape));
}
group.versionCode = serializableGroup.versionCode;
return group;
}

toSerializableShape(isIdIncluded: boolean): SerializableGroup {
return new SerializableGroup(
this.id,
!isIdIncluded,
this.versionCode,
this.mapItems(item => item.toSerializableShape(isIdIncluded)),
);
}

add(shape: AbstractShape, position: AddPosition = AddPosition.Last): void {
this.update(() => this.addInternal(shape, position));
}

private addInternal(shape: AbstractShape, position: AddPosition = AddPosition.Last): boolean {
if (shape.parentId !== null && shape.parentId !== this.id) {
return false;
}
shape.parentId = this.id;
this.quickList.add(shape, position);
return true;
}

remove(shape: AbstractShape): void {
this.update(() => this.quickList.remove(shape) !== null);
}

changeOrder(shape: AbstractShape, moveActionType: MoveActionType): void {
this.update(() => this.quickList.move(shape, moveActionType));
}

toString(): string {
return `Group(${this.id})`;
}

static toShape(parentId: string, serializableShape: AbstractSerializableShape): AbstractShape {
switch (serializableShape.constructor) {
case SerializableRectangle:
return Rectangle.fromSerializable(serializableShape as SerializableRectangle, parentId);
case SerializableText:
return Text.fromSerializable(serializableShape as SerializableText, parentId);
case SerializableLine:
return Line.fromSerializable(serializableShape as SerializableLine, parentId);
case SerializableGroup:
return Group.fromSerializable(serializableShape as SerializableGroup, parentId);
default:
throw new Error("Unknown shape type");
}
}

private mapItems<T>(callback: (item: AbstractShape) => T): T[] {
return Array.from(this.items).map(callback);
}
}
16 changes: 12 additions & 4 deletions monosketch-svelte/src/lib/mono/shape/shape/rectangle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,25 @@ import { AbstractShape } from "$mono/shape/shape/abstract-shape";
/**
* A rectangle shape.
*/
class Rectangle extends AbstractShape {
export class Rectangle extends AbstractShape {
private boundInner: Rect;

constructor(rect: Rect, id: string | null, parentId: string | null = null) {
constructor(rect: Rect, id: string | null = null, parentId: string | null = null) {
super(id, parentId);
this.boundInner = rect;
this.setExtra(ShapeExtraManager.defaultRectangleExtra);
this.setExtra(RectangleExtra.create(ShapeExtraManager.defaultRectangleExtra));
}

static fromRect({ rect, id = null, parentId = null }: {
rect: Rect,
id?: string | null,
parentId?: string | null
}): Rectangle {
return new Rectangle(rect, id, parentId);
}

// Constructor that takes startPoint and endPoint
static fromPoints(startPoint: Point, endPoint: Point, id: string | null, parentId?: string): Rectangle {
static fromPoints(startPoint: Point, endPoint: Point, id: string | null, parentId: string | null = null): Rectangle {
const rect = Rect.byLTRB(startPoint.left, startPoint.top, endPoint.left, endPoint.top);
return new Rectangle(rect, id, parentId);
}
Expand Down

0 comments on commit 2d52b4d

Please sign in to comment.