Skip to content

Commit

Permalink
Add Group shape
Browse files Browse the repository at this point in the history
  • Loading branch information
tuanchauict committed Dec 9, 2024
1 parent d035976 commit 5719e96
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 0 deletions.
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);
}
}

0 comments on commit 5719e96

Please sign in to comment.