-
-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #620 from tuanchauict/shape-manager-js
Add Shape manager and Command
- Loading branch information
Showing
14 changed files
with
618 additions
and
35 deletions.
There are no files selected for viewing
146 changes: 146 additions & 0 deletions
146
monosketch-svelte/src/lib/mono/shape/command/shape-manager-commands.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
201 changes: 201 additions & 0 deletions
201
monosketch-svelte/src/lib/mono/shape/shape-manager.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
Oops, something went wrong.