Skip to content

Commit

Permalink
Merge pull request #620 from tuanchauict/shape-manager-js
Browse files Browse the repository at this point in the history
Add Shape manager and Command
  • Loading branch information
tuanchauict authored Dec 12, 2024
2 parents c614b95 + d003def commit 7fdd638
Show file tree
Hide file tree
Showing 14 changed files with 618 additions and 35 deletions.
146 changes: 146 additions & 0 deletions monosketch-svelte/src/lib/mono/shape/command/shape-manager-commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright (c) 2024, tuanchauict
*/

import { AddPosition, MoveActionType } from "$mono/shape/collection/quick-list";
import { type Command, ShapeManager } from "$mono/shape/shape-manager";
import type { AbstractShape } from "$mono/shape/shape/abstract-shape";
import { Group } from "$mono/shape/shape/group";

/**
* A [Command] for adding new shape into [ShapeManager].
*/
export class AddShape implements Command {
constructor(private readonly shape: AbstractShape) {
this.shape = shape;
}

getDirectAffectedParent(shapeManager: ShapeManager): Group | null {
return shapeManager.getGroup(this.shape.parentId);
}

execute(shapeManager: ShapeManager, parent: Group) {
parent.add(this.shape);
this.shape.parentId = parent.id;
shapeManager.register(this.shape);
}
}

/**
* A [Command] for removing a shape from [ShapeManager].
*/
export class RemoveShape implements Command {
constructor(private readonly shape: AbstractShape) {
this.shape = shape;
}

getDirectAffectedParent(shapeManager: ShapeManager): Group | null {
return shapeManager.getGroup(this.shape.parentId);
}

execute(shapeManager: ShapeManager, parent: Group) {
parent.remove(this.shape);
shapeManager.unregister(this.shape);

if (parent === shapeManager.root) {
return;
}
switch (parent.itemCount) {
case 1:
shapeManager.execute(new Ungroup(parent));
break;
case 0:
shapeManager.execute(new RemoveShape(parent));
break;
}
}
}

/**
* A [Command] for grouping shapes.
*/
export class GroupShapes implements Command {
constructor(private readonly sameParentShapes: AbstractShape[]) {
this.sameParentShapes = sameParentShapes;
}

getDirectAffectedParent(shapeManager: ShapeManager): Group | null {
if (this.sameParentShapes.length < 2) {
return null;
}
const parentId = this.sameParentShapes[0].parentId;
if (this.sameParentShapes.some(shape => shape.parentId !== parentId)) {
return null;
}
return shapeManager.getGroup(parentId);
}

execute(shapeManager: ShapeManager, parent: Group) {
const group = Group.create({ parentId: parent.id });
parent.add(group, AddPosition.After(this.sameParentShapes[this.sameParentShapes.length - 1]));
shapeManager.register(group);

for (const shape of this.sameParentShapes) {
parent.remove(shape);
shape.parentId = group.id;
group.add(shape);
}
}
}

/**
* A [Command] for decomposing a [Group].
*/
export class Ungroup implements Command {
constructor(private readonly group: Group) {
this.group = group;
}

getDirectAffectedParent(shapeManager: ShapeManager): Group | null {
return shapeManager.getGroup(this.group.parentId);
}

execute(shapeManager: ShapeManager, parent: Group) {
const items = this.group.itemArray.reverse();

for (const shape of items) {
this.group.remove(shape);
shape.parentId = null;
parent.add(shape, AddPosition.After(this.group));
}
shapeManager.execute(new RemoveShape(this.group));
}
}

/**
* A [Command] for changing order of a shape.
*/
export class ChangeOrder implements Command {

constructor(private readonly shape: AbstractShape, private readonly changeOrderType: ChangeOrderType) {
this.shape = shape;
this.changeOrderType = changeOrderType;
}

getDirectAffectedParent(shapeManager: ShapeManager): Group | null {
return shapeManager.getGroup(this.shape.parentId);
}

execute(_shapeManager: ShapeManager, parent: Group) {
parent.changeOrder(this.shape, ChangeOrderTypeToMoveActionType[this.changeOrderType]);
}
}

export enum ChangeOrderType {
FORWARD,
BACKWARD,
FRONT,
BACK,
}

const ChangeOrderTypeToMoveActionType: Record<ChangeOrderType, MoveActionType> = {
[ChangeOrderType.FORWARD]: MoveActionType.DOWN,
[ChangeOrderType.BACKWARD]: MoveActionType.UP,
[ChangeOrderType.FRONT]: MoveActionType.BOTTOM,
[ChangeOrderType.BACK]: MoveActionType.TOP,
};
3 changes: 3 additions & 0 deletions monosketch-svelte/src/lib/mono/shape/interaction-bound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export interface InteractionBound {
readonly interactionPoints: InteractionPoint[];
}

/**
* A class which defines interaction bound for scalable shapes.
*/
export class ScalableInteractionBound implements InteractionBound {
readonly type = InteractionBoundType.SCALABLE_SHAPE;

Expand Down
201 changes: 201 additions & 0 deletions monosketch-svelte/src/lib/mono/shape/shape-manager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*
* Copyright (c) 2024, tuanchauict
*/

import { Rect } from "$libs/graphics-geo/rect";
import {
AddShape,
ChangeOrder,
ChangeOrderType,
GroupShapes,
RemoveShape,
Ungroup,
} from "$mono/shape/command/shape-manager-commands";
import { ShapeManager } from "$mono/shape/shape-manager";
import type { AbstractShape } from "$mono/shape/shape/abstract-shape";
import { Group } from "$mono/shape/shape/group";
import { MockShape } from "$mono/shape/shape/mock-shape";
import { Rectangle } from "$mono/shape/shape/rectangle";
import { beforeEach, describe, expect, test } from "vitest";

describe('ShapeManagerTest', () => {
let target: ShapeManager;

beforeEach(() => {
target = new ShapeManager();
});

const addShape = (shape: AbstractShape) => {
target.execute(new AddShape(shape));
};

test('testExecute_Add', () => {
const shape1 = new MockShape(Rect.ZERO);
target.execute(new AddShape(shape1));
expect(target.root.itemArray).toStrictEqual([shape1]);

const group1 = Group.create();
target.execute(new AddShape(group1));
expect(target.root.itemArray).toEqual([shape1, group1]);

const shape2 = Rectangle.fromRect({ rect: Rect.ZERO, parentId: group1.id });
target.execute(new AddShape(shape2));
expect(target.root.itemArray).toEqual([shape1, group1]);
expect(group1.itemArray).toEqual([shape2]);

const shape3 = new MockShape(Rect.ZERO, '1000');
target.execute(new AddShape(shape3));
expect(target.root.itemArray).toEqual([shape1, group1]);
expect(group1.itemArray).toEqual([shape2]);
});

test('testExecute_Remove_singleGroupItem_removeGroup', () => {
const group = Group.create();
const shape = Rectangle.fromRect({ rect: Rect.ZERO, parentId: group.id });

addShape(group);
addShape(shape);
target.execute(new RemoveShape(shape));
expect(target.root.itemCount).toBe(0);
});

test('testExecute_Remove_removeGroupItem_ungroup', () => {
const group = Group.create();
const shape1 = Rectangle.fromRect({ rect: Rect.ZERO });
const shape2 = Rectangle.fromRect({ rect: Rect.ZERO, parentId: group.id });
const shape3 = Rectangle.fromRect({ rect: Rect.ZERO, parentId: group.id });

addShape(group);
addShape(shape1);
addShape(shape2);
addShape(shape3);

target.execute(new RemoveShape(shape3));
expect(target.root.itemArray).toEqual([shape2, shape1]);
expect(shape2.parentId).toBe(target.root.id);
});

test('testExecute_Remove_removeGroupItem_unchangeRoot', () => {
const group = Group.create();
const shape1 = Rectangle.fromRect({ rect: Rect.ZERO });
const shape2 = Rectangle.fromRect({ rect: Rect.ZERO, parentId: group.id });
const shape3 = Rectangle.fromRect({ rect: Rect.ZERO, parentId: group.id });
const shape4 = Rectangle.fromRect({ rect: Rect.ZERO, parentId: group.id });

addShape(group);
addShape(shape1);
addShape(shape2);
addShape(shape3);
addShape(shape4);

target.execute(new RemoveShape(shape4));
expect(target.root.itemArray).toEqual([group, shape1]);
expect(group.itemArray).toEqual([shape2, shape3]);
});

test('testExecute_Group_invalid', () => {
const group = Group.create();
const shape1 = Rectangle.fromRect({ rect: Rect.ZERO });
const shape2 = Rectangle.fromRect({ rect: Rect.ZERO, parentId: group.id });

addShape(group);
addShape(shape1);
addShape(shape2);

target.execute(new GroupShapes([shape1]));
expect(target.root.itemArray).toEqual([group, shape1]);

target.execute(new GroupShapes([shape1, shape2]));
expect(target.root.itemArray).toEqual([group, shape1]);
expect(group.itemArray).toEqual([shape2]);
});

test('testExecute_Group_valid', () => {
const shape0 = Rectangle.fromRect({ rect: Rect.ZERO });
const shape1 = Rectangle.fromRect({ rect: Rect.ZERO });
const shape2 = Rectangle.fromRect({ rect: Rect.ZERO });
const shape3 = Rectangle.fromRect({ rect: Rect.ZERO });

addShape(shape0);
addShape(shape1);
addShape(shape2);
addShape(shape3);
target.execute(new GroupShapes([shape1, shape2]));

const items = target.root.itemArray;

expect(target.root.itemCount).toBe(3);
expect(items[0]).toBe(shape0);
expect(items[2]).toBe(shape3);
const group = items[1] as Group;
expect(group.itemArray).toEqual([shape1, shape2]);
expect(target.getGroup(shape1.parentId)).toBe(group);
expect(target.getGroup(shape2.parentId)).toBe(group);
});

test('testExecute_Ungroup', () => {
const group = Group.create();
const shape0 = Rectangle.fromRect({ rect: Rect.ZERO });
const shape1 = Rectangle.fromRect({ rect: Rect.ZERO, parentId: group.id });
const shape2 = Rectangle.fromRect({ rect: Rect.ZERO, parentId: group.id });
const shape3 = Rectangle.fromRect({ rect: Rect.ZERO });

addShape(shape0);
addShape(group);
addShape(shape1);
addShape(shape2);
addShape(shape3);

target.execute(new Ungroup(group));

expect(target.root.itemArray).toStrictEqual([shape0, shape1, shape2, shape3]);
expect(shape1.parentId).toBe(target.root.id);
expect(shape2.parentId).toBe(target.root.id);
});

test('testExecute_ChangeOrder', () => {
const shape1 = new MockShape(Rect.ZERO);
const shape2 = new MockShape(Rect.ZERO);
const shape3 = new MockShape(Rect.ZERO);

addShape(shape1);
addShape(shape2);
addShape(shape3);

target.execute(new ChangeOrder(shape3, ChangeOrderType.BACK));
expect(target.root.itemArray).toEqual([shape1, shape2, shape3]);

target.execute(new ChangeOrder(shape1, ChangeOrderType.BACK));
expect(target.root.itemArray).toEqual([shape2, shape3, shape1]);

target.execute(new ChangeOrder(shape1, ChangeOrderType.FRONT));
expect(target.root.itemArray).toEqual([shape1, shape2, shape3]);

target.execute(new ChangeOrder(shape3, ChangeOrderType.FRONT));
expect(target.root.itemArray).toEqual([shape3, shape1, shape2]);

target.execute(new ChangeOrder(shape1, ChangeOrderType.FORWARD));
expect(target.root.itemArray).toEqual([shape1, shape3, shape2]);

target.execute(new ChangeOrder(shape3, ChangeOrderType.BACKWARD));
expect(target.root.itemArray).toEqual([shape1, shape2, shape3]);
});

test('testRecursiveVersionUpdate', () => {
const group0 = Group.create();
addShape(group0);
const group1 = Group.create({ parentId: group0.id });
addShape(group1);

const rootOldVersion = target.root.versionCode;
const group0OldVersion = group0.versionCode;
const group1OldVersion = group1.versionCode;

const group2 = Group.create({ parentId: group1.id });
addShape(group2);

expect(target.root.versionCode).not.toBe(rootOldVersion);
expect(group0.versionCode).not.toEqual(group0OldVersion);
expect(group1.versionCode).not.toBe(group1OldVersion);
});
});
Loading

0 comments on commit 7fdd638

Please sign in to comment.