-
-
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 #614 from tuanchauict/group-js
Port Group to TS
- Loading branch information
Showing
4 changed files
with
253 additions
and
14 deletions.
There are no files selected for viewing
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
119 changes: 119 additions & 0 deletions
119
monosketch-svelte/src/lib/mono/shape/shape/group.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,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]); | ||
}); | ||
}); |
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,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); | ||
} | ||
} |
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