Merge pull request #370 from idrawjs/dev-v1.x

Dev v1.x
This commit is contained in:
Deepsea 2026-03-28 20:55:21 +08:00 committed by GitHub
commit 5094ed2e92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
255 changed files with 17739 additions and 12252 deletions

View file

@ -21,8 +21,8 @@ jobs:
- run: npm run test
- run: npm run build
- run: npm run version:reset-for-release
# - run: npm publish --provenance --access public -w ./packages/types --tag next
- run: npm publish --provenance --access public -w ./packages/types
- run: npm publish --provenance --access public -w ./packages/types --tag next
# - run: npm publish --provenance --access public -w ./packages/types
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: npm publish --provenance --access public -w ./packages/util

View file

@ -1,6 +1,6 @@
{
"private": false,
"version": "0.4.3",
"version": "1.0.0-alpha.0",
"workspaces": [
"packages/*"
],

View file

@ -1,6 +1,6 @@
{
"name": "@idraw/core",
"version": "0.4.0",
"version": "1.0.0",
"description": "",
"main": "dist/esm/index.js",
"module": "dist/esm/index.js",
@ -21,12 +21,12 @@
"author": "idrawjs",
"license": "MIT",
"devDependencies": {
"@idraw/types": "workspace:^0.4"
"@idraw/types": "workspace:*"
},
"dependencies": {},
"peerDependencies": {
"@idraw/renderer": "workspace:^0.4",
"@idraw/util": "workspace:^0.4"
"@idraw/renderer": "workspace:*",
"@idraw/util": "workspace:*"
},
"publishConfig": {
"access": "public",

View file

@ -1,8 +1,8 @@
import { Renderer, Calculator } from '@idraw/renderer';
import {
// throttle,
calcElementsContextSize,
EventEmitter
calcMaterialsContextSize,
EventEmitter,
} from '@idraw/util';
import type {
Data,
@ -11,9 +11,9 @@ import type {
BoardMiddlewareObject,
BoardWatcherEventMap,
ViewSizeInfo,
PointSize,
Point,
BoardExtendEventMap,
UtilEventEmitter
UtilEventEmitter,
} from '@idraw/types';
import { BoardWatcher } from './watcher';
import { Sharer } from './sharer';
@ -40,18 +40,19 @@ export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
#eventHub: EventEmitter<T> = new EventEmitter<T>();
#hasDestroyed: boolean = false;
constructor(opts: BoardOptions) {
const { boardContent } = opts;
const { boardContent, container } = opts;
const sharer = new Sharer();
const watcher = new BoardWatcher({
boardContent,
sharer,
disabled: opts?.disableWatcher
disabled: opts?.disableWatcher,
container,
});
const renderer = new Renderer({
viewContext: boardContent.viewContext,
tempContext: boardContent.tempContext,
sharer
sharer,
});
const calculator = renderer.getCalculator();
@ -70,7 +71,7 @@ export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
},
afterDrawFrame: (e) => {
this.#handleAfterDrawFrame(e);
}
},
});
this.#init();
this.#resetActiveMiddlewareObjs();
@ -131,6 +132,7 @@ export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
this.#watcher.on('scrollX', this.#handleScrollX.bind(this));
this.#watcher.on('scrollY', this.#handleScrollY.bind(this));
this.#watcher.on('resize', this.#handleResize.bind(this));
this.#watcher.on('click', this.#handleClick.bind(this));
this.#watcher.on('doubleClick', this.#handleDoubleClick.bind(this));
this.#watcher.on('contextMenu', this.#handleContextMenu.bind(this));
}
@ -190,6 +192,16 @@ export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
}
}
#handleClick(e: BoardWatcherEventMap['click']) {
for (let i = 0; i < this.#activeMiddlewareObjs.length; i++) {
const obj = this.#activeMiddlewareObjs[i];
const result = obj?.click?.(e);
if (result === false) {
return;
}
}
}
#handleDoubleClick(e: BoardWatcherEventMap['doubleClick']) {
for (let i = 0; i < this.#activeMiddlewareObjs.length; i++) {
const obj = this.#activeMiddlewareObjs[i];
@ -320,21 +332,21 @@ export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
const viewSizeInfo = sharer.getActiveViewSizeInfo();
const viewScaleInfo = sharer.getActiveViewScaleInfo();
// const currentScaleInfo = sharer.getActiveViewScaleInfo();
const newViewContextSize = calcElementsContextSize(data.elements, {
const newViewContextSize = calcMaterialsContextSize(data.materials, {
viewWidth: viewSizeInfo.width,
viewHeight: viewSizeInfo.height,
extend: true
extend: true,
});
this.#viewer.resetVirtualFlatItemMap(data, {
this.#viewer.resetVirtualItemMap(data, {
viewSizeInfo,
viewScaleInfo
viewScaleInfo,
});
this.#viewer.drawFrame();
const newViewSizeInfo = {
...viewSizeInfo,
...newViewContextSize
...newViewContextSize,
};
this.#sharer.setActiveViewSizeInfo(newViewSizeInfo);
@ -372,7 +384,7 @@ export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
this.#middlewareMap.set(middleware, {
status: 'enable',
middlewareObject: obj,
config
config,
});
this.#resetActiveMiddlewareObjs();
}
@ -398,14 +410,14 @@ export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
}
}
scale(opts: { scale: number; point: PointSize; ignoreUpdateVisibleStatus?: boolean }) {
scale(opts: { scale: number; point: Point; ignoreUpdateVisibleStatus?: boolean }) {
const viewer = this.#viewer;
const { ignoreUpdateVisibleStatus } = opts;
const { moveX, moveY } = viewer.scale({
...opts,
...{
ignoreUpdateVisibleStatus: true
}
ignoreUpdateVisibleStatus: true,
},
});
viewer.scroll({ moveX, moveY, ignoreUpdateVisibleStatus });
}

View file

@ -1,4 +1,4 @@
import type { ActiveStore, Element, ElementDetailMap, RecursivePartial, StoreSharer, ViewScaleInfo, ViewSizeInfo } from '@idraw/types';
import type { ActiveStore, Material, RecursivePartial, StoreSharer, ViewScaleInfo, ViewSizeInfo } from '@idraw/types';
import { Store } from '@idraw/util';
const defaultActiveStorage: ActiveStore = {
@ -7,13 +7,13 @@ const defaultActiveStorage: ActiveStore = {
devicePixelRatio: 1,
contextWidth: 0,
contextHeight: 0,
data: null,
data: { materials: [] },
scale: 1,
offsetLeft: 0,
offsetRight: 0,
offsetTop: 0,
offsetBottom: 0,
overrideElementMap: null
overrideMaterialMap: null,
};
export class Sharer implements StoreSharer<Record<string | number | symbol | any, any>> {
@ -24,10 +24,10 @@ export class Sharer implements StoreSharer<Record<string | number | symbol | any
constructor() {
const activeStore = new Store<ActiveStore>({
defaultStorage: defaultActiveStorage
defaultStorage: defaultActiveStorage,
});
const sharedStore = new Store({
defaultStorage: {}
defaultStorage: {},
});
this.#activeStore = activeStore;
this.#sharedStore = sharedStore;
@ -65,7 +65,7 @@ export class Sharer implements StoreSharer<Record<string | number | symbol | any
offsetTop: this.#activeStore.get('offsetTop'),
offsetBottom: this.#activeStore.get('offsetBottom'),
offsetLeft: this.#activeStore.get('offsetLeft'),
offsetRight: this.#activeStore.get('offsetRight')
offsetRight: this.#activeStore.get('offsetRight'),
};
return viewScaleInfo;
}
@ -93,16 +93,16 @@ export class Sharer implements StoreSharer<Record<string | number | symbol | any
height: this.#activeStore.get('height'),
devicePixelRatio: this.#activeStore.get('devicePixelRatio'),
contextWidth: this.#activeStore.get('contextWidth'),
contextHeight: this.#activeStore.get('contextHeight')
contextHeight: this.#activeStore.get('contextHeight'),
};
return sizeInfo;
}
getActiveOverrideElemenentMap(): Record<string, RecursivePartial<Element<keyof ElementDetailMap, Record<string, any>>>> | null {
return this.#activeStore.get('overrideElementMap');
getActiveOverrideMaterialMap(): Record<string, RecursivePartial<Material>> | null {
return this.#activeStore.get('overrideMaterialMap');
}
setActiveOverrideElemenentMap(map: Record<string, RecursivePartial<Element<keyof ElementDetailMap, Record<string, any>>>> | null): void {
this.#activeStore.set('overrideElementMap', map);
setActiveOverrideMaterialMap(map: Record<string, RecursivePartial<Material>> | null): void {
this.#activeStore.set('overrideMaterialMap', map);
}
}

View file

@ -1,6 +1,6 @@
import { EventEmitter, viewScale, viewScroll, calcViewScaleInfo } from '@idraw/util';
import type {
PointSize,
Point,
BoardViewer,
BoardViewerEventMap,
BoardViewerOptions,
@ -9,7 +9,7 @@ import type {
BoardViewerFrameSnapshot,
ViewScaleInfo,
ViewSizeInfo,
Data
Data,
} from '@idraw/types';
const { requestAnimationFrame } = window;
@ -55,7 +55,7 @@ export class Viewer extends EventEmitter<BoardViewerEventMap> implements BoardVi
height,
contextHeight,
contextWidth,
devicePixelRatio
devicePixelRatio,
} = snapshot.activeStore;
const viewScaleInfo: ViewScaleInfo = {
@ -63,19 +63,19 @@ export class Viewer extends EventEmitter<BoardViewerEventMap> implements BoardVi
offsetTop,
offsetBottom,
offsetLeft,
offsetRight
offsetRight,
};
const viewSizeInfo: ViewSizeInfo = {
width,
height,
contextHeight,
contextWidth,
devicePixelRatio
devicePixelRatio,
};
if (snapshot?.activeStore.data) {
renderer.drawData(snapshot.activeStore.data, {
viewScaleInfo,
viewSizeInfo
viewSizeInfo,
});
}
@ -98,7 +98,7 @@ export class Viewer extends EventEmitter<BoardViewerEventMap> implements BoardVi
}
}
resetVirtualFlatItemMap(
resetVirtualItemMap(
data: Data,
opts: {
viewScaleInfo: ViewScaleInfo;
@ -106,7 +106,7 @@ export class Viewer extends EventEmitter<BoardViewerEventMap> implements BoardVi
}
): void {
if (data) {
this.#opts.calculator.resetVirtualFlatItemMap(data, opts);
this.#opts.calculator.resetVirtualItemMap(data, opts);
}
}
@ -116,14 +116,15 @@ export class Viewer extends EventEmitter<BoardViewerEventMap> implements BoardVi
const sharedStore: Record<string, any> = sharer.getSharedStoreSnapshot();
// const activeStore: ActiveStore = sharer.getActiveStoreSnapshot({ deepClone: true });
// const sharedStore: Record<string, any> = sharer.getSharedStoreSnapshot({ deepClone: true });
this.#drawFrameSnapshotQueue.push({
activeStore,
sharedStore
sharedStore,
});
this.#drawAnimationFrame();
}
scale(opts: { scale: number; point: PointSize; ignoreUpdateVisibleStatus?: boolean }): {
scale(opts: { scale: number; point: Point; ignoreUpdateVisibleStatus?: boolean }): {
moveX: number;
moveY: number;
} {
@ -133,13 +134,13 @@ export class Viewer extends EventEmitter<BoardViewerEventMap> implements BoardVi
scale,
point,
viewScaleInfo: sharer.getActiveViewScaleInfo(),
viewSizeInfo: sharer.getActiveViewSizeInfo()
viewSizeInfo: sharer.getActiveViewSizeInfo(),
});
sharer.setActiveStorage('scale', scale);
if (!ignoreUpdateVisibleStatus) {
this.#opts.calculator.updateVisiableStatus({
viewScaleInfo: sharer.getActiveViewScaleInfo(),
viewSizeInfo: sharer.getActiveViewSizeInfo()
viewSizeInfo: sharer.getActiveViewSizeInfo(),
});
}
return { moveX, moveY };
@ -154,13 +155,13 @@ export class Viewer extends EventEmitter<BoardViewerEventMap> implements BoardVi
moveX,
moveY,
viewScaleInfo: prevViewScaleInfo,
viewSizeInfo
viewSizeInfo,
});
sharer.setActiveViewScaleInfo(viewScaleInfo);
if (!ignoreUpdateVisibleStatus) {
this.#opts.calculator.updateVisiableStatus({
viewScaleInfo: sharer.getActiveViewScaleInfo(),
viewSizeInfo: sharer.getActiveViewSizeInfo()
viewSizeInfo: sharer.getActiveViewSizeInfo(),
});
}
return viewScaleInfo;
@ -169,14 +170,14 @@ export class Viewer extends EventEmitter<BoardViewerEventMap> implements BoardVi
updateViewScaleInfo(opts: { scale: number; offsetX: number; offsetY: number }): ViewScaleInfo {
const { sharer } = this.#opts;
const viewScaleInfo = calcViewScaleInfo(opts, {
viewSizeInfo: sharer.getActiveViewSizeInfo()
viewSizeInfo: sharer.getActiveViewSizeInfo(),
});
sharer.setActiveViewScaleInfo(viewScaleInfo);
this.#opts.calculator.updateVisiableStatus({
viewScaleInfo: sharer.getActiveViewScaleInfo(),
viewSizeInfo: sharer.getActiveViewSizeInfo()
viewSizeInfo: sharer.getActiveViewSizeInfo(),
});
return viewScaleInfo;
}
@ -206,7 +207,7 @@ export class Viewer extends EventEmitter<BoardViewerEventMap> implements BoardVi
if (!opts?.ignoreUpdateVisibleStatus) {
this.#opts.calculator.updateVisiableStatus({
viewScaleInfo: sharer.getActiveViewScaleInfo(),
viewSizeInfo: sharer.getActiveViewSizeInfo()
viewSizeInfo: sharer.getActiveViewSizeInfo(),
});
}
return newViewSize;

View file

@ -1,13 +1,12 @@
import type {
Point,
ActionPoint,
BoardWatcherEventMap,
Data,
Element,
ElementType,
Material,
BoardWatcherOptions,
BoardWatcherStore
BoardWatcherStore,
} from '@idraw/types';
import { EventEmitter, Store } from '@idraw/util';
import { EventEmitter, Store, ATTR_VALID_WATCH, getHTMLElementRectInPage } from '@idraw/util';
function isBoardAvailableNum(num: any): boolean {
return num > 0 || num < 0 || num === 0;
@ -20,7 +19,7 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
constructor(opts: BoardWatcherOptions) {
super();
const store = new Store<BoardWatcherStore>({
defaultStorage: { hasPointDown: false, prevClickPoint: null, inCanvas: true }
defaultStorage: { hasPointDown: false, inCanvas: true },
});
this.#store = store;
this.#opts = opts;
@ -38,16 +37,19 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
if (this.#hasDestroyed) {
return;
}
const canvas = this.#opts.boardContent.boardContext.canvas;
// const canvas = this.#opts.boardContent.boardContext.canvas;
const container = window;
container.addEventListener('mousemove', this.#onHover);
container.addEventListener('mousedown', this.#onPointStart);
const innerContainer: HTMLElement = this.#opts?.container || this.#opts.boardContent.boardContext.canvas;
container.addEventListener('mousemove', this.#onPointMove);
container.addEventListener('mouseup', this.#onPointEnd);
// container.addEventListener('mouseleave', this.#onPointLeave);
canvas.addEventListener('wheel', this.#onWheel, { passive: false });
container.addEventListener('click', this.#onClick);
container.addEventListener('contextmenu', this.#onContextMenu);
innerContainer.addEventListener('mousemove', this.#onHover);
innerContainer.addEventListener('mousedown', this.#onPointStart);
innerContainer.addEventListener('wheel', this.#onWheel, { passive: false });
innerContainer.addEventListener('click', this.#onClick);
innerContainer.addEventListener('contextmenu', this.#onContextMenu);
innerContainer.addEventListener('dblclick', this.#doubleClick);
}
offEvents() {
@ -55,15 +57,16 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
return;
}
const container = window;
const canvas = this.#opts.boardContent.boardContext.canvas;
container.removeEventListener('mousemove', this.#onHover);
container.removeEventListener('mousedown', this.#onPointStart);
const innerContainer: HTMLElement = this.#opts?.container || this.#getBoardCanvas();
container.removeEventListener('mousemove', this.#onPointMove);
container.removeEventListener('mouseup', this.#onPointEnd);
container.removeEventListener('mouseleave', this.#onPointLeave);
canvas.removeEventListener('wheel', this.#onWheel);
container.removeEventListener('click', this.#onClick);
container.removeEventListener('contextmenu', this.#onContextMenu);
innerContainer.removeEventListener('mousemove', this.#onHover);
innerContainer.removeEventListener('mousedown', this.#onPointStart);
innerContainer.removeEventListener('wheel', this.#onWheel);
innerContainer.removeEventListener('click', this.#onClick);
innerContainer.removeEventListener('contextmenu', this.#onContextMenu);
innerContainer.removeEventListener('dblclick', this.#doubleClick);
}
destroy() {
@ -72,7 +75,12 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
this.#hasDestroyed = true;
}
#getBoardCanvas() {
return this.#opts.boardContent.boardContext.canvas;
}
#onWheel = (e: WheelEvent) => {
const nativeEvent = e;
if (!this.#isInTarget(e)) {
return;
}
@ -80,19 +88,21 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
if (!this.#isVaildPoint(point)) {
return;
}
e.preventDefault();
e.stopPropagation();
const deltaX = e.deltaX > 0 || e.deltaX < 0 ? e.deltaX : 0;
const deltaY = e.deltaY > 0 || e.deltaY < 0 ? e.deltaY : 0;
if (e.ctrlKey === true && this.has('wheelScale')) {
this.trigger('wheelScale', { deltaX, deltaY, point });
this.trigger('wheelScale', { deltaX, deltaY, point, nativeEvent });
} else if (this.has('wheel')) {
this.trigger('wheel', { deltaX, deltaY, point });
this.trigger('wheel', { deltaX, deltaY, point, nativeEvent });
}
};
#onContextMenu = (e: MouseEvent) => {
const nativeEvent = e;
if (e.button !== 2) {
return;
}
@ -104,10 +114,11 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
if (!this.#isVaildPoint(point)) {
return;
}
this.trigger('contextMenu', { point });
this.trigger('contextMenu', { point, nativeEvent });
};
#onClick = (e: MouseEvent) => {
const nativeEvent = e;
if (!this.#isInTarget(e)) {
return;
}
@ -116,39 +127,43 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
if (!this.#isVaildPoint(point)) {
return;
}
const maxLimitTime = 500;
const t = Date.now();
const preClickPoint = this.#store.get('prevClickPoint');
if (
preClickPoint &&
t - preClickPoint.t <= maxLimitTime &&
Math.abs(preClickPoint.x - point.x) <= 5 &&
Math.abs(preClickPoint.y - point.y) <= 5
) {
this.trigger('doubleClick', { point });
} else {
this.#store.set('prevClickPoint', point);
this.trigger('click', { point, nativeEvent });
};
#doubleClick = (e: MouseEvent) => {
const nativeEvent = e;
if (!this.#isInTarget(e)) {
return;
}
e.preventDefault();
const point = this.#getPoint(e);
if (!this.#isVaildPoint(point)) {
return;
}
this.trigger('doubleClick', { point, nativeEvent });
};
#onPointLeave = (e: MouseEvent) => {
const nativeEvent = e;
this.#store.set('hasPointDown', false);
e.preventDefault();
const point = this.#getPoint(e);
this.trigger('pointLeave', { point });
this.trigger('pointLeave', { point, nativeEvent });
};
#onPointEnd = (e: MouseEvent) => {
const nativeEvent = e;
this.#store.set('hasPointDown', false);
if (!this.#isInTarget(e)) {
return;
}
e.preventDefault();
const point = this.#getPoint(e);
this.trigger('pointEnd', { point });
this.trigger('pointEnd', { point, nativeEvent });
};
#onPointMove = (e: MouseEvent) => {
const nativeEvent = e;
if (!this.#isInTarget(e)) {
return;
}
@ -157,7 +172,7 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
const point = this.#getPoint(e);
if (!this.#isVaildPoint(point)) {
if (this.#store.get('hasPointDown')) {
this.trigger('pointLeave', { point });
this.trigger('pointLeave', { point, nativeEvent });
this.#store.set('hasPointDown', false);
}
return;
@ -165,10 +180,11 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
if (this.#store.get('hasPointDown') !== true) {
return;
}
this.trigger('pointMove', { point });
this.trigger('pointMove', { point, nativeEvent });
};
#onPointStart = (e: MouseEvent) => {
const nativeEvent = e;
// mouse-left-click: button = 0
// mouse-right-click: button = 2
// mouse-scroll button = 1
@ -180,14 +196,17 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
}
e.preventDefault();
const point = this.#getPoint(e);
if (!this.#isVaildPoint(point)) {
return;
}
this.#store.set('hasPointDown', true);
this.trigger('pointStart', { point });
this.trigger('pointStart', { point, nativeEvent });
};
#onHover = (e: MouseEvent) => {
const nativeEvent = e;
if (!this.#isInTarget(e)) {
if (this.#store.get('inCanvas') === true) {
this.#store.set('inCanvas', false);
@ -204,25 +223,39 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
if (!this.#isVaildPoint(point)) {
return;
}
this.trigger('hover', { point });
this.trigger('hover', { point, nativeEvent });
};
#isInTarget(e: MouseEvent | WheelEvent) {
return e.target === this.#opts.boardContent.boardContext.canvas;
const $target = e.target as HTMLElement;
if ($target.getAttribute(ATTR_VALID_WATCH) === 'true') {
return true;
}
if ($target !== this.#getBoardCanvas()) {
return false;
}
const rect = getHTMLElementRectInPage(this.#opts.boardContent.boardContext.canvas);
return (
e.pageX >= rect.pageX &&
e.pageX <= rect.pageX + rect.width &&
e.pageY >= rect.pageY &&
e.pageY <= rect.pageY + rect.height
);
}
#getPoint(e: MouseEvent): Point {
#getPoint(e: MouseEvent): ActionPoint {
const boardCanvas = this.#opts.boardContent.boardContext.canvas;
const rect = boardCanvas.getBoundingClientRect();
const p: Point = {
const p: ActionPoint = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
t: Date.now()
t: Date.now(),
};
return p;
}
#isVaildPoint(p: Point): boolean {
#isVaildPoint(p: ActionPoint): boolean {
const viewSize = this.#opts.sharer.getActiveViewSizeInfo();
const { width, height } = viewSize;
if (isBoardAvailableNum(p.x) && isBoardAvailableNum(p.y) && p.x <= width && p.y <= height) {
@ -234,19 +267,19 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
interface PointResult {
index: number;
element: Element<ElementType> | null;
material: Material | null;
}
export function getPointResult(p: Point, data: Data): PointResult {
export function getPointResult(p: ActionPoint, data: Data): PointResult {
const result: PointResult = {
index: -1,
element: null
material: null,
};
for (let i = 0; i < data.elements.length; i++) {
const elem = data.elements[i];
if (p.x >= elem.x && p.x <= elem.x + elem.w && p.y >= elem.y && p.y <= elem.y + elem.h) {
for (let i = 0; i < data.materials.length; i++) {
const mtrl = data.materials[i];
if (p.x >= mtrl.x && p.x <= mtrl.x + mtrl.width && p.y >= mtrl.y && p.y <= mtrl.y + mtrl.height) {
result.index = i;
result.element = elem;
result.material = mtrl;
break;
}
}

481
packages/core/src/core.ts Normal file
View file

@ -0,0 +1,481 @@
import type {
Data,
Point,
CoreOptions,
Middleware,
ViewSizeInfo,
CoreEventMap,
ViewScaleInfo,
LoadItemMap,
MaterialType,
RecursivePartial,
Material,
StrictMaterial,
ModifyRecord,
MaterialPosition,
DataLayout,
FlattenLayout,
DataGlobal,
} from '@idraw/types';
import {
deepClone,
createMaterial,
getMaterialPositionFromList,
toFlattenMaterial,
deleteMaterialInList,
findMaterialFromListByPosition,
updateMaterialInListByPosition,
insertMaterialToListByPosition,
moveMaterialPosition,
toFlattenLayout,
toFlattenGlobal,
get,
mergeLayout,
mergeGlobal,
setHTMLCSSProps,
createBoardContent,
validateMaterials,
} from '@idraw/util';
import { Board } from './board';
import { Cursor } from './cursor/cursor';
import { getModifyMaterialRecord } from './record';
export class Core<E extends CoreEventMap = CoreEventMap> {
#board: Board<E>;
// #opts: CoreOptions;
#canvas: HTMLCanvasElement;
#container: HTMLDivElement;
constructor(container: HTMLDivElement, opts: CoreOptions) {
const { devicePixelRatio = 1, width, height, disableWatcher = false } = opts;
setHTMLCSSProps(container, { width, height });
// this.#opts = opts;
this.#container = container;
const canvas = document.createElement('canvas');
canvas.setAttribute('tabindex', '0');
setHTMLCSSProps(canvas, { margin: 0, padding: 0 });
this.#canvas = canvas;
this.#initContainer();
container.appendChild(canvas);
const boardContent = createBoardContent(canvas, { width, height, devicePixelRatio });
const board = new Board<E>({ boardContent, container, disableWatcher });
const sharer = board.getSharer();
sharer.setActiveViewSizeInfo({
width,
height,
devicePixelRatio,
contextWidth: width,
contextHeight: height,
});
this.#board = board;
this.resize(sharer.getActiveViewSizeInfo());
const eventHub = board.getEventHub();
new Cursor(container, {
eventHub,
});
}
isDestroyed() {
return this.#board.isDestroyed();
}
destroy() {
this.#board.destroy();
this.#canvas.remove();
}
#initContainer() {
setHTMLCSSProps(this.#container, {
position: 'relative',
margin: '0px',
padding: '0px',
overflow: 'hidden',
});
}
use<C extends any = any>(middleware: Middleware<any, any, any>, config?: C) {
this.#board.use<C>(middleware, config);
}
disuse(middleware: Middleware<any, any, any>) {
this.#board.disuse(middleware);
}
resetMiddlewareConfig<C extends any = any>(middleware: Middleware<any, any, any>, config?: Partial<C>) {
this.#board.resetMiddlewareConfig(middleware, config);
}
#resetData(data: Data) {
validateMaterials(data?.materials || []);
this.#board.setData(data);
}
setData(data: Data) {
const loader = this.#board.getRenderer().getLoader();
loader.reset();
this.#resetData(data);
}
getData(): Data | null {
return this.#board.getData();
}
scale(opts: { scale: number; point: Point }) {
this.#board.scale(opts);
const viewer = this.#board.getViewer();
viewer.drawFrame();
}
resize(newViewSize: Partial<ViewSizeInfo>) {
const board = this.#board;
const container = this.#container;
const sharer = board.getSharer();
const viewSizeInfo = sharer.getActiveViewSizeInfo();
const viewSize = {
...viewSizeInfo,
...newViewSize,
};
const { width, height } = viewSize;
setHTMLCSSProps(container, { width, height });
board.resize(viewSize);
}
clear() {
this.#board.clear();
}
on<T extends keyof E>(name: T, callback: (e: E[T]) => void) {
const eventHub = this.#board.getEventHub();
eventHub.on(name, callback);
}
off<T extends keyof E>(name: T, callback: (e: E[T]) => void) {
const eventHub = this.#board.getEventHub();
eventHub.off(name, callback);
}
trigger<T extends keyof E>(name: T, e: E[T]) {
const eventHub = this.#board.getEventHub();
eventHub.trigger(name, e);
}
getViewInfo(): { viewSizeInfo: ViewSizeInfo; viewScaleInfo: ViewScaleInfo } {
const board = this.#board;
const sharer = board.getSharer();
const viewSizeInfo = sharer.getActiveViewSizeInfo();
const viewScaleInfo = sharer.getActiveViewScaleInfo();
return {
viewSizeInfo,
viewScaleInfo,
};
}
refresh() {
this.#board.getViewer().drawFrame();
}
forceRender() {
const renderer = this.#board.getRenderer();
const calculator = renderer.getCalculator();
const loader = renderer.getLoader();
const data = this.getData();
if (data) {
const { viewScaleInfo, viewSizeInfo } = this.getViewInfo();
calculator.resetVirtualItemMap(data, {
viewScaleInfo,
viewSizeInfo,
});
}
loader.reset();
this.refresh();
}
setViewScale(opts: { scale: number; offsetX: number; offsetY: number }) {
this.#board.updateViewScaleInfo(opts);
}
getLoadItemMap(): LoadItemMap {
return this.#board.getRenderer().getLoadItemMap();
}
onBoardWatcherEvents() {
this.#board.onWatcherEvents();
}
offBoardWatcherEvents() {
this.#board.offWatcherEvents();
}
createMaterial<T extends MaterialType = MaterialType>(
type: T,
material: RecursivePartial<StrictMaterial<T>>,
opts?: {
viewCenter?: boolean;
}
): StrictMaterial<T> {
const { viewScaleInfo, viewSizeInfo } = this.getViewInfo();
return createMaterial<T>(
type,
material || {},
opts?.viewCenter === true
? {
viewScaleInfo,
viewSizeInfo,
}
: undefined
);
}
updateMaterial(material: Material): ModifyRecord<'updateMaterial'> | null {
const data: Data = this.getData() || { materials: [] };
const id = material.id;
const position = getMaterialPositionFromList(id, data.materials);
const beforeMtrl = findMaterialFromListByPosition(position, data.materials);
if (!beforeMtrl) {
return null;
}
const before = toFlattenMaterial(beforeMtrl);
const updatedMaterial = updateMaterialInListByPosition(position, material, data.materials, {
onlyUpdateContent: true,
}) as Material;
const after = toFlattenMaterial(updatedMaterial);
const loader = this.#board.getRenderer().getLoader();
loader.resetMaterialAsset(material);
this.#resetData(data);
this.refresh();
const modifyRecord: ModifyRecord<'updateMaterial'> = {
type: 'updateMaterial',
time: Date.now(),
content: { method: 'updateMaterial', id, before, after },
};
return modifyRecord;
}
modifyMaterial(
material: RecursivePartial<Omit<Material, 'id'>> & Pick<Material, 'id'>
): ModifyRecord<'modifyMaterial'> | null {
const { id, ...restMaterial } = material;
const data: Data = this.getData() || { materials: [] };
const position = getMaterialPositionFromList(id, data.materials);
const beforeMtrl = findMaterialFromListByPosition(position, data.materials);
if (!beforeMtrl) {
return null;
}
const modifyRecord: ModifyRecord<'modifyMaterial'> = getModifyMaterialRecord({
modifiedMaterial: material,
beforeMaterial: beforeMtrl,
});
updateMaterialInListByPosition(position, restMaterial, data.materials) as Material;
const loader = this.#board.getRenderer().getLoader();
loader.resetMaterialAsset({ ...material, type: beforeMtrl.type });
this.#resetData(data);
this.refresh();
return modifyRecord;
}
modifyMaterials(
materials: Array<RecursivePartial<Omit<Material, 'id'>> & Pick<Material, 'id'>>
): ModifyRecord<'modifyMaterials'> | null {
const data: Data = this.getData() || { materials: [] };
let modifyRecord: ModifyRecord<'modifyMaterials'> | null = null;
const before: (FlattenLayout & { id: string })[] = [];
const after: (FlattenLayout & { id: string })[] = [];
materials.forEach((material) => {
const { id, ...restMaterial } = material;
const position = getMaterialPositionFromList(id, data.materials);
const beforeMtrl = findMaterialFromListByPosition(position, data.materials);
if (!beforeMtrl) {
return null;
}
const tempRecord = getModifyMaterialRecord({
modifiedMaterial: material,
beforeMaterial: beforeMtrl,
});
if (tempRecord.content) {
before.push({
...tempRecord.content.before,
id,
});
after.push({
...tempRecord.content.after,
id,
});
}
updateMaterialInListByPosition(position, restMaterial, data.materials) as Material;
});
modifyRecord = {
type: 'modifyMaterials',
time: Date.now(),
content: {
method: 'modifyMaterials',
before,
after,
},
};
this.#resetData(data);
this.refresh();
return modifyRecord;
}
addMaterial(
material: Material,
opts?: {
position: MaterialPosition;
}
): ModifyRecord<'addMaterial'> {
const data: Data = this.getData() || { materials: [] };
if (!opts || !opts?.position?.length) {
data.materials.push(material);
} else if (opts?.position) {
const position = [...(opts?.position || [])];
insertMaterialToListByPosition(material, position, data.materials);
}
const position: MaterialPosition = getMaterialPositionFromList(material.id, data.materials);
const modifyRecord: ModifyRecord<'addMaterial'> = {
type: 'addMaterial',
time: Date.now(),
content: { method: 'addMaterial', id: material.id, position, material: deepClone(material) },
};
this.#resetData(data);
this.refresh();
return modifyRecord;
}
deleteMaterial(id: string): ModifyRecord<'deleteMaterial'> {
const data: Data = this.getData() || { materials: [] };
const position = getMaterialPositionFromList(id, data.materials);
const material = findMaterialFromListByPosition(position, data.materials);
const modifyRecord: ModifyRecord<'deleteMaterial'> = {
type: 'deleteMaterial',
time: Date.now(),
content: { method: 'deleteMaterial', id, position, material: material ? deepClone(material) : null },
};
if (material) {
const loader = this.#board.getRenderer().getLoader();
loader.resetMaterialAsset(material);
}
deleteMaterialInList(id, data.materials);
this.#resetData(data);
this.refresh();
return modifyRecord;
}
moveMaterial(id: string, to: MaterialPosition): ModifyRecord<'moveMaterial'> {
const data: Data = this.getData() || { materials: [] };
const from = getMaterialPositionFromList(id, data.materials);
const modifyRecord: ModifyRecord<'moveMaterial'> = {
type: 'moveMaterial',
time: Date.now(),
content: { method: 'moveMaterial', id, from: [...from], to: [...to] },
};
const { materials: list } = moveMaterialPosition(data.materials, { from, to });
data.materials = list;
this.#resetData(data);
this.refresh();
return modifyRecord;
}
modifyLayout(layout: RecursivePartial<DataLayout> | null): ModifyRecord<'modifyLayout'> {
const data: Data = this.getData() || { materials: [] };
const modifyRecord: ModifyRecord<'modifyLayout'> = {
type: 'modifyLayout',
time: Date.now(),
content: {
method: 'modifyLayout',
before: null,
after: null,
},
};
if (layout === null) {
if (data.layout) {
modifyRecord.content.before = toFlattenLayout(data.layout);
delete data['layout'];
this.#resetData(data);
this.refresh();
return modifyRecord;
} else {
return modifyRecord;
}
}
const beforeLayout = data.layout;
let before: FlattenLayout = {};
const after: FlattenLayout = toFlattenLayout(layout);
if (data.layout) {
Object.keys(after).forEach((key: string) => {
let val = get(beforeLayout, key);
if (val === undefined && /(cornerRadius|strokeWidth)\[[0-9]{1,}\]$/.test(key)) {
key = key.replace(/\[[0-9]{1,}\]$/, '');
val = get(beforeLayout, key);
}
before[key] = val;
});
before = toFlattenLayout(before);
modifyRecord.content.before = before;
} else {
data.layout = {} as any;
}
modifyRecord.content.after = after;
mergeLayout(data.layout as DataLayout, layout) as DataLayout;
this.#resetData(data);
this.refresh();
return modifyRecord;
}
modifyGlobal(global: RecursivePartial<DataGlobal> | null) {
const data: Data = this.getData() || { materials: [] };
const modifyRecord: ModifyRecord<'modifyGlobal'> = {
type: 'modifyGlobal',
time: Date.now(),
content: {
method: 'modifyGlobal',
before: null,
after: null,
},
};
if (global === null) {
if (data.global) {
modifyRecord.content.before = toFlattenGlobal(data.global);
delete data['global'];
this.#resetData(data);
this.refresh();
return modifyRecord;
} else {
return modifyRecord;
}
}
const beforeGlobal = data.global;
let before: FlattenLayout = {};
const after: FlattenLayout = toFlattenGlobal(global);
if (data.global) {
Object.keys(after).forEach((key: string) => {
before[key] = get(beforeGlobal, key);
});
before = toFlattenGlobal(before);
modifyRecord.content.before = before;
} else {
data.global = {} as any;
}
modifyRecord.content.after = after;
mergeGlobal(data.global as DataGlobal, global) as DataGlobal;
this.#resetData(data);
this.refresh();
return modifyRecord;
}
}

View file

@ -8,3 +8,7 @@ export const CURSOR_DRAG_DEFAULT = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUg
export const CURSOR_DRAG_ACTIVE = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAER0lEQVRYhe2YT2hjRRjAf8lL22xsNsm6EWKrSKvuIkIh+O9QRFxEW18KUsoe7FHoRaWCN1FPetOrIHgVKS0q9P5OxaJbodkalgVrtVZjS7Ntd02z6abPw3yzmaT585q+elj2g2HmvZn35jffN/PNNwP35R6XgM/fuif4n+dO2klQvgsaZRc4NJJvoJbHdhrIAkJAN2ADHwFfAw9J3ZoB/b9I0AA6A0SBc0Aa2EVpSqddeZ+QdmfkO+u0gIPSQQR4HfhRQH4AHMDNZDJXXNd1M5nMFalzdB3wJTAOPAD0yEB9066G6wXepVZTd5MpTdporZ6jVqsatmMJoTR3HvgJQ1u2bS+3ArRte9l1XXdsbGyJo1pdBN6Wf3d3ChlAmSQO9LeC8fquQRpDWaerHWSjSr1iu4BkJyOsF9u2s67rkslkluTVxygltAVsJBZqdCngEj5osIlW+4EYytRNF04jeu3vulCT+7QkLH20dEOhumft97pQI4s3+iiRSPwtxVSbd39J8eEGvzFXc1NAs8KSFAZeBt4AHgNeBFDWObkEAne7HAK2gT2gCFQatdca1GbtBj4E3veFprVYVLXXcg4GqM6588BbAMlkcm1qamqzr6/v6ikBet5RgiizJoDPAXdgYGDZXHkzMzPrrk9CdRWngUeAsxxdCzVwpgZ/BigWiz1mo4mJif7jqMajeJrU5hywgGvAej6fvzA0NLThN1Eul9uT4g5VTbYFNKUIfAbsZ7PZvuHh4Wt+As7Ozu5IcY2j219TQB0NV4A7qODgO4CFhYWLg4ODOb8AHccpG4A68m6pRQ1YAQ6A28A+8BXwBcDq6upTqVTquh+AuVyuW4q/opRRaQdZD1gCbgE3ge+BT4HdfD7/ZDwe/z2bzRY6hRsfH1/e3Nx8FDX/sgbgoZfvg6jo4ixqW7oIPA+8CrwHrAJuJBLJd+JaHMf5k6qmPgGeQe1SCenXk0/U21xcIC8AzwGvAJPAEuAmEon1xcXFba9w8/Pz5oqdB14CnpY+oij35km0qwmjwqAU8ISM9hIwBeQA17KsG9PT07+1gxsZGdmgdqW+BjwLDAAPoo4ALU+W9arVwWoIpXp9kouKZpPAO8AwQCwW+2d0dLQ0OTkZSafT0UKhUJ6bm9t2HKeysrIS3tra0g7+KvABUJC0g5rrJdRcbLpImtleRzYashc1P2OSXwbebDVykX3gW+Ab1AHqhuQ3pe6AJlFMO0CoPROHDcio5I8DL1A9C8dQbmod+APYAK4DvwjQnsDdErgyVTfTEaCG1GFYGHXG7TVSo2OkdvhlAflXoHSuNdfStFqaRhEi2kfdprrj6M5LAt0I8EDaaMdflPal48CB95hMr3Bt8h4jD0kyL5E0pN6dysZzW7N2AqjbmhdIZjJvufTOpE19x3g+9s1XJ/ck5tVbfdhu+rxDfLiSO+lFToCjZwrXyH2/0Lwv95z8B1jAqXmDnj4YAAAAAElFTkSuQmCC`;
export const CURSOR_RESIZE_ROTATE = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAIiklEQVRYhe2YW2yUxxmGn7W96zXGNnZsr2FJHQyYBHNIU1ttAqVUVjlJUAtxQyUXhKgQktUDdSUkuEDtBVJ9UQXRC0RJRblrq/SCIARpFImWQ8VBIZQinJpQYozNyWaxiw/rfXsx3+z+6xNOe9tPGv2nOTzzffPPvDMhSXwJC1nKAXIDV/9ellLAqF1T9o5A2Ry7J5A/Fbh3mUMh8qYJ5kHCQATID6QIkBcAHAVGgKFAStq33EB53/ao5Rm064i9g0CmqcDyrNICYCZQBJTYdaa9j1jelDUwCAwAz4F+ex61DhZauXzr1CDwDHhq1wFg2Do1KWAo0NsCAyoDKoGYXcuBWdZY1PKnzAsDQB/wBOg1yKTVN8vqmoHz6nOgC7gHdFrbCasrNRFgyMCj5qVXgDlANfAaMA/4ir2rMI+MtSHgEdAN9BjsiNVZYR0sNq8+AtqB69Z2MuDBcYA+pAXW0yoDWgTUWXrd8k1l+cBcSxjIIBlPB63G8uUB/8Z57znwAkgGAX1YC4BSK7QIWA7UWwoDJJNJzp49y4ULF2hvb6e7u5tEIkFhYSGVlZXMmzePhoYGNmzYQElJCVZv2tPt7e3s2bOHvr4+9u3bx/r16+PWjg/zQ9x4BEk+5UkqklQt6ZuSdkn6jaRbMhsYGFBbW5uWLVvmp5MpU3V1tVpbW9XV1aWgbdy4MZ2npqbGvx6W9DtJ35e0RFJREDBX0gxJcUnfkPQDScck3fGlz549q7q6ummBjU2xWEzvvfdeGvCtt95Kf6uoqAiyfyBpt6SvSZrlAUOSIpLKJS2X9D1Jv5Z025d69913FQqFJmy8rKxMS5YsUUNDg5YvX67Zs2dPCrpjxw6NjIxoxYoV6XdVVVVBwI8l/UTS1yWVesBcC+18SRsk/ULS33yJtra2CRtramrSiRMn1NXVpWQyKUlKpVJ6+vSpTp48qZ07dyo/P39cua1bt6q+vn4ywL9IapX0tqQyDxiRFDPq3ZL+JCklSadOnRrXwMKFC3X69GlNx65du6aVK1eOqyMYjTGAf50IsNC8t1FSm6TPJOn+/fuKxWJZFb/zzjvjBvx0bO/evQqHwxNG4mUe9MtYMZk5bwHA4cOH6enpSc9BixYt4uTJk5SVlflXfcBN4DFuapoHLPQfz58/z9WrVwmFQsTjceLxOHfv3uUlNlZcpFeMEtzsXg2QSCQ4duxYulQoFOLQoUNBuE7gvAE+x82b/VZHyblz52hsbCSZTL4MCGWrqUECq0gQsBi3ts4B+PDDD3n48GG61OrVq1mzZo1/fGZw54HbVmGV1bMAWPbRRx9NC24CwD7cajLiAXNwIS7ELW3lAGfOnMmqZPv27cHHvwM3gH8AnwH/wnn0C+AuwLZt25g/f/5L4cLhMNu2bfOPnbh1+xkZ9UMebvmKGmQEoLOzM11JQUEBK1eu9I/JAFAPTqnk4hTNY9xS1V5TU1N7+fJlOjo6SKXS+jPLJFFUVMTixYsxmJtW/inOi2nAXDI/C0BWeMvKyojFYv6xz1LCKhnCRSEBPMCFfBaQKi0tfb2+vn5qFzp7AnwCXAM+t45mAUJGfgMwOpoWtIRCWZ/89BB8TuJ+lG5cNEatkXYgjtN+BYG2ktaxBE4U3AP+iRsud3EeHCTwk6Ss0IhvtaKiIk3Q19dHb28vhYWF4H6mYtxwiBrQIE4aPTHgIVzoO3E/TzlOV4YD3/stj9eMPj2xzg4HAYMSPQnkxePxNGB/fz+XLl1iy5Yt4HTeq+aZTtyA9h18Yb33G6aRQBqyDiUNLihmH1q54LAZ9ZHKISPRn1loaGxsJGjHjx8PPi6xtAgnNufgVHcRmf1J2DpTiJsjvYout3c5gQ49Ns89s3fpOdB70I+Hx7iBXrV27VqKi4tJJBKAm3auXLmCDfpy4G3rZSHur/aSPg83BCpx24MFuNUlbnkHcGMubEDdxpG0lBn8AcBBsgfsVysqKti6dStHjhwBYGRkhJaWFs6dO0ckEgGYj9v0xIA71rkha7jUgBYCb5hHGRoaIj8/f4Z5s8jafUpmN/diLBwAkmZKWiipSdKvJN2VpI6ODhUVFWUt7Js2bdLAwMBYLdAt6VNJlyV9IumLsRkOHjyo2tparVixQrdvp2Xmn00YrJI0R05VBRV+GjAiqcoUxA8lnfI1HD16dJz6WLVqla5fvz4tFdPZ2anm5uas8s3Nzf7zp5J+LmmNpFcl5U8GmCupWFKtpO9K+qWkG76W3bt3j4OMRqNqaWnRhQsXNDQ0lAWVTCZ169YtHThwQFVVVePKtrS0+Kw3pgsYso+Vkuol7ZD0W0ldvsHW1tZJZXxtba0aGxvV1NSkdevWaenSpYpGoxPmXb9+vXp6ejzgx5J+ZiGeLSk8GaD34ky5Hd23JP1I0h8kPQmGu7y8/L/aNEUiEe3fv1/Dw8Pp6Es6KqlZ0puSXpHbVU4KiPVglqQFkr4j6aeSfi/pvq/13r172rVrlyorK6cFFo1GtXnzZl25ciU4Ch5J+qOkH0v6tqTXzDk5EwGGlNFjOWQOd8px4vUN4E3cpn2pz/jgwQPef/99Ll68yJ07d+jt7WVwcJBIJEJJSQlz586loaGBTZs2UVdXF5w0OoDLwFWcbPsct6r04+bRLHEYCoWyAD1kBDfHleHmsxrccccS3NFHNV/eenBy6iZwCycMOnHz5wBuDh2nyyY6H0zhFmoF7vtxk+l9q3ieQfrDo8msF7cy3cN56g7ZWnKcep7IxnrQmz+wHKu2K3EKJWbPpWSUTQ4ZtRJc23sC6ZGB9ZM52JwcboIQjzUvZvNxYfeHlyVkZFcBbux6wKA6ShjoM5yM8uH0Xpuy8ekAQuZc2W8P8nEei9p9mMmPgAfJHO0O27e0lHppw9MEDJo//A4eoE91iB48SJ80lFMB/t/+V/sPGZfTmtMFR4EAAAAASUVORK5CYII=`;
export const CURSOR_PEN = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAuoSURBVHgB7VpnTJVNFh6KIMUudhTsvaGuigV1bVFji73v6q7u6mbtLa49atQYjb1+cdXYe8eNJvao+FlxLWBDBRX1Q0BEmH2euTPXC94LXMDv157k5JW575w5Z+ac55wzry7CPrmCpcicsvLOTyUXO39T+dSgoCCvqlWr1nZ1dQ1wcXFxT0lJ+fj169cYPN8lJiZGnThx4qvNPKnn/u4GuaT7txs4pXr16j5Hjhz5c/ny5f+JvwP078ngB+Dw9+/f/7po0aKnT548eevm5hadlJQUc/jw4Rj93u9qhIvNU7mNn5+f99WrV8cGBgb+6/Xr1+5hYWECuy6KFy+uuFixYsLb29vM/wS+S164cGHY9evXX+fJk+f9ly9foj9+/Bh97ty5+J9tkIuN8gLKe126dOkfFStWnPnq1SuP8ePHy507dyoj8+XLJ2rWrCnq1q0rGjduLPz9/UXRokWVUXzC1YRWNgJ8LzU19c7s2bMfhYeHR+OUfgNFHD9+PFp8dzfzfo6JK3uUKVPG6/bt25OklEnv3r2TrVu3TuUCDRs2lO3bt5fVqlWT+fPnl3pRxaVLl5ZdunSRM2fOlLt27ZIXL16Ujx49klBW2lASOBK8ed68eYO6d+9eU6/pYvPMEeWhIARlPywSh6OnUkp58oYNG5QWL1++lPBzuXjxYjlw4EDZokULWalSJenj42M1yNPTU9aqVUsOGTJErlixQp46dUriROXDhw8l3NAYdAnu9td27doFpjMk2+QF9rx37955Sh86dKhSpmTJkhLoI+Ei8tixY9IecbePHj0qoZAcMGCAbNasmUTgSy8vL6tR/HeDBg0k3CkVMSGTk5PN9JMLFixobTZQWAAkW+Q9YcKEQEBj+OfPnyX8X7q7u8tt27bJ4cOHKyUQG0pRQ/BvuwZxnLvNd4FSslevXupEKI9yEPxy2rRp8vHjx2bK4+XLl/fWm+gmsmlEAfi5/9OnT38FzsvatWsrV4iMjJSAR+uJlChRQrmEI3JkVHR0tFy/fr0ypmDBgkpWvXr15OnTp41PxS5ZsqQHxvOC3UU23MmPRpw8eTKU0hiUXOTQoUNKelxcnBw8eLAaK1u2rKQbZJd4Mp06dVKyaMzu3buN1eGzZs1qhnEPYXEppwKbBpTYu3fveghKmj59ulpg/vz5EnhuNYKBy3G62Pnz551S3PZ0EhIS5KhRo5QsX19fbpQ6iW/fvh3r2LFjBW2AuzMGFAWXWbly5TQeJ+HQw8ND9u7dW8bExFgX/vDhg+zXr5/ViCtXrkhnyRgCZa1GlCpVSt68eVONAwmnIt8U0SeRZVfihLII5L7w+Rf3799X+I46SKGM7cKfPn1SvsyFkdTktWvXpLNka0Tfvn2VrP79+yvZoHc46RCMeWojsuRKhcDloHAQAvc+AxnZVp3CjRs3fliYQYlkpBauUaOGRKkhnSUji0BRpUoVJQu1lxo8e/bsdEB3MWEJ6iydQn6eANg/NDRUOXfnzp2VUBxpmoVNMqJrmWAnoiCDK3y/e/euhAy5f/9+eeDAAXnnzp0MIZc0depUJYebwgoA4zdDQkIaawOydAq+4FI8BQTyvyEz2QgFvNkmnjRGvHjxQpUYfA9lh5wxY4YqN5j8hE5i3F3ElgIBR/TmzRvlrswVly9fVrYB9fpovbyycgosLXlkZdetWzcLAj7u2LFDCRw0aJDalfRkjKALtGrVyqowy4qmTZvKnj17qlLDGDN27FgrotkjA9MbN25Ux4IcMQMFIjfVR2QBkXhUDORykydPHozgiqIrEB2Y1FDz213UGDFlyhSlKBaUmzZtkowhUnx8vFy1apVKgFQOp+vQAM5DxaqSJgtBGHsQybUu5uUTFljNkOhnBWkAurA/PHv27AEzMEuAvHnzWiHOHhE5iEY0YM+ePdZxW7/fsmWLRKktW7ZsKZ8/f25XDtGMNRfWV+9gE27gBFsIS3xygx3GgWofNRN1XsMtYoFAAlAqsBMCLuTQciQ0AYNF8+bNBRS0jsMg67/hhqJ+/foCpbZAj2FXTkBAgEBmVr+jJhM4DV+tuJvIJAZM804DvnEA7eIzPFLq1KmjXkBDIrBJdicDjQRcRcDVBOqnH37nPCgjihQpIuCaAlnYrhw2S5yPExXYfW6AH+YZ38/QiB8MgJAneMSz+0IgC0CkQI9gdzIXpYIwWrWd6cmcBJVXmrjZLzY5TmOBeOrUMa8AKuFWcKuCWj9OdHFkQKoNC7hQBGOQu8qdu3XrltoZewTEEegbBPoFgWrW7jvsqSmjXLlyqv20RzQUoKEMWLNmjXj79q0rsvM4oNIQIJuPja4OiYGsMjJgMRjN/CPCHm4nVCAzUTmikSNHKpQh5MKINIHMv5kj+PvSpUt/yCm2dOHCBQUcfBc1VyozPihpzpw5/YUlJ2RYpdLfTEYOgLBLnN21a9c0pbU9ioqKUpjP95gT2IISMpctWybRZ6vx4OBgiV21GpaeDCTTCAS8mjNp0qRUQjLe3wP5lfQmO8wJPB4eFZNHwPbt21dCXgLxmRDIdjE2NtbhwsygLD9ss7DQ7WSHDh0c5hJbMqeDZKrksOJlMYk1wtq0adNEWFDJ4SlwkGmbGTlwxIgRfQFnr5iFWQ6wQ8Ndkd2FjRFMXGvXrpV9+vRRxrAc37p1q6o6MyNzKgADa1amHOYZuHJokyZNGmkP8czIAP5YGFweeaA6kssFCh03bpwSOGzYMKVkRgpkh8xc9hvGZdn5mQSK3nw2YLaysFQLDg0wccDUXQZcGWXFOOxeLCtPZlsKZm+bm2SU51WOKdF5G4IbPjUO8AgFcrXWsVk0MwMYB6awqwr0qoPs+R8K2rx5s6pV2MeyVM5N5ekmptNjOaErUokEer1ChQrdMF5DWO5nmRMyLK+NG6m6iBOxK8MAZwobJ06cqBZBbpAI8lxxG5bZRnkWg0QhrXwYlGdJ3YCbCWYC8RVZqEyZ8Xz0hOrgarg2XASZ8YQ0YwQucHlRJSMiIqSzZJTnHZS5dypUqJA8c+aMGn/w4MGtypUrD8I4kYf1jD+4gN7cTHsDF31MnEC/qwtlGyE7boFsVScT3wsXLqwW5o0bL8CwaIZoY5Q2T4KBaerplrjSUeO48LqLpmgwxoPB9bTrEFjMxVeWyE1PYNDwmiMIeB4MfP4FCiRwIS5oMqzQtxTImCqBsZVknPB2g9cvxjCjPK9VRo8erbCel8Xmxg9ZOxxg8SetfH1woNaBcenUXRFfpK/R5+hKVcANUVK0xOXTSmReFRMMPp4GkozyX2GTwAzzKpGGGGJ5Qlg2ytNYrfx/cZpUvrlWvrxW3kdkoaGxR+rKXVhgtaQ2go12IySpv+Oe/yyaHtVrMoPiJkHiWkb26NFDduvWTbWUvIEzih48eFApz76ZmZ0XWvv27VPKowd4gkT1F8huppWvkE75bF+/0wgGDjMgSwwmE2bEYGTmdiji5uDW4gJKjCjokWjP903Q856JxR4zOvtmfDRRvwPhInEDMVJ833mnlc/sBVctiIYwLuhW+fQCrlDIBzteCztYDUpWRax40efZJ7Rt27YKTsB7zJgxYvXq1UoYK2PEkkBtxWYoEkYtRgPPT1TsdNh0/AZOBCcJS3+SK19wGNQeWnnuDtGJ2BwkLAHHXjJE8x/BHcGd5s6d+wsBh/013YsVqWns2fWhyPubsOw85XDn/fQaWb6Rc4Z4EgxsngIhlsHNZMfYIFY31MaEgNuBO4PboyReg94iEjobjE3E1eUdxIcj5Z32eWctNR8haIynfnrYefI3wl8CdroegrolklUxXGI9xskcg+/zpoBuwlYvR26TnaMy37RsjXGzYQ/NXkiCPkAp9e1ZK2a+iVHhOM058vmc+pqrSPuhzrgaXYENiKf43oiY/ptf+D+Dv2jm38kimwGb28FijLF1qTwirQFUlkqbXc8R2uR6tNvIdUvH5v9SpKTjHEHlzzLAVn76j9mm1EgV/ych/ge/lJWo0YnclAAAAABJRU5ErkJggg==`;
export const CURSOR_PLUS = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARySURBVHgBxVfNSyRXEK+Z6flwmYxBiGCChnyIMRJEiZBLQIjxIl4k6CV42UsC+QMM/gkJ5JJzxAQNCnvwEFAvHjwk7kGiGI1iiOIHe1hddZzpnunp7v1V9yu7nZ1xehaXLShef7x69avfq1fVTfSaJUL1C9tEoXaVdzbVIVGqT9hBHGrNzc19sbOz82hra+vPg4OD7zs6OtLqfYxeLrBQokGj8/PzX1mWteP4YpycnPzQ2dnZInPoFQhHlmIH29vbf7DXiYkJZ3Bw0Dk8POTbbH9//2fkMaSFXTQs0gj59Gu2bb/LD5eWlmh5eZnOz8/5Nh2Lxd4MAAi1DWEBOOQxwAtriJavCUAoOBaLRWYoQXVsQ1iqJPNvJVg06vmIRLxHAMbRs9p0zwzIFjCABBxVW1wjP/p734LakzxgoqFsqm1BJDA65EfGDFSNDlsRU3NY48pWgASvKwIQii26DUBAGHKPGmBVApDP53MYstAi1CwLIrgmix0EIEesODs7+2VPT883pVLpA44WUQnqGCiO4rmDqvd+JQCojj/pun6N5HRwMhyMrhO2wzr5dDq9MDAw8PP+/n5OBWsHnVeqcHdKX1+fS+ve3h7f2iFMjOPj4x+DFVMLUO90dXU9BOqPJicnaXFx0T3fcsRe2DtNI/QB93p4eJgQXUTqQbnwjjU1NdH09HSyra3t2+bm5kfoI0/LAbDDt3hcW1uj9fV1CiMMZHd3t+a8ZDJJ2SynBzWAiTcUA7acWbfKGYbxF8buqakp7ezs7E4GMpkMDQ0Nuc6RN9Td3U2FQqHiXDikeDxOyB1CDv1jmuY1Y4K6Brz/jdB3WltbP11dXf0Fk3Ih9tPp7e11cwBbUTMHEIwNBv4eHR39Gmx8CDtmu4EZ4EV485yjo6OrsbGx31taWh4DcQZ2ScfLYGbJHS8vL42FhYXv2tvbmzkyFvQAHiLj4+O/bm5uHqZSKffDhB3DxoaW4F/HMT0B2H28K4lPAWApOlKnp6dPoLxZDeQ1FiksmmLLyOVyRiWq0ab/29jY4MxkAKZyYqlrtskrlTrh5oCtEPHDa2VQUgYJ8pOU53K3M+V8lwvaMdtkAwFZARUQXAN0dW9JISqRX6HEOPhh4TYh6ANmDACsKgB48QsVjDiR9QVEsZwBqXRCmUk+7cE2nFKLcFZXbDQAVlAM8ChU3+RYgI1bOUDkNwpHvZSGI6xoylCeV/vyFZp1BaBQtnZQbxYOirwop9hWztlBJKKKg/QkIURtjam0oBi4U+r5HrihEsIRUmNjo1vhEomEzCvRbcprSthPsiB1sYuLi8cYP56ZmdGurq4IzYVQRf/F8XxGFWi+D2HKOQmboO+hUH2+srLyGyqmDjYsgNgcGRl5CCY+wfu3oVzrY2EXDivMc4NaPAMQrSxIhwQif4oK9z+en5F3CriecA7U/E2rB4AcxQcKBI/udwR5SStFKKuuayagLFqPyOeVnGmOkCOVY6crx5KMNSX0L5QSqWyOutbVvRQXk/wiE0qeA2qKq4lnbJY2AAAAAElFTkSuQmCC`;

View file

@ -1,7 +1,26 @@
import type { UtilEventEmitter, CoreEventMap } from '@idraw/types';
import { limitAngle, loadImage, parseAngleToRadian } from '@idraw/util';
import { CURSOR, CURSOR_RESIZE, CURSOR_DRAG_DEFAULT, CURSOR_DRAG_ACTIVE, CURSOR_RESIZE_ROTATE } from './cursor-image';
import { coreEventKeys } from '../config';
import {
limitAngle,
loadImage,
parseAngleToRadian,
createId,
addClassName,
removeClassName,
injectStyles,
} from '@idraw/util';
import {
CURSOR,
CURSOR_RESIZE,
CURSOR_DRAG_DEFAULT,
CURSOR_DRAG_ACTIVE,
CURSOR_RESIZE_ROTATE,
CURSOR_PEN,
CURSOR_PLUS,
} from './cursor-image';
import { coreEventKeys } from '../static';
const key = `idraw-core-cursor`;
const ID = `${key}-${createId()}`;
export class Cursor {
#eventHub: UtilEventEmitter<CoreEventMap>;
@ -13,8 +32,12 @@ export class Cursor {
'drag-default': CURSOR_DRAG_DEFAULT,
'drag-active': CURSOR_DRAG_ACTIVE,
'rotate-0': CURSOR_RESIZE,
rotate: CURSOR_RESIZE_ROTATE
rotate: CURSOR_RESIZE_ROTATE,
pen: CURSOR_PEN,
plus: CURSOR_PLUS,
};
#classNameMap: Record<string, string> = {};
constructor(
container: HTMLDivElement,
opts: {
@ -25,13 +48,30 @@ export class Cursor {
this.#eventHub = opts.eventHub;
this.#init();
this.#loadResizeCursorBaseImage();
Object.keys(this.#cursorImageMap).forEach((cursorKey: string) => {
const className = `${ID}-${cursorKey}`;
this.#classNameMap[cursorKey] = className;
const image = this.#cursorImageMap[cursorKey];
this.#injectCursorStyle(cursorKey, className, image);
});
}
#injectCursorStyle(cursorKey: string, className: string, image: string) {
const { offsetX, offsetY } = this.#getCursorOffset(cursorKey);
injectStyles({
rootClassName: className,
type: 'element',
styles: {
cursor: `image-set(url(${image})2x) ${offsetX} ${offsetY}, auto`,
},
});
}
#init() {
const eventHub = this.#eventHub;
this.#resetCursor('default');
eventHub.on(coreEventKeys.CURSOR, (e) => {
if (e.type === 'over-element' || !e.type) {
if (e.type === 'over-material' || !e.type) {
this.#resetCursor('auto');
} else if (e.type === 'resize-rotate') {
this.#resetCursor('rotate');
@ -41,6 +81,10 @@ export class Cursor {
this.#resetCursor('drag-default');
} else if (e.type === 'drag-active') {
this.#resetCursor('drag-active');
} else if (e.type === 'pen') {
this.#resetCursor('pen');
} else if (e.type === 'plus') {
this.#resetCursor('plus');
} else {
this.#resetCursor('auto');
}
@ -53,30 +97,41 @@ export class Cursor {
this.#resizeCursorBaseImage = img;
})
.catch((err) => {
// eslint-disable-next-line no-console
console.error(err);
});
}
#getCursorOffset(cursorKey: string) {
let offsetX = 0;
let offsetY = 0;
if (cursorKey.startsWith('rotate-') && this.#cursorImageMap[cursorKey]) {
offsetX = 10;
offsetY = 10;
} else if (cursorKey === 'rotate') {
offsetX = 10;
offsetY = 10;
} else if (cursorKey === 'plus') {
offsetX = 5;
offsetY = 3;
}
return {
offsetX,
offsetY,
};
}
#resetCursor(cursorKey: string) {
if (this.#cursorType === cursorKey) {
return;
}
this.#cursorType = cursorKey;
const image = this.#cursorImageMap[this.#cursorType] || this.#cursorImageMap['auto'];
let offsetX = 0;
let offsetY = 0;
if (cursorKey.startsWith('rotate-') && this.#cursorImageMap[this.#cursorType]) {
offsetX = 10;
offsetY = 10;
} else if (cursorKey === 'rotate') {
offsetX = 10;
offsetY = 10;
}
if (cursorKey === 'default') {
this.#container.style.cursor = 'default';
} else {
this.#container.style.cursor = `image-set(url(${image})2x) ${offsetX} ${offsetY}, auto`;
}
const container = this.#container;
const currentClassName = this.#classNameMap[cursorKey] || this.#classNameMap['auto'];
const allClassNames: string[] = Object.keys(this.#classNameMap).map((name) => this.#classNameMap[name]);
removeClassName(container, allClassNames);
addClassName(container, [currentClassName]);
}
#setCursorResize(e: CoreEventMap[typeof coreEventKeys.CURSOR]) {
@ -98,7 +153,7 @@ export class Cursor {
} else if (e.type === 'resize-top-left') {
totalAngle += 315;
}
totalAngle += limitAngle(e?.element?.angle || 0);
totalAngle += limitAngle(e?.material?.angle || 0);
if (Array.isArray(e.groupQueue) && e.groupQueue.length > 0) {
e.groupQueue.forEach((group) => {
totalAngle += limitAngle(group.angle || 0);
@ -110,8 +165,8 @@ export class Cursor {
}
#appendRotateResizeImage(angle: number): string {
const key = `rotate-${angle}`;
if (!this.#cursorImageMap[key]) {
const cursorKey = `rotate-${angle}`;
if (!this.#cursorImageMap[cursorKey]) {
const baseImage = this.#resizeCursorBaseImage;
if (baseImage) {
const canvas = document.createElement('canvas');
@ -119,7 +174,7 @@ export class Cursor {
const h = baseImage.height;
const center = {
x: w / 2,
y: h / 2
y: h / 2,
};
canvas.width = w;
canvas.height = h;
@ -137,9 +192,13 @@ export class Cursor {
ctx.translate(-center.x, -center.y);
const base = canvas.toDataURL('image/png');
this.#cursorImageMap[key] = base;
this.#cursorImageMap[cursorKey] = base;
const className = `${ID}-${cursorKey}`;
this.#classNameMap[cursorKey] = className;
this.#injectCursorStyle(cursorKey, className, base);
}
}
return key;
return cursorKey;
}
}

View file

@ -1,481 +1,16 @@
import type {
Data,
PointSize,
CoreOptions,
Middleware,
ViewSizeInfo,
CoreEventMap,
ViewScaleInfo,
LoadItemMap,
ElementType,
RecursivePartial,
Element,
ModifyRecord,
ElementPosition,
DataLayout,
FlattenLayout,
DataGlobal
} from '@idraw/types';
import {
deepClone,
createElement,
getElementPositionFromList,
toFlattenElement,
deleteElementInList,
findElementFromListByPosition,
updateElementInListByPosition,
insertElementToListByPosition,
moveElementPosition,
toFlattenLayout,
toFlattenGlobal,
get,
mergeLayout,
mergeGlobal
} from '@idraw/util';
import { Board, Sharer, Calculator } from './board';
import { createBoardContent, validateElements } from '@idraw/util';
import { Cursor } from './cursor/cursor';
import { getModifyElementRecord } from './record';
export { coreEventKeys } from './config';
export type { CoreEventKeys } from './config';
export { Board, Sharer, Calculator };
// export { MiddlewareSelector } from './middleware/selector';
export { MiddlewareSelector } from './middlewares/selector';
export { MiddlewareScroller } from './middlewares/scroller';
export { coreEventKeys } from './static';
export type { CoreEventKeys } from './static';
export { Board, Sharer, Calculator } from './board';
export { MiddlewareCreator, getMiddlewareCreatorStyles } from './middlewares/creator';
export { MiddlewareSelector, getMiddlewareSelectorStyles } from './middlewares/selector';
export { MiddlewareScroller, getMiddlewareScrollerStyles } from './middlewares/scroller';
export { MiddlewareScaler } from './middlewares/scaler';
export { MiddlewareRuler } from './middlewares/ruler';
export { MiddlewareTextEditor } from './middlewares/text-editor';
export { MiddlewareRuler, getMiddlewareRulerStyles } from './middlewares/ruler';
export { MiddlewareTextEditor, getMiddlewareTextEditorStyles } from './middlewares/text-editor';
export { MiddlewareDragger } from './middlewares/dragger';
export { MiddlewareInfo } from './middlewares/info';
export { MiddlewareInfo, getMiddlewareInfoStyles } from './middlewares/info';
export { MiddlewareLayoutSelector } from './middlewares/layout-selector';
export { MiddlewarePointer } from './middlewares/pointer';
export class Core<E extends CoreEventMap = CoreEventMap> {
#board: Board<E>;
// #opts: CoreOptions;
#canvas: HTMLCanvasElement;
#container: HTMLDivElement;
constructor(container: HTMLDivElement, opts: CoreOptions) {
const { devicePixelRatio = 1, width, height, disableWatcher = false } = opts;
// this.#opts = opts;
this.#container = container;
const canvas = document.createElement('canvas');
canvas.setAttribute('tabindex', '0');
this.#canvas = canvas;
this.#initContainer();
container.appendChild(canvas);
const boardContent = createBoardContent(canvas, { width, height, devicePixelRatio });
const board = new Board<E>({ boardContent, container, disableWatcher });
const sharer = board.getSharer();
sharer.setActiveViewSizeInfo({
width,
height,
devicePixelRatio,
contextWidth: width,
contextHeight: height
});
this.#board = board;
this.resize(sharer.getActiveViewSizeInfo());
const eventHub = board.getEventHub();
new Cursor(container, {
eventHub
});
}
isDestroyed() {
return this.#board.isDestroyed();
}
destroy() {
this.#board.destroy();
this.#canvas.remove();
}
#initContainer() {
const container = this.#container;
container.style.position = 'relative';
}
use<C extends any = any>(middleware: Middleware<any, any, any>, config?: C) {
this.#board.use<C>(middleware, config);
}
disuse(middleware: Middleware<any, any, any>) {
this.#board.disuse(middleware);
}
resetMiddlewareConfig<C extends any = any>(middleware: Middleware<any, any, any>, config?: Partial<C>) {
this.#board.resetMiddlewareConfig(middleware, config);
}
#resetData(data: Data) {
validateElements(data?.elements || []);
this.#board.setData(data);
}
setData(data: Data) {
const loader = this.#board.getRenderer().getLoader();
loader.reset();
this.#resetData(data);
}
getData(): Data | null {
return this.#board.getData();
}
scale(opts: { scale: number; point: PointSize }) {
this.#board.scale(opts);
const viewer = this.#board.getViewer();
viewer.drawFrame();
}
resize(newViewSize: Partial<ViewSizeInfo>) {
const board = this.#board;
const sharer = board.getSharer();
const viewSizeInfo = sharer.getActiveViewSizeInfo();
board.resize({
...viewSizeInfo,
...newViewSize
});
}
clear() {
this.#board.clear();
}
on<T extends keyof E>(name: T, callback: (e: E[T]) => void) {
const eventHub = this.#board.getEventHub();
eventHub.on(name, callback);
}
off<T extends keyof E>(name: T, callback: (e: E[T]) => void) {
const eventHub = this.#board.getEventHub();
eventHub.off(name, callback);
}
trigger<T extends keyof E>(name: T, e: E[T]) {
const eventHub = this.#board.getEventHub();
eventHub.trigger(name, e);
}
getViewInfo(): { viewSizeInfo: ViewSizeInfo; viewScaleInfo: ViewScaleInfo } {
const board = this.#board;
const sharer = board.getSharer();
const viewSizeInfo = sharer.getActiveViewSizeInfo();
const viewScaleInfo = sharer.getActiveViewScaleInfo();
return {
viewSizeInfo,
viewScaleInfo
};
}
refresh() {
this.#board.getViewer().drawFrame();
}
forceRender() {
const renderer = this.#board.getRenderer();
const calculator = renderer.getCalculator();
const loader = renderer.getLoader();
const data = this.getData();
if (data) {
const { viewScaleInfo, viewSizeInfo } = this.getViewInfo();
calculator.resetVirtualFlatItemMap(data, {
viewScaleInfo,
viewSizeInfo
});
}
loader.reset();
this.refresh();
}
setViewScale(opts: { scale: number; offsetX: number; offsetY: number }) {
this.#board.updateViewScaleInfo(opts);
}
getLoadItemMap(): LoadItemMap {
return this.#board.getRenderer().getLoadItemMap();
}
onBoardWatcherEvents() {
this.#board.onWatcherEvents();
}
offBoardWatcherEvents() {
this.#board.offWatcherEvents();
}
createElement<T extends ElementType = ElementType>(
type: T,
element: RecursivePartial<Element<T>>,
opts?: {
viewCenter?: boolean;
}
): Element<T> {
const { viewScaleInfo, viewSizeInfo } = this.getViewInfo();
return createElement<T>(
type,
element || {},
opts?.viewCenter === true
? {
viewScaleInfo,
viewSizeInfo
}
: undefined
);
}
updateElement(element: Element): ModifyRecord<'updateElement'> | null {
const data: Data = this.getData() || { elements: [] };
const uuid = element.uuid;
const position = getElementPositionFromList(uuid, data.elements);
const beforeElem = findElementFromListByPosition(position, data.elements);
if (!beforeElem) {
return null;
}
const before = toFlattenElement(beforeElem);
const updatedElement = updateElementInListByPosition(position, element, data.elements, { strict: true }) as Element;
const after = toFlattenElement(updatedElement);
const loader = this.#board.getRenderer().getLoader();
loader.resetElementAsset(element);
this.#resetData(data);
this.refresh();
const modifyRecord: ModifyRecord<'updateElement'> = {
type: 'updateElement',
time: Date.now(),
content: { method: 'updateElement', uuid, before, after }
};
return modifyRecord;
}
modifyElement(
element: RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>
): ModifyRecord<'modifyElement'> | null {
const { uuid, ...restElement } = element;
const data: Data = this.getData() || { elements: [] };
const position = getElementPositionFromList(uuid, data.elements);
const beforeElem = findElementFromListByPosition(position, data.elements);
if (!beforeElem) {
return null;
}
const modifyRecord: ModifyRecord<'modifyElement'> = getModifyElementRecord({
modifiedElement: element,
beforeElement: beforeElem
});
updateElementInListByPosition(position, restElement, data.elements) as Element;
const loader = this.#board.getRenderer().getLoader();
loader.resetElementAsset({ ...element, type: beforeElem.type });
this.#resetData(data);
this.refresh();
return modifyRecord;
}
modifyElements(
elements: Array<RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>>
): ModifyRecord<'modifyElements'> | null {
const data: Data = this.getData() || { elements: [] };
let modifyRecord: ModifyRecord<'modifyElements'> | null = null;
const before: (FlattenLayout & { uuid: string })[] = [];
const after: (FlattenLayout & { uuid: string })[] = [];
elements.forEach((element) => {
const { uuid, ...restElement } = element;
const position = getElementPositionFromList(uuid, data.elements);
const beforeElem = findElementFromListByPosition(position, data.elements);
if (!beforeElem) {
return null;
}
const tempRecord = getModifyElementRecord({
modifiedElement: element,
beforeElement: beforeElem
});
if (tempRecord.content) {
before.push({
...tempRecord.content.before,
uuid
});
after.push({
...tempRecord.content.after,
uuid
});
}
updateElementInListByPosition(position, restElement, data.elements) as Element;
});
modifyRecord = {
type: 'modifyElements',
time: Date.now(),
content: {
method: 'modifyElements',
before,
after
}
};
this.#resetData(data);
this.refresh();
return modifyRecord;
}
addElement(
element: Element,
opts?: {
position: ElementPosition;
}
): ModifyRecord<'addElement'> {
const data: Data = this.getData() || { elements: [] };
if (!opts || !opts?.position?.length) {
data.elements.push(element);
} else if (opts?.position) {
const position = [...(opts?.position || [])];
insertElementToListByPosition(element, position, data.elements);
}
const position: ElementPosition = getElementPositionFromList(element.uuid, data.elements);
const modifyRecord: ModifyRecord<'addElement'> = {
type: 'addElement',
time: Date.now(),
content: { method: 'addElement', uuid: element.uuid, position, element: deepClone(element) }
};
this.#resetData(data);
this.refresh();
return modifyRecord;
}
deleteElement(uuid: string): ModifyRecord<'deleteElement'> {
const data: Data = this.getData() || { elements: [] };
const position = getElementPositionFromList(uuid, data.elements);
const element = findElementFromListByPosition(position, data.elements);
const modifyRecord: ModifyRecord<'deleteElement'> = {
type: 'deleteElement',
time: Date.now(),
content: { method: 'deleteElement', uuid, position, element: element ? deepClone(element) : null }
};
if (element) {
const loader = this.#board.getRenderer().getLoader();
loader.resetElementAsset(element);
}
deleteElementInList(uuid, data.elements);
this.#resetData(data);
this.refresh();
return modifyRecord;
}
moveElement(uuid: string, to: ElementPosition): ModifyRecord<'moveElement'> {
const data: Data = this.getData() || { elements: [] };
const from = getElementPositionFromList(uuid, data.elements);
const modifyRecord: ModifyRecord<'moveElement'> = {
type: 'moveElement',
time: Date.now(),
content: { method: 'moveElement', uuid, from: [...from], to: [...to] }
};
const { elements: list } = moveElementPosition(data.elements, { from, to });
data.elements = list;
this.#resetData(data);
this.refresh();
return modifyRecord;
}
modifyLayout(layout: RecursivePartial<DataLayout> | null): ModifyRecord<'modifyLayout'> {
const data: Data = this.getData() || { elements: [] };
const modifyRecord: ModifyRecord<'modifyLayout'> = {
type: 'modifyLayout',
time: Date.now(),
content: {
method: 'modifyLayout',
before: null,
after: null
}
};
if (layout === null) {
if (data.layout) {
modifyRecord.content.before = toFlattenLayout(data.layout);
delete data['layout'];
this.#resetData(data);
this.refresh();
return modifyRecord;
} else {
return modifyRecord;
}
}
const beforeLayout = data.layout;
let before: FlattenLayout = {};
const after: FlattenLayout = toFlattenLayout(layout);
if (data.layout) {
Object.keys(after).forEach((key: string) => {
let val = get(beforeLayout, key);
if (val === undefined && /(borderRadius|borderWidth)\[[0-9]{1,}\]$/.test(key)) {
key = key.replace(/\[[0-9]{1,}\]$/, '');
val = get(beforeLayout, key);
}
before[key] = val;
});
before = toFlattenLayout(before);
modifyRecord.content.before = before;
} else {
data.layout = {} as any;
}
modifyRecord.content.after = after;
mergeLayout(data.layout as DataLayout, layout) as DataLayout;
this.#resetData(data);
this.refresh();
return modifyRecord;
}
modifyGlobal(global: RecursivePartial<DataGlobal> | null) {
const data: Data = this.getData() || { elements: [] };
const modifyRecord: ModifyRecord<'modifyGlobal'> = {
type: 'modifyGlobal',
time: Date.now(),
content: {
method: 'modifyGlobal',
before: null,
after: null
}
};
if (global === null) {
if (data.global) {
modifyRecord.content.before = toFlattenGlobal(data.global);
delete data['global'];
this.#resetData(data);
this.refresh();
return modifyRecord;
} else {
return modifyRecord;
}
}
const beforeGlobal = data.global;
let before: FlattenLayout = {};
const after: FlattenLayout = toFlattenGlobal(global);
if (data.global) {
Object.keys(after).forEach((key: string) => {
before[key] = get(beforeGlobal, key);
});
before = toFlattenGlobal(before);
modifyRecord.content.before = before;
} else {
data.global = {} as any;
}
modifyRecord.content.after = after;
mergeGlobal(data.global as DataGlobal, global) as DataGlobal;
this.#resetData(data);
this.refresh();
return modifyRecord;
}
}
export { MiddlewarePathEditor, getMiddlewarePathEditorStyles } from './middlewares/path-editor';
export { MiddlewarePathCreator, getMiddlewarePathCreatorStyles } from './middlewares/path-creator';
export { Core } from './core';

View file

@ -0,0 +1,15 @@
import type { UtilEventEmitter, CoreEventMap, CoreEventChange } from '@idraw/types';
import { coreEventKeys } from '../static';
type EventHub = UtilEventEmitter<CoreEventMap>;
export function triggerChangeEvent(eventHub: EventHub, e: CoreEventChange, status?: 'continuous' | 'all') {
if (status === 'continuous') {
eventHub.trigger(coreEventKeys.CHANGING, e);
} else if (status === 'all') {
eventHub.trigger(coreEventKeys.CHANGING, e);
eventHub.trigger(coreEventKeys.CHANGE, e);
} else {
eventHub.trigger(coreEventKeys.CHANGE, e);
}
}

View file

@ -0,0 +1,79 @@
import { ATTR_VALID_WATCH, createHTMLElement, assembleHTMLElement, setHTMLCSSProps } from '@idraw/util';
import type { Point } from '@idraw/types';
import { classNameMap } from './static';
function destroyBoxs($root: HTMLDivElement | null, opts: { className: string }) {
if (!$root) {
return;
}
const { className } = opts;
// clear existed hover box
const $prevBoxs = Array.from($root.getElementsByClassName(className));
$prevBoxs.forEach(($box) => {
$box.remove();
});
}
export function initRoot(opts: { rootClassName: string; $container: HTMLElement }) {
const { rootClassName, $container } = opts;
const create = createHTMLElement;
const $root = create(
'div',
{
className: rootClassName,
[ATTR_VALID_WATCH]: 'true',
},
[
// create('div', { className: classNameMap.creationAreaBox, [ATTR_VALID_WATCH]: 'true' })
]
);
$container.appendChild($root);
return $root;
}
export function getCreationAreaBox($root: HTMLDivElement) {
const $boxs = $root.getElementsByClassName(classNameMap.creationAreaBox);
if ($boxs[0]) {
return $boxs[0] as HTMLElement;
}
const $box = createHTMLElement('div', { [ATTR_VALID_WATCH]: 'true', className: classNameMap.creationAreaBox });
assembleHTMLElement($root, {}, [$box]);
return $box as HTMLElement;
}
export function clearCreationAreaBox($root: HTMLDivElement | null) {
destroyBoxs($root, { className: classNameMap.creationAreaBox });
}
export function resetCreationAreaBox(
$root: HTMLDivElement | null,
opts: {
start: Point;
end: Point;
}
) {
if (!$root) {
return;
}
const { start, end } = opts;
if (start && end) {
const $box = getCreationAreaBox($root);
// start = calcViewPoint(start, { viewScaleInfo });
// end = calcViewPoint(end, { viewScaleInfo });
setHTMLCSSProps($box, {
left: Math.min(start.x, end.x),
top: Math.min(start.y, end.y),
width: Math.abs(end.x - start.x),
height: Math.abs(end.y - start.y),
});
} else {
clearCreationAreaBox($root);
}
}

View file

@ -0,0 +1,178 @@
import type {
Middleware,
MiddlewareCreatorConfig,
CoreEventMap,
MaterialType,
Material,
ModifyRecord,
} from '@idraw/types';
import { deepClone, addClassName, removeClassName, updateMaterialInList } from '@idraw/util';
import {
classNameMap,
defaultConfig,
defaultStyles,
getRootClassName,
keyStartPoint,
keyEndPoint,
keyActiveMaterialType,
} from './static';
import type { CreatorSharedStorage } from './types';
import { initStyles, destroyStyles, getMiddlewareCreatorStyles } from './styles';
import { initRoot, resetCreationAreaBox, clearCreationAreaBox } from './dom';
import { createMaterialByArea, updateMaterialByArea } from './util';
import { coreEventKeys } from '../../static';
import { triggerChangeEvent } from '../common';
export { getMiddlewareCreatorStyles };
export const MiddlewareCreator: Middleware<CreatorSharedStorage, CoreEventMap, MiddlewareCreatorConfig> = (
opts,
config
) => {
const { sharer, viewer, calculator, eventHub } = opts;
let innerConfig = {
...defaultStyles,
...defaultConfig,
...config,
};
const styles = getMiddlewareCreatorStyles(innerConfig);
const rootClassName = getRootClassName();
let $root: HTMLDivElement | null = null;
let activeMaterial: Material | null = null;
const clear = () => {
sharer.setSharedStorage(keyStartPoint, null); // null | Point;
sharer.setSharedStorage(keyEndPoint, null); // null | Point;
activeMaterial = null;
};
clear();
let creative: boolean = false;
const createCallback = ({ type }: { type: Exclude<MaterialType, 'path' | 'foreignObject' | 'svgCode'> }) => {
creative = true;
if ($root) {
eventHub.trigger(coreEventKeys.CURSOR, {
type: 'plus',
});
sharer.setSharedStorage(keyActiveMaterialType, type);
addClassName($root, [classNameMap.creative]);
eventHub.trigger(coreEventKeys.CLEAR_SELECT);
}
};
const clearCreateCallback = () => {
eventHub.trigger(coreEventKeys.CURSOR, {
type: 'auto',
});
creative = false;
if ($root) {
removeClassName($root, [classNameMap.creative]);
}
};
return {
name: '@middleware/creator',
use() {
initStyles(rootClassName, styles);
$root = initRoot({ rootClassName, $container: opts.container as HTMLElement });
eventHub.on(coreEventKeys.CREATE, createCallback);
eventHub.on(coreEventKeys.CLEAR_CREATE, clearCreateCallback);
},
disuse() {
destroyStyles(rootClassName);
// clear dom
$root?.remove();
$root = null;
eventHub.trigger(coreEventKeys.CURSOR, {
type: 'auto',
});
eventHub.off(coreEventKeys.CREATE, createCallback);
eventHub.off(coreEventKeys.CLEAR_CREATE, clearCreateCallback);
},
resetConfig(config) {
innerConfig = { ...innerConfig, ...config };
},
pointStart: (e) => {
clear();
if (!creative) {
return;
}
sharer.setSharedStorage(keyStartPoint, e.point);
},
pointMove: (e) => {
if (!creative) {
return;
}
sharer.setSharedStorage(keyEndPoint, e.point);
const activeMaterialType = sharer.getSharedStorage(keyActiveMaterialType);
const start = sharer.getSharedStorage(keyStartPoint);
const end = sharer.getSharedStorage(keyEndPoint);
const viewScaleInfo = sharer.getActiveViewScaleInfo();
const viewSizeInfo = sharer.getActiveViewSizeInfo();
const data = sharer.getActiveStorage('data');
if (activeMaterial && start && end) {
activeMaterial = updateMaterialByArea(activeMaterial, { start, end, viewScaleInfo, calculator });
updateMaterialInList(activeMaterial.id, activeMaterial, data.materials);
calculator.modifyVirtualAttributes(activeMaterial, { viewScaleInfo, viewSizeInfo, groupQueue: [] });
} else if (activeMaterialType && start && end) {
activeMaterial = createMaterialByArea(activeMaterialType, { start, end, viewScaleInfo, calculator });
data.materials.push(activeMaterial);
calculator.resetVirtualItemMap(data, { viewScaleInfo, viewSizeInfo });
}
viewer.drawFrame();
},
pointEnd: () => {
if (!creative) {
return;
}
if (activeMaterial) {
const data = sharer.getActiveStorage('data');
const modifyRecord: ModifyRecord<'addMaterial'> = {
type: 'addMaterial',
time: Date.now(),
content: {
method: 'addMaterial',
id: activeMaterial.id,
position: [data.materials?.length],
material: deepClone(activeMaterial),
},
};
triggerChangeEvent(eventHub, { data, type: 'addMaterial', modifyRecord }, 'all');
}
if (innerConfig.selectAfterCreated === true && activeMaterial?.id) {
const id = activeMaterial.id;
eventHub.trigger(coreEventKeys.SELECT, { ids: [id], type: 'selectMaterial' });
}
innerConfig.afterCreated?.();
clearCreationAreaBox($root);
clearCreateCallback();
clear();
},
beforeDrawFrame() {
const start = sharer.getSharedStorage(keyStartPoint);
const end = sharer.getSharedStorage(keyEndPoint);
if (start && end) {
resetCreationAreaBox($root, {
start,
end,
});
} else {
clearCreationAreaBox($root);
}
},
};
};

View file

@ -0,0 +1,28 @@
import type { MiddlewareCreatorStyles, MiddlewareCreatorConfig } from '@idraw/types';
import { createId } from '@idraw/util';
export const key = 'CREATOR';
export const keyStartPoint = Symbol(`${key}_startPoint`);
export const keyEndPoint = Symbol(`${key}_endPoint`);
export const keyActiveMaterialType = Symbol(`${key}_activeMaterialType`);
export const prefix = `idraw-middleware-creator`;
export const getRootClassName = () => `${prefix}-${createId()}`;
export const creationAreaBorderWidth = 1.5;
export const defaultStyles: MiddlewareCreatorStyles = {
zIndex: 2,
creationAreaBorderColor: '#1973ba',
};
export const defaultConfig: Partial<MiddlewareCreatorConfig> = {
selectAfterCreated: true,
};
export const classNameMap = {
// selection area
creationAreaBox: `${prefix}-creationAreaBox`,
creative: `${prefix}-creative`,
};

View file

@ -0,0 +1,39 @@
import type { MiddlewareCreatorStyles, MiddlewareCreatorConfig, StylesProps } from '@idraw/types';
import { injectStyles, removeStyles, getMiddlewareValidStyles } from '@idraw/util';
import { classNameMap, creationAreaBorderWidth } from './static';
export function initStyles(rootClassName: string, styles: MiddlewareCreatorStyles) {
const cls = (str: string) => `.${str}`;
const stylesProps: StylesProps = {
display: 'none',
zIndex: styles.zIndex,
position: 'absolute',
background: 'transparent',
top: 0,
bottom: 0,
left: 0,
right: 0,
overflow: 'hidden',
[`&${cls(classNameMap.creative)}`]: {
display: 'block',
},
// selection area box
[cls(classNameMap.creationAreaBox)]: {
position: 'absolute',
outline: `${creationAreaBorderWidth}px solid ${styles.creationAreaBorderColor}`,
background: '#0000ff1f', // TODO
},
};
injectStyles({ styles: stylesProps, rootClassName, type: 'element' });
}
export function destroyStyles(rootClassName: string) {
removeStyles({ rootClassName, type: 'element' });
}
export function getMiddlewareCreatorStyles<C = MiddlewareCreatorConfig, S = MiddlewareCreatorStyles>(config: C): S {
const styles: S = getMiddlewareValidStyles<C, S>(config, ['zIndex', 'creationAreaBorderColor']);
return styles;
}

View file

@ -0,0 +1,8 @@
import type { Point, MaterialType } from '@idraw/types';
import { keyStartPoint, keyEndPoint, keyActiveMaterialType } from './static';
export type CreatorSharedStorage = {
[keyStartPoint]: Point | null;
[keyEndPoint]: Point | null;
[keyActiveMaterialType]: Exclude<MaterialType, 'path' | 'foreignObject' | 'svgCode'> | null;
};

View file

@ -0,0 +1,61 @@
import type { MaterialType, Material, MaterialSize, Point, ViewScaleInfo, ViewCalculator } from '@idraw/types';
import { createId, calcPointFromView, getDefaultMaterialAttributes } from '@idraw/util';
type Options = { start: Point; end: Point; viewScaleInfo: ViewScaleInfo; calculator: ViewCalculator };
function getMaterialSizeByArea(opts: Options) {
const { start, end, viewScaleInfo, calculator } = opts;
const startPoint = calcPointFromView(start, { viewScaleInfo });
const endPoint = calcPointFromView(end, { viewScaleInfo });
const size: MaterialSize = {
x: calculator.toGridNum(Math.min(startPoint.x, endPoint.x)),
y: calculator.toGridNum(Math.min(startPoint.y, endPoint.y)),
width: calculator.toGridNum(Math.abs(endPoint.x - startPoint.x)),
height: calculator.toGridNum(Math.abs(endPoint.y - startPoint.y)),
};
return size;
}
export function createMaterialByArea(type: Exclude<MaterialType, 'path' | 'foreignObject' | 'svgCode'>, opts: Options) {
const { fill, text, href } = getDefaultMaterialAttributes();
const defaultMtrlAttrs: Partial<Material> = { fill };
if (type === 'circle') {
defaultMtrlAttrs.r = 1;
} else if (type === 'ellipse') {
defaultMtrlAttrs.rx = 1;
defaultMtrlAttrs.ry = 1;
} else if (type === 'text') {
defaultMtrlAttrs.text = text;
defaultMtrlAttrs.fontSize = 1;
} else if (type === 'image') {
defaultMtrlAttrs.href = href;
} else if (type === 'group') {
defaultMtrlAttrs.children = [];
}
const mtrl: Material = {
id: createId(),
type,
...defaultMtrlAttrs,
...getMaterialSizeByArea(opts),
};
return mtrl;
}
export function updateMaterialByArea(mtrl: Material, opts: Options) {
const size = getMaterialSizeByArea(opts);
const { type } = mtrl;
const updatedMtrl: Material = {
...mtrl,
...size,
};
if (type === 'circle') {
updatedMtrl.r = Math.min(size.width, size.height) / 2;
} else if (type === 'ellipse') {
updatedMtrl.rx = size.width / 2;
updatedMtrl.ry = size.height / 2;
} else if (type === 'text') {
updatedMtrl.fontSize = Math.min(size.width, size.height);
}
return updatedMtrl;
}

View file

@ -1,5 +1,5 @@
import type { Middleware, CoreEventMap, Point } from '@idraw/types';
import { coreEventKeys } from '../../config';
import { coreEventKeys } from '../../static';
const key = 'DRAG';
const keyPrevPoint = Symbol(`${key}_prevPoint`);
@ -19,7 +19,7 @@ export const MiddlewareDragger: Middleware<DraggerSharedStorage, CoreEventMap> =
return;
}
eventHub.trigger(coreEventKeys.CURSOR, {
type: 'drag-default'
type: 'drag-default',
});
},
@ -28,7 +28,7 @@ export const MiddlewareDragger: Middleware<DraggerSharedStorage, CoreEventMap> =
sharer.setSharedStorage(keyPrevPoint, point);
isDragging = true;
eventHub.trigger(coreEventKeys.CURSOR, {
type: 'drag-active'
type: 'drag-active',
});
},
@ -48,8 +48,8 @@ export const MiddlewareDragger: Middleware<DraggerSharedStorage, CoreEventMap> =
isDragging = false;
sharer.setSharedStorage(keyPrevPoint, null);
eventHub.trigger(coreEventKeys.CURSOR, {
type: 'drag-default'
type: 'drag-default',
});
}
},
};
};

View file

@ -1,14 +0,0 @@
import type { MiddlewareInfoStyle } from '@idraw/types';
const infoBackground = '#1973bac6';
const infoTextColor = '#ffffff';
export const infoFontSize = 10;
export const infoLineHeight = 16;
export const MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE = '@middleware/internal-event/show-info-angle';
export const defaltStyle: MiddlewareInfoStyle = {
textBackground: infoBackground,
textColor: infoTextColor
};

View file

@ -1,43 +1,43 @@
import type { PointSize, ViewContext2D } from '@idraw/types';
import type { Point, ViewContext2D } from '@idraw/types';
import { rotateByCenter } from '@idraw/util';
import type { MiddlewareInfoStyle } from '@idraw/types';
import type { MiddlewareInfoStyles } from '@idraw/types';
const fontFamily = 'monospace';
export function drawSizeInfoText(
ctx: ViewContext2D,
opts: {
point: PointSize;
rotateCenter: PointSize;
point: Point;
rotateCenter: Point;
angle: number;
text: string;
fontSize: number;
lineHeight: number;
style: MiddlewareInfoStyle;
styles: MiddlewareInfoStyles;
}
) {
const { point, rotateCenter, angle, text, style, fontSize, lineHeight } = opts;
const { textColor, textBackground } = style;
const { point, rotateCenter, angle, text, styles, fontSize, lineHeight } = opts;
const { textColor, textBackground } = styles;
rotateByCenter(ctx, angle, rotateCenter, () => {
ctx.$setFont({
fontWeight: '300',
fontSize,
fontFamily
fontFamily,
});
const padding = (lineHeight - fontSize) / 2;
const textWidth = ctx.$undoPixelRatio(ctx.measureText(text).width);
const bgStart = {
x: point.x - textWidth / 2 - padding,
y: point.y
y: point.y,
};
const bgEnd = {
x: bgStart.x + textWidth + padding * 2,
y: bgStart.y + fontSize + padding
y: bgStart.y + fontSize + padding,
};
const textStart = {
x: point.x - textWidth / 2,
y: point.y
y: point.y,
};
ctx.setLineDash([]);
ctx.fillStyle = textBackground;
@ -58,37 +58,37 @@ export function drawSizeInfoText(
export function drawPositionInfoText(
ctx: ViewContext2D,
opts: {
point: PointSize;
rotateCenter: PointSize;
point: Point;
rotateCenter: Point;
angle: number;
text: string;
fontSize: number;
lineHeight: number;
style: MiddlewareInfoStyle;
styles: MiddlewareInfoStyles;
}
) {
const { point, rotateCenter, angle, text, style, fontSize, lineHeight } = opts;
const { textBackground, textColor } = style;
const { point, rotateCenter, angle, text, styles, fontSize, lineHeight } = opts;
const { textBackground, textColor } = styles;
rotateByCenter(ctx, angle, rotateCenter, () => {
ctx.$setFont({
fontWeight: '300',
fontSize,
fontFamily
fontFamily,
});
const padding = (lineHeight - fontSize) / 2;
const textWidth = ctx.$undoPixelRatio(ctx.measureText(text).width);
const bgStart = {
x: point.x,
y: point.y
y: point.y,
};
const bgEnd = {
x: bgStart.x + textWidth + padding * 2,
y: bgStart.y + fontSize + padding
y: bgStart.y + fontSize + padding,
};
const textStart = {
x: point.x + padding,
y: point.y
y: point.y,
};
ctx.setLineDash([]);
ctx.fillStyle = textBackground;
@ -109,37 +109,37 @@ export function drawPositionInfoText(
export function drawAngleInfoText(
ctx: ViewContext2D,
opts: {
point: PointSize;
rotateCenter: PointSize;
point: Point;
rotateCenter: Point;
angle: number;
text: string;
fontSize: number;
lineHeight: number;
style: MiddlewareInfoStyle;
styles: MiddlewareInfoStyles;
}
) {
const { point, rotateCenter, angle, text, style, fontSize, lineHeight } = opts;
const { textBackground, textColor } = style;
const { point, rotateCenter, angle, text, styles, fontSize, lineHeight } = opts;
const { textBackground, textColor } = styles;
rotateByCenter(ctx, angle, rotateCenter, () => {
ctx.$setFont({
fontWeight: '300',
fontSize,
fontFamily
fontFamily,
});
const padding = (lineHeight - fontSize) / 2;
const textWidth = ctx.$undoPixelRatio(ctx.measureText(text).width);
const bgStart = {
x: point.x,
y: point.y
y: point.y,
};
const bgEnd = {
x: bgStart.x + textWidth + padding * 2,
y: bgStart.y + fontSize + padding
y: bgStart.y + fontSize + padding,
};
const textStart = {
x: point.x + padding,
y: point.y
y: point.y,
};
ctx.setLineDash([]);
ctx.fillStyle = textBackground;

View file

@ -1,4 +1,4 @@
import type { Middleware, ViewRectInfo, Element, MiddlewareInfoConfig, CoreEventMap } from '@idraw/types';
import type { Middleware, BoundingInfo, Material, MiddlewareInfoConfig, CoreEventMap } from '@idraw/types';
import {
formatNumber,
getViewScaleInfoFromSnapshot,
@ -6,18 +6,20 @@ import {
createUUID,
limitAngle,
rotatePoint,
parseAngleToRadian
parseAngleToRadian,
} from '@idraw/util';
import { keySelectedElementList, keyActionType, keyGroupQueue } from '../selector';
import { keySelectedMaterialList, keyActionType, keyGroupQueue } from '../selector';
import { drawSizeInfoText, drawPositionInfoText, drawAngleInfoText } from './draw-info';
import type { DeepInfoSharedStorage } from './types';
import { defaltStyle, MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE } from './config';
import { defaltStyle, MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE, getMiddlewareInfoStyles } from './static';
export { MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE };
const infoFontSize = 10;
const infoLineHeight = 16;
export { getMiddlewareInfoStyles };
export const MiddlewareInfo: Middleware<
DeepInfoSharedStorage,
CoreEventMap & {
@ -29,8 +31,9 @@ export const MiddlewareInfo: Middleware<
const { overlayContext } = boardContent;
let innerConfig = {
...defaltStyle,
...config
...config,
};
const styles = getMiddlewareInfoStyles(innerConfig);
let showAngleInfo = true;
@ -54,74 +57,69 @@ export const MiddlewareInfo: Middleware<
},
beforeDrawFrame({ snapshot }) {
const { textBackground, textColor } = innerConfig;
const style = {
textBackground,
textColor
};
const { sharedStore } = snapshot;
const selectedElementList = sharedStore[keySelectedElementList];
const selectedMaterialList = sharedStore[keySelectedMaterialList];
const actionType = sharedStore[keyActionType];
const groupQueue = sharedStore[keyGroupQueue] || [];
if (selectedElementList.length === 1) {
const elem = selectedElementList[0];
if (elem && ['select', 'drag', 'resize'].includes(actionType as string)) {
if (selectedMaterialList?.length === 1) {
const mtrl = selectedMaterialList[0];
if (mtrl && ['select', 'drag', 'resize'].includes(actionType as string)) {
const viewScaleInfo = getViewScaleInfoFromSnapshot(snapshot);
const viewSizeInfo = getViewSizeInfoFromSnapshot(snapshot);
const { x, y, w, h, angle } = elem;
const { x, y, width, height, angle } = mtrl;
const totalGroupQueue = [
...groupQueue,
...[
{
uuid: createUUID(),
id: createUUID(),
x,
y,
w,
h,
width,
height,
angle,
type: 'group',
detail: { children: [] }
} as Element<'group'>
]
children: [],
} as Material,
],
];
const calcOpts = { viewScaleInfo, viewSizeInfo };
const rangeRectInfo = calculator.calcViewRectInfoFromOrigin(elem.uuid, calcOpts);
const rangeBoundingInfo = calculator.calcViewBoundingInfoFromOrigin(mtrl.id, calcOpts);
let totalAngle = 0;
totalGroupQueue.forEach((group) => {
totalAngle += group.angle || 0;
});
const totalRadian = parseAngleToRadian(limitAngle(0 - totalAngle));
if (rangeRectInfo) {
const elemCenter = rangeRectInfo?.center;
const rectInfo: ViewRectInfo = {
topLeft: rotatePoint(elemCenter, rangeRectInfo.topLeft, totalRadian),
topRight: rotatePoint(elemCenter, rangeRectInfo.topRight, totalRadian),
bottomRight: rotatePoint(elemCenter, rangeRectInfo.bottomRight, totalRadian),
bottomLeft: rotatePoint(elemCenter, rangeRectInfo.bottomLeft, totalRadian),
center: rotatePoint(elemCenter, rangeRectInfo.center, totalRadian),
top: rotatePoint(elemCenter, rangeRectInfo.top, totalRadian),
right: rotatePoint(elemCenter, rangeRectInfo.right, totalRadian),
bottom: rotatePoint(elemCenter, rangeRectInfo.bottom, totalRadian),
left: rotatePoint(elemCenter, rangeRectInfo.left, totalRadian)
if (rangeBoundingInfo) {
const mtrlCenter = rangeBoundingInfo?.center;
const boundingBox: BoundingInfo = {
topLeft: rotatePoint(mtrlCenter, rangeBoundingInfo.topLeft, totalRadian),
topRight: rotatePoint(mtrlCenter, rangeBoundingInfo.topRight, totalRadian),
bottomRight: rotatePoint(mtrlCenter, rangeBoundingInfo.bottomRight, totalRadian),
bottomLeft: rotatePoint(mtrlCenter, rangeBoundingInfo.bottomLeft, totalRadian),
center: rotatePoint(mtrlCenter, rangeBoundingInfo.center, totalRadian),
top: rotatePoint(mtrlCenter, rangeBoundingInfo.top, totalRadian),
right: rotatePoint(mtrlCenter, rangeBoundingInfo.right, totalRadian),
bottom: rotatePoint(mtrlCenter, rangeBoundingInfo.bottom, totalRadian),
left: rotatePoint(mtrlCenter, rangeBoundingInfo.left, totalRadian),
};
const x = formatNumber(elem.x, { decimalPlaces: 2 });
const y = formatNumber(elem.y, { decimalPlaces: 2 });
const w = formatNumber(elem.w, { decimalPlaces: 2 });
const h = formatNumber(elem.h, { decimalPlaces: 2 });
const x = formatNumber(mtrl.x, { decimalPlaces: 2 });
const y = formatNumber(mtrl.y, { decimalPlaces: 2 });
const w = formatNumber(mtrl.width, { decimalPlaces: 2 });
const h = formatNumber(mtrl.height, { decimalPlaces: 2 });
// // test start ----
// const ctx = overlayContext;
// ctx.beginPath();
// ctx.moveTo(rectInfo.topLeft.x, rectInfo.topLeft.y);
// ctx.lineTo(rectInfo.topRight.x, rectInfo.topRight.y);
// ctx.lineTo(rectInfo.bottomRight.x, rectInfo.bottomRight.y);
// ctx.lineTo(rectInfo.bottomLeft.x, rectInfo.bottomLeft.y);
// ctx.moveTo(boundingBox.topLeft.x, boundingBox.topLeft.y);
// ctx.lineTo(boundingBox.topRight.x, boundingBox.topRight.y);
// ctx.lineTo(boundingBox.bottomRight.x, boundingBox.bottomRight.y);
// ctx.lineTo(boundingBox.bottomLeft.x, boundingBox.bottomLeft.y);
// ctx.closePath();
// ctx.strokeStyle = 'red';
// ctx.stroke();
@ -129,53 +127,53 @@ export const MiddlewareInfo: Middleware<
const xyText = `${formatNumber(x, { decimalPlaces: 0 })},${formatNumber(y, { decimalPlaces: 0 })}`;
const whText = `${formatNumber(w, { decimalPlaces: 0 })}x${formatNumber(h, { decimalPlaces: 0 })}`;
const angleText = `${formatNumber(elem.angle || 0, { decimalPlaces: 0 })}°`;
const angleText = `${formatNumber(limitAngle(mtrl.angle || 0), { decimalPlaces: 0 })}°`;
drawSizeInfoText(overlayContext, {
point: {
x: rectInfo.bottom.x,
y: rectInfo.bottom.y + infoFontSize
x: boundingBox.bottom.x,
y: boundingBox.bottom.y + infoFontSize,
},
rotateCenter: rectInfo.center,
rotateCenter: boundingBox.center,
angle: totalAngle,
text: whText,
fontSize: infoFontSize,
lineHeight: infoLineHeight,
style
styles,
});
drawPositionInfoText(overlayContext, {
point: {
x: rectInfo.topLeft.x,
y: rectInfo.topLeft.y - infoFontSize * 2
x: boundingBox.topLeft.x,
y: boundingBox.topLeft.y - infoFontSize * 2,
},
rotateCenter: rectInfo.center,
rotateCenter: boundingBox.center,
angle: totalAngle,
text: xyText,
fontSize: infoFontSize,
lineHeight: infoLineHeight,
style
styles,
});
if (showAngleInfo) {
if (elem.operations?.rotatable !== false) {
if (mtrl.operations?.rotatable !== false) {
drawAngleInfoText(overlayContext, {
point: {
x: rectInfo.top.x + infoFontSize + 4,
y: rectInfo.top.y - infoFontSize * 2 - 18
x: boundingBox.top.x + infoFontSize + 4,
y: boundingBox.top.y - infoFontSize * 2 - 18,
},
rotateCenter: rectInfo.center,
rotateCenter: boundingBox.center,
angle: totalAngle,
text: angleText,
fontSize: infoFontSize,
lineHeight: infoLineHeight,
style
styles,
});
}
}
}
}
}
}
},
};
};

View file

@ -0,0 +1,20 @@
import type { MiddlewareInfoStyles, MiddlewareInfoConfig } from '@idraw/types';
import { getMiddlewareValidStyles } from '@idraw/util';
const infoBackground = '#1973bac6';
const infoTextColor = '#ffffff';
export const infoFontSize = 10;
export const infoLineHeight = 16;
export const MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE = '@middleware/internal-event/show-info-angle';
export const defaltStyle: MiddlewareInfoStyles = {
textBackground: infoBackground,
textColor: infoTextColor,
};
export function getMiddlewareInfoStyles<C = MiddlewareInfoConfig, S = MiddlewareInfoStyles>(config: C): S {
const styles: S = getMiddlewareValidStyles<C, S>(config, ['textBackground', 'textColor']);
return styles;
}

View file

@ -1,7 +1,7 @@
import { keySelectedElementList, keyHoverElement, keyActionType, keyGroupQueue } from '../selector';
import { keySelectedMaterialList, keyHoverMaterial, keyActionType, keyGroupQueue } from '../selector';
import type { DeepSelectorSharedStorage } from '../selector';
export type DeepInfoSharedStorage = Pick<
DeepSelectorSharedStorage,
typeof keySelectedElementList | typeof keyHoverElement | typeof keyActionType | typeof keyGroupQueue
typeof keySelectedMaterialList | typeof keyHoverMaterial | typeof keyActionType | typeof keyGroupQueue
>;

View file

@ -1,20 +0,0 @@
import type { MiddlewareLayoutSelectorStyle } from '@idraw/types';
export const key = 'LAYOUT_SELECT';
// export const keyHoverElement = Symbol(`${key}_hoverElementSize`);
export const keyLayoutActionType = Symbol(`${key}_layoutActionType`); // 'resize' | null = null;
export const keyLayoutControlType = Symbol(`${key}_layoutControlType`); // ControlType | null;
export const keyLayoutController = Symbol(`${key}_layoutController`); // ElementSizeController | null = null;
export const keyLayoutIsHoverContent = Symbol(`${key}_layoutIsHoverContent`); // boolean | null
export const keyLayoutIsHoverController = Symbol(`${key}_layoutIsHoverController`); // boolean | null
export const keyLayoutIsSelected = Symbol(`${key}_layoutIsSelected`); // boolean | null
export const keyLayoutIsBusyMoving = Symbol(`${key}_layoutIsSelected`); // boolean | null
// const selectColor = '#b331c9';
// const disabledColor = '#5b5959b5';
export const controllerSize = 10;
export const defaultStyle: MiddlewareLayoutSelectorStyle = {
activeColor: '#b331c9'
};

View file

@ -0,0 +1,168 @@
import type { ViewScaleInfo, DataLayout, HTMLCSSProps } from '@idraw/types';
import {
ATTR_VALID_WATCH,
createHTMLElement,
assembleHTMLElement,
calcViewMaterialSize,
setHTMLCSSProps,
addClassName,
removeClassName,
} from '@idraw/util';
import { classNameMap, ATTR_HANDLER_TYPE } from './static';
type Options = { viewScaleInfo: ViewScaleInfo; layout?: DataLayout; rootClassName: string; hover: boolean };
export function clearMaterialLayoutBoxs($container: HTMLDivElement, opts: Pick<Options, 'rootClassName'>) {
const { rootClassName } = opts;
const $boxs = $container.getElementsByClassName(rootClassName);
Array.from($boxs).forEach(($box) => {
$box.remove();
});
}
function renderLayoutBoxHandlers($container: HTMLElement, opts: Options) {
const $existHandlers = $container.querySelectorAll(`[${ATTR_HANDLER_TYPE}]`);
const { rootClassName, layout, viewScaleInfo, hover } = opts;
if (!layout) {
return;
}
const layoutSize = calcViewMaterialSize(layout, { viewScaleInfo });
const { x, y, height, width } = layoutSize;
const edgeLeftStyle: HTMLCSSProps = {
left: x,
top: y,
height,
};
const edgeTopStyle: HTMLCSSProps = {
left: x,
top: y,
width,
};
const edgeRightStyle: HTMLCSSProps = {
left: x + width,
top: y,
height,
};
const edgeBottomStyle: HTMLCSSProps = {
left: x,
top: y + height,
width,
};
const cornerTopLeftStyle: HTMLCSSProps = {
left: x,
top: y,
};
const cornerTopRightStyle: HTMLCSSProps = {
left: x + width,
top: y,
};
const cornerBottomLeftStyle: HTMLCSSProps = {
left: x,
top: y + height,
};
const cornerBottomRightStyle: HTMLCSSProps = {
left: x + width,
top: y + height,
};
if ($existHandlers.length > 0) {
const $edgeLeft = $container.getElementsByClassName(classNameMap.edgeLeftHandler)[0] as HTMLElement;
const $edgeRight = $container.getElementsByClassName(classNameMap.edgeRightHandler)[0] as HTMLElement;
const $edgeTop = $container.getElementsByClassName(classNameMap.edgeTopHandler)[0] as HTMLElement;
const $edgeBottom = $container.getElementsByClassName(classNameMap.edgeBottomHandler)[0] as HTMLElement;
const $cornerTopLeft = $container.getElementsByClassName(classNameMap.cornerTopLeftHandler)[0] as HTMLElement;
const $cornerTopRight = $container.getElementsByClassName(classNameMap.cornerTopRightHandler)[0] as HTMLElement;
const $cornerBottomLeft = $container.getElementsByClassName(classNameMap.cornerBottomLeftHandler)[0] as HTMLElement;
const $cornerBottomRight = $container.getElementsByClassName(
classNameMap.cornerBottomRightHandler
)[0] as HTMLElement;
setHTMLCSSProps($edgeLeft, edgeLeftStyle);
setHTMLCSSProps($edgeRight, edgeRightStyle);
setHTMLCSSProps($edgeTop, edgeTopStyle);
setHTMLCSSProps($edgeBottom, edgeBottomStyle);
setHTMLCSSProps($cornerTopLeft, cornerTopLeftStyle);
setHTMLCSSProps($cornerTopRight, cornerTopRightStyle);
setHTMLCSSProps($cornerBottomLeft, cornerBottomLeftStyle);
setHTMLCSSProps($cornerBottomRight, cornerBottomRightStyle);
} else {
const create = createHTMLElement;
const baseAttrs = {
[ATTR_VALID_WATCH]: 'true',
};
assembleHTMLElement($container, {}, [
create('div', {
[ATTR_HANDLER_TYPE]: 'left',
...baseAttrs,
className: `${rootClassName} ${classNameMap.edgeHandler} ${classNameMap.edgeLeftHandler}`,
style: edgeLeftStyle,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'top',
...baseAttrs,
className: `${rootClassName} ${classNameMap.edgeHandler} ${classNameMap.edgeTopHandler}`,
style: edgeTopStyle,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'right',
...baseAttrs,
className: `${rootClassName} ${classNameMap.edgeHandler} ${classNameMap.edgeRightHandler}`,
style: edgeRightStyle,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'bottom',
...baseAttrs,
className: `${rootClassName} ${classNameMap.edgeHandler} ${classNameMap.edgeBottomHandler}`,
style: edgeBottomStyle,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'top-left',
...baseAttrs,
className: `${rootClassName} ${classNameMap.cornerHandler} ${classNameMap.cornerTopLeftHandler}`,
style: cornerTopLeftStyle,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'top-right',
...baseAttrs,
className: `${rootClassName} ${classNameMap.cornerHandler} ${classNameMap.cornerTopRightHandler}`,
style: cornerTopRightStyle,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'bottom-left',
...baseAttrs,
className: `${rootClassName} ${classNameMap.cornerHandler} ${classNameMap.cornerBottomLeftHandler}`,
style: cornerBottomLeftStyle,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'bottom-right',
...baseAttrs,
className: `${rootClassName} ${classNameMap.cornerHandler} ${classNameMap.cornerBottomRightHandler}`,
style: cornerBottomRightStyle,
}),
]);
}
const $handlers = Array.from($container.querySelectorAll(`[${ATTR_HANDLER_TYPE}]`)) as HTMLElement[];
if (hover) {
$handlers.forEach(($item) => {
addClassName($item, [classNameMap.hover]);
});
} else {
$handlers.forEach(($item) => {
removeClassName($item, [classNameMap.hover]);
});
}
}
export function resetMaterialSelectedBox($contaier: HTMLDivElement, opts: Options) {
const { layout } = opts;
if (layout) {
renderLayoutBoxHandlers($contaier, opts);
} else {
clearMaterialLayoutBoxs($contaier, opts);
}
}

View file

@ -1,40 +1,45 @@
import type {
Middleware,
ElementSize,
MaterialSize,
Point,
MiddlewareLayoutSelectorConfig,
CoreEventMap,
RecursivePartial,
ModifyRecord,
DataLayout
DataLayout,
} from '@idraw/types';
import {
calcLayoutSizeController,
isViewPointInVertexes,
getViewScaleInfoFromSnapshot,
isViewPointInElementSize,
calcViewElementSize,
getElementSize,
toFlattenLayout
// calcLayoutSizeController,
// isViewPointInVertexes,
// getViewScaleInfoFromSnapshot,
isViewPointInMaterialSize,
calcViewMaterialSize,
getMaterialSize,
toFlattenLayout,
} from '@idraw/util';
import type { LayoutSelectorSharedStorage, ControlType } from './types';
import {
keyLayoutActionType,
keyLayoutController,
// keyLayoutController,
keyLayoutControlType,
keyLayoutIsHoverContent,
keyLayoutIsHoverController,
keyLayoutIsSelected,
keyLayoutIsBusyMoving,
controllerSize,
defaultStyle
} from './config';
defaultStyle,
getRootClassName,
ATTR_HANDLER_TYPE,
} from './static';
import { getMiddlewareLayoutSelectorStyles, initStyles, destroyStyles } from './styles';
import {
keyActionType as keyElementActionType
// keyHoverElement
keyActionType as keyMaterialActionType,
// keyHoverMaterial
} from '../selector';
import { drawLayoutController, drawLayoutHover } from './util';
import { coreEventKeys } from '../../config';
// import { drawLayoutController, drawLayoutHover } from './util';
import { resetMaterialSelectedBox, clearMaterialLayoutBoxs } from './dom';
import { coreEventKeys } from '../../static';
import { triggerChangeEvent } from '../common';
export { keyLayoutIsSelected, keyLayoutIsBusyMoving };
@ -43,24 +48,27 @@ export const MiddlewareLayoutSelector: Middleware<
CoreEventMap,
MiddlewareLayoutSelectorConfig
> = (opts, config) => {
const { sharer, boardContent, calculator, viewer, eventHub } = opts;
const { overlayContext } = boardContent;
const { sharer, calculator, viewer, eventHub } = opts;
// const { overlayContext } = boardContent;
let innerConfig = {
...defaultStyle,
...config
...config,
};
const styles = getMiddlewareLayoutSelectorStyles(innerConfig);
const rootClassName = getRootClassName();
let prevPoint: Point | null = null;
let prevIsHoverContent: boolean | null = null;
let prevIsSelected: boolean | null = null;
let pointStartLayoutSize: RecursivePartial<ElementSize> | null = null;
let pointStartLayoutSize: RecursivePartial<MaterialSize> | null = null;
const clear = () => {
prevPoint = null;
sharer.setSharedStorage(keyLayoutActionType, null);
sharer.setSharedStorage(keyLayoutControlType, null);
sharer.setSharedStorage(keyLayoutController, null);
// sharer.setSharedStorage(keyLayoutController, null);
sharer.setSharedStorage(keyLayoutIsHoverContent, null);
sharer.setSharedStorage(keyLayoutIsHoverController, null);
sharer.setSharedStorage(keyLayoutIsSelected, null);
@ -69,18 +77,9 @@ export const MiddlewareLayoutSelector: Middleware<
prevIsSelected = null;
};
// const isInElementHover = () => {
// const hoverElement = sharer.getSharedStorage(keyHoverElement);
// if (hoverElement) {
// clear();
// return true;
// }
// return false;
// };
const isInElementAction = () => {
const elementActionType = sharer.getSharedStorage(keyElementActionType);
if (elementActionType && elementActionType !== 'area') {
const isInMaterialAction = () => {
const materialActionType = sharer.getSharedStorage(keyMaterialActionType);
if (materialActionType && materialActionType !== 'area') {
clear();
return true;
}
@ -90,8 +89,8 @@ export const MiddlewareLayoutSelector: Middleware<
const getLayoutSize = () => {
const data = sharer.getActiveStorage('data');
if (data?.layout) {
const { x, y, w, h } = data.layout;
return { x, y, w, h };
const { x, y, width, height } = data.layout;
return { x, y, width, height };
}
return null;
};
@ -99,55 +98,35 @@ export const MiddlewareLayoutSelector: Middleware<
const isInLayout = (p: Point) => {
const size = getLayoutSize();
if (size) {
const { x, y, w, h } = size;
const { x, y, width, height } = size;
const viewScaleInfo = sharer.getActiveViewScaleInfo();
const viewSize = calcViewElementSize(
const viewSize = calcViewMaterialSize(
{
x: x - controllerSize / 2,
y: y - controllerSize / 2,
w: w + controllerSize,
h: h + controllerSize
width: width + controllerSize,
height: height + controllerSize,
},
{ viewScaleInfo }
);
return isViewPointInElementSize(p, viewSize);
return isViewPointInMaterialSize(p, viewSize);
}
return false;
};
const resetController = () => {
const viewScaleInfo = sharer.getActiveViewScaleInfo();
const size: ElementSize | null = getLayoutSize();
if (size) {
const controller = calcLayoutSizeController(size, { viewScaleInfo, controllerSize: 10 });
sharer.setSharedStorage(keyLayoutController, controller);
} else {
sharer.setSharedStorage(keyLayoutController, null);
}
};
const resetControlType = (e?: { point: Point }) => {
const resetControlType = (e: { point: Point; nativeEvent: Event }) => {
const data = sharer.getActiveStorage('data');
const controller = sharer.getSharedStorage(keyLayoutController);
const $target = e.nativeEvent.target as HTMLElement;
let controllerType: ControlType | null = null;
if (controller && data?.layout && e?.point) {
// sharer.setSharedStorage(keyLayoutControlType, null);
let layoutControlType: ControlType | null = null;
if (controller) {
const { topLeft, top, topRight, right, bottomRight, bottom, bottomLeft, left } = controller;
const list = [topLeft, top, topRight, right, bottomRight, bottom, bottomLeft, left];
for (let i = 0; i < list.length; i++) {
const item = list[i];
if (isViewPointInVertexes(e.point, item.vertexes)) {
layoutControlType = `${item.type}` as ControlType;
break;
}
}
if (layoutControlType) {
sharer.setSharedStorage(keyLayoutControlType, layoutControlType);
eventHub.trigger(coreEventKeys.CLEAR_SELECT);
controllerType = layoutControlType;
}
if ($target?.hasAttribute(ATTR_HANDLER_TYPE) && data?.layout && e?.point) {
sharer.setSharedStorage(keyLayoutControlType, null);
const layoutControlType: ControlType | null = $target.getAttribute(ATTR_HANDLER_TYPE) as ControlType | null;
if (layoutControlType) {
sharer.setSharedStorage(keyLayoutControlType, layoutControlType);
eventHub.trigger(coreEventKeys.CLEAR_SELECT);
controllerType = layoutControlType;
}
}
@ -167,7 +146,7 @@ export const MiddlewareLayoutSelector: Middleware<
eventHub.trigger(coreEventKeys.CURSOR, {
type: controlType ? `resize-${controlType}` : controlType,
groupQueue: [],
element: getLayoutSize()
material: getLayoutSize(),
});
};
@ -176,7 +155,12 @@ export const MiddlewareLayoutSelector: Middleware<
use: () => {
clear();
resetController();
initStyles(rootClassName, styles);
},
disuse: () => {
clear();
destroyStyles(rootClassName);
},
resetConfig(config) {
@ -187,10 +171,7 @@ export const MiddlewareLayoutSelector: Middleware<
if (sharer.getSharedStorage(keyLayoutIsBusyMoving) === true) {
return;
}
if (isInElementAction()) {
return;
}
// if (isInElementHover()) {
// if (isInMaterialAction()) {
// return;
// }
@ -204,65 +185,72 @@ export const MiddlewareLayoutSelector: Middleware<
}
}
if (sharer.getSharedStorage(keyLayoutIsSelected) === true) {
const prevLayoutActionType = sharer.getSharedStorage(keyLayoutActionType);
const data = sharer.getActiveStorage('data');
// if (sharer.getSharedStorage(keyLayoutIsSelected) === true) {
const prevLayoutActionType = sharer.getSharedStorage(keyLayoutActionType);
const data = sharer.getActiveStorage('data');
if (data?.layout) {
if (prevLayoutActionType !== 'resize') {
resetController();
const layoutControlType = resetControlType(e);
if (data?.layout) {
if (prevLayoutActionType !== 'resize') {
const layoutControlType = resetControlType(e);
if (layoutControlType) {
updateCursor(layoutControlType);
} else {
updateCursor();
sharer.setSharedStorage(keyLayoutActionType, null);
}
} else {
const layoutControlType = resetControlType(e);
if (layoutControlType) {
updateCursor(layoutControlType);
} else {
updateCursor();
sharer.setSharedStorage(keyLayoutActionType, null);
}
} else {
const layoutControlType = resetControlType(e);
updateCursor(layoutControlType);
}
if (sharer.getSharedStorage(keyLayoutIsHoverController) === true) {
return false;
}
return;
}
// if (sharer.getSharedStorage(keyLayoutIsHoverController) === true) {
// return false;
// }
// return;
// }
if (sharer.getSharedStorage(keyLayoutIsHoverContent) && !prevIsHoverContent) {
viewer.drawFrame();
}
prevIsHoverContent = sharer.getSharedStorage(keyLayoutIsHoverContent);
// if (sharer.getSharedStorage(keyLayoutIsHoverContent) && !prevIsHoverContent) {
// viewer.drawFrame();
// }
// prevIsHoverContent = sharer.getSharedStorage(keyLayoutIsHoverContent);
if (sharer.getSharedStorage(keyLayoutIsHoverController) === true) {
return false;
}
// if (sharer.getSharedStorage(keyLayoutIsHoverController) === true) {
// return false;
// }
},
pointStart: (e) => {
if (isInElementAction()) {
return;
}
// const inMaterial = isInMaterialAction();
// if (inMaterial) {
// if (opts.container) {
// clearMaterialLayoutBoxs(opts.container, { rootClassName });
// }
// return;
// }
if (isInLayout(e.point)) {
sharer.setSharedStorage(keyLayoutIsSelected, true);
} else {
if (prevIsSelected === true) {
if (opts.container) {
clearMaterialLayoutBoxs(opts.container, { rootClassName });
}
clear();
viewer.drawFrame();
}
sharer.setSharedStorage(keyLayoutIsSelected, false);
}
const data = sharer.getActiveStorage('data');
if (data?.layout) {
pointStartLayoutSize = getElementSize(data.layout as any);
pointStartLayoutSize = getMaterialSize(data.layout as any);
} else {
pointStartLayoutSize = null;
}
resetController();
const layoutControlType = resetControlType(e);
prevPoint = e.point;
if (layoutControlType) {
@ -282,7 +270,7 @@ export const MiddlewareLayoutSelector: Middleware<
pointMove: (e) => {
if (!sharer.getSharedStorage(keyLayoutIsSelected)) {
if (isInElementAction()) {
if (isInMaterialAction()) {
return;
}
}
@ -299,70 +287,69 @@ export const MiddlewareLayoutSelector: Middleware<
const viewMoveY = e.point.y - prevPoint.y;
const moveX = viewMoveX / scale;
const moveY = viewMoveY / scale;
const { x, y, w, h, operations = {} } = data.layout;
const { x, y, width, height, operations = {} } = data.layout;
const { position = 'absolute' } = operations;
if (layoutControlType === 'top') {
if (position === 'relative') {
data.layout.h = calculator.toGridNum(h - moveY);
data.layout.height = calculator.toGridNum(height - moveY);
viewer.scroll({ moveY: viewMoveY });
} else {
data.layout.y = calculator.toGridNum(y + moveY);
data.layout.h = calculator.toGridNum(h - moveY);
data.layout.height = calculator.toGridNum(height - moveY);
}
} else if (layoutControlType === 'right') {
data.layout.w = calculator.toGridNum(w + moveX);
data.layout.width = calculator.toGridNum(width + moveX);
} else if (layoutControlType === 'bottom') {
data.layout.h = calculator.toGridNum(h + moveY);
data.layout.height = calculator.toGridNum(height + moveY);
} else if (layoutControlType === 'left') {
if (position === 'relative') {
data.layout.w = calculator.toGridNum(w - moveX);
data.layout.width = calculator.toGridNum(width - moveX);
viewer.scroll({ moveX: viewMoveX });
} else {
data.layout.x = calculator.toGridNum(x + moveX);
data.layout.w = calculator.toGridNum(w - moveX);
data.layout.width = calculator.toGridNum(width - moveX);
}
} else if (layoutControlType === 'top-left') {
if (position === 'relative') {
data.layout.w = calculator.toGridNum(w - moveX);
data.layout.h = calculator.toGridNum(h - moveY);
data.layout.width = calculator.toGridNum(width - moveX);
data.layout.height = calculator.toGridNum(height - moveY);
viewer.scroll({ moveX: viewMoveX, moveY: viewMoveY });
} else {
data.layout.x = calculator.toGridNum(x + moveX);
data.layout.y = calculator.toGridNum(y + moveY);
data.layout.w = calculator.toGridNum(w - moveX);
data.layout.h = calculator.toGridNum(h - moveY);
data.layout.width = calculator.toGridNum(width - moveX);
data.layout.height = calculator.toGridNum(height - moveY);
}
} else if (layoutControlType === 'top-right') {
if (position === 'relative') {
viewer.scroll({
moveY: viewMoveY
moveY: viewMoveY,
});
data.layout.w = calculator.toGridNum(w + moveX);
data.layout.h = calculator.toGridNum(h - moveY);
data.layout.width = calculator.toGridNum(width + moveX);
data.layout.height = calculator.toGridNum(height - moveY);
} else {
data.layout.y = calculator.toGridNum(y + moveY);
data.layout.w = calculator.toGridNum(w + moveX);
data.layout.h = calculator.toGridNum(h - moveY);
data.layout.width = calculator.toGridNum(width + moveX);
data.layout.height = calculator.toGridNum(height - moveY);
}
} else if (layoutControlType === 'bottom-right') {
data.layout.w = calculator.toGridNum(w + moveX);
data.layout.h = calculator.toGridNum(h + moveY);
data.layout.width = calculator.toGridNum(width + moveX);
data.layout.height = calculator.toGridNum(height + moveY);
} else if (layoutControlType === 'bottom-left') {
if (position === 'relative') {
viewer.scroll({
moveX: viewMoveX
moveX: viewMoveX,
});
data.layout.w = calculator.toGridNum(w - moveX);
data.layout.h = calculator.toGridNum(h + moveY);
data.layout.width = calculator.toGridNum(width - moveX);
data.layout.height = calculator.toGridNum(height + moveY);
} else {
data.layout.x = calculator.toGridNum(x + moveX);
data.layout.w = calculator.toGridNum(w - moveX);
data.layout.h = calculator.toGridNum(h + moveY);
data.layout.width = calculator.toGridNum(width - moveX);
data.layout.height = calculator.toGridNum(height + moveY);
}
}
}
prevPoint = e.point;
resetController();
viewer.drawFrame();
return false;
}
@ -386,14 +373,14 @@ export const MiddlewareLayoutSelector: Middleware<
content: {
method: 'modifyLayout',
before: toFlattenLayout(pointStartLayoutSize as DataLayout),
after: toFlattenLayout(getElementSize(data.layout as any) as DataLayout)
}
after: toFlattenLayout(getMaterialSize(data.layout as any) as DataLayout),
},
};
}
eventHub.trigger(coreEventKeys.CHANGE, {
triggerChangeEvent(eventHub, {
type: 'resizeLayout',
data,
modifyRecord
modifyRecord,
});
}
pointStartLayoutSize = null;
@ -407,42 +394,45 @@ export const MiddlewareLayoutSelector: Middleware<
},
beforeDrawFrame: ({ snapshot }) => {
if (isInElementAction()) {
if (isInMaterialAction()) {
return;
}
const { activeColor } = innerConfig;
const style = { activeColor };
const {
// sharedStore,
activeStore,
} = snapshot;
// const layoutActionType = sharedStore[keyLayoutActionType];
// const layoutIsHover = sharedStore[keyLayoutIsHoverContent];
// const layoutIsSelected = sharedStore[keyLayoutIsSelected];
const { sharedStore, activeStore } = snapshot;
const layoutActionType = sharedStore[keyLayoutActionType];
const layoutIsHover = sharedStore[keyLayoutIsHoverContent];
const layoutIsSelected = sharedStore[keyLayoutIsSelected];
const viewScaleInfo = sharer.getActiveViewScaleInfo();
if (activeStore.data?.layout) {
const { x, y, w, h } = activeStore.data.layout;
const viewScaleInfo = getViewScaleInfoFromSnapshot(snapshot);
const size = { x, y, w, h };
const controller = calcLayoutSizeController(size, { viewScaleInfo, controllerSize });
if (opts.container && activeStore.data?.layout) {
// if (activeStore.data?.layout) {
// if (layoutIsHover === true) {
// resetMaterialSelectedBox(opts.container, {
// rootClassName,
// viewScaleInfo,
// layout: activeStore.data?.layout,
// hover: true,
// });
// } else if (layoutIsHover === false) {
// clearMaterialLayoutBoxs(opts.container, { rootClassName });
// }
if (layoutIsHover === true) {
const viewSize = calcViewElementSize(size, { viewScaleInfo });
drawLayoutHover(overlayContext, { layoutSize: viewSize, style });
}
if ((layoutActionType && ['resize'].includes(layoutActionType)) || layoutIsSelected === true) {
drawLayoutController(overlayContext, { controller, style });
}
// if (layoutActionType && ['resize'].includes(layoutActionType)) {
resetMaterialSelectedBox(opts.container, {
rootClassName,
viewScaleInfo,
layout: activeStore.data?.layout,
hover: false,
});
// }
// } else {
// // clearMaterialLayoutBoxs(opts.container, { rootClassName });
// }
}
},
scrollX: () => {
clear();
},
scrollY: () => {
clear();
},
wheelScale: () => {
clear();
}
};
};

View file

@ -0,0 +1,55 @@
import type { MiddlewareLayoutSelectorStyles } from '@idraw/types';
import { createId } from '@idraw/util';
export const key = 'LAYOUT_SELECTOR';
// export const keyHoverMaterial = Symbol(`${key}_hoverMaterialSize`);
export const keyLayoutActionType = Symbol(`${key}_layoutActionType`); // 'resize' | null = null;
export const keyLayoutControlType = Symbol(`${key}_layoutControlType`); // ControlType | null;
export const keyLayoutController = Symbol(`${key}_layoutController`); // MaterialSizeController | null = null;
export const keyLayoutIsHoverContent = Symbol(`${key}_layoutIsHoverContent`); // boolean | null
export const keyLayoutIsHoverController = Symbol(`${key}_layoutIsHoverController`); // boolean | null
export const keyLayoutIsSelected = Symbol(`${key}_layoutIsSelected`); // boolean | null
export const keyLayoutIsBusyMoving = Symbol(`${key}_layoutIsSelected`); // boolean | null
export const prefix = `idraw-middleware-layout-selector`;
export const getRootClassName = () => `${prefix}-${createId()}`;
export const ATTR_HANDLER_TYPE = 'data-idraw-handler-type';
export const selectedBoxBorderWidth = 1.5;
export const hoverBoxBorderWidth = 1;
export const cornerHandlerSize = 10;
export const cornerHandlerBorderWidth = 1.5;
export const edgeHandlerSize = 10;
// legacy
export const controllerSize = 10;
export const defaultStyle: MiddlewareLayoutSelectorStyles = {
zIndex: 2,
activeColor: '#b331c9',
handlerBorderColor: '#b331c9',
handlerBackground: '#ffffff',
handlerHoverBackground: '#bb8fc3',
handlerActiveBackground: '#b467c2',
};
export const classNameMap = {
// hover
hover: `${prefix}-hover`,
// edge handler
edgeHandler: `${prefix}-edgeHandler`,
edgeTopHandler: `${prefix}-edgeTopHandler`,
edgeRightHandler: `${prefix}-edgeRightandler`,
edgeBottomHandler: `${prefix}-edgeBottomHandler`,
edgeLeftHandler: `${prefix}-edgeLeftHandler`,
// corner handler
cornerHandler: `${prefix}-cornerHandler`,
cornerTopLeftHandler: `${prefix}-cornerTopLeftHandler`,
cornerTopRightHandler: `${prefix}-cornerTopRightHandler`,
cornerBottomLeftHandler: `${prefix}-cornerBottomLeftHandler`,
cornerBottomRightHandler: `${prefix}-cornerBottomRightHandler`,
};

View file

@ -0,0 +1,149 @@
import type { MiddlewareLayoutSelectorStyles, MiddlewareLayoutSelectorConfig, StylesProps } from '@idraw/types';
import { injectStyles, removeStyles, getMiddlewareValidStyles } from '@idraw/util';
import {
classNameMap,
cornerHandlerBorderWidth,
cornerHandlerSize,
edgeHandlerSize,
hoverBoxBorderWidth,
selectedBoxBorderWidth,
} from './static';
export function initStyles(rootClassName: string, styles: MiddlewareLayoutSelectorStyles) {
const cls = (str: string) => `.${str}`;
const stylesProps: StylesProps = {
zIndex: styles.zIndex,
position: 'absolute',
background: 'transparent',
[`&${cls(classNameMap.hover)}`]: {
[`&${cls(classNameMap.cornerHandler)}`]: {
display: 'none',
},
[`&${cls(classNameMap.edgeHandler)}`]: {
width: `${hoverBoxBorderWidth}px`,
height: `${hoverBoxBorderWidth}px`,
[`&${cls(classNameMap.edgeLeftHandler)}`]: {
width: `${hoverBoxBorderWidth}px`,
},
[`&${cls(classNameMap.edgeRightHandler)}`]: {
width: `${hoverBoxBorderWidth}px`,
},
[`&${cls(classNameMap.edgeTopHandler)}`]: {
height: `${hoverBoxBorderWidth}px`,
},
[`&${cls(classNameMap.edgeBottomHandler)}`]: {
height: `${hoverBoxBorderWidth}px`,
},
},
},
[`&${cls(classNameMap.cornerHandler)}`]: {
outline: `${cornerHandlerBorderWidth}px solid ${styles.handlerBorderColor}`,
background: styles.handlerBackground,
width: `${cornerHandlerSize}px`,
height: `${cornerHandlerSize}px`,
top: 'unset',
bottom: 'unset',
left: 'unset',
right: 'unset',
['&:hover']: {
background: styles.handlerHoverBackground,
},
['&:active']: {
background: styles.handlerActiveBackground,
},
[`&${cls(classNameMap.cornerTopLeftHandler)}`]: {
transform: 'translate(-50%, -50%)',
},
[`&${cls(classNameMap.cornerTopRightHandler)}`]: {
transform: 'translate(-50%, -50%)',
},
[`&${cls(classNameMap.cornerBottomLeftHandler)}`]: {
transform: 'translate(-50%, -50%)',
},
[`&${cls(classNameMap.cornerBottomRightHandler)}`]: {
transform: 'translate(-50%, -50%)',
},
},
[`&${cls(classNameMap.edgeHandler)}`]: {
width: `${cornerHandlerSize}px`,
height: `${cornerHandlerSize}px`,
['&:after']: {
position: 'absolute',
content: '""',
background: styles.handlerBorderColor,
},
[`&${cls(classNameMap.edgeLeftHandler)}`]: {
width: `${edgeHandlerSize}px`,
transform: 'translateX(-50%)',
['&:after']: {
top: 0,
bottom: 0,
left: '50%',
right: 'unset',
width: selectedBoxBorderWidth,
},
},
[`&${cls(classNameMap.edgeRightHandler)}`]: {
width: `${edgeHandlerSize}px`,
transform: 'translateX(-50%)',
['&:after']: {
top: 0,
bottom: 0,
left: '50%',
right: 'unset',
width: selectedBoxBorderWidth,
},
},
[`&${cls(classNameMap.edgeTopHandler)}`]: {
height: `${edgeHandlerSize}px`,
transform: 'translateY(-50%)',
['&:after']: {
left: 0,
right: 0,
top: '50%',
bottom: 'unset',
height: selectedBoxBorderWidth,
},
},
[`&${cls(classNameMap.edgeBottomHandler)}`]: {
height: `${edgeHandlerSize}px`,
transform: 'translateY(-50%)',
['&:after']: {
left: 0,
right: 0,
top: '50%',
bottom: 'unset',
height: selectedBoxBorderWidth,
},
},
},
};
injectStyles({ styles: stylesProps, rootClassName, type: 'element' });
}
export function destroyStyles(rootClassName: string) {
removeStyles({ rootClassName, type: 'element' });
}
export function getMiddlewareLayoutSelectorStyles<
C = MiddlewareLayoutSelectorConfig,
S = MiddlewareLayoutSelectorStyles,
>(config: C): S {
const styles: S = getMiddlewareValidStyles<C, S>(config, [
'zIndex',
'activeColor',
'handlerBorderColor',
'handlerBackground',
'handlerHoverBackground',
'handlerActiveBackground',
]);
return styles;
}

View file

@ -1,4 +1,4 @@
import type { LayoutSizeController, Element } from '@idraw/types';
import type { LayoutSizeController, Material } from '@idraw/types';
import {
keyLayoutActionType,
keyLayoutControlType,
@ -6,21 +6,29 @@ import {
keyLayoutIsHoverContent,
keyLayoutIsHoverController,
keyLayoutIsSelected,
keyLayoutIsBusyMoving
} from './config';
import { keyActionType as keyElementActionType, keyHoverElement } from '../selector';
import type { ActionType as ElementActionType } from '../selector';
keyLayoutIsBusyMoving,
} from './static';
import { keyActionType as keyMaterialActionType, keyHoverMaterial } from '../selector';
import type { ActionType as MaterialActionType } from '../selector';
export type ActionType = 'resize' | null;
export type ControlType = 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
export type ControlType =
| 'left'
| 'right'
| 'top'
| 'bottom'
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right';
export type LayoutSelectorSharedStorage = {
[keyLayoutActionType]: ActionType | null;
[keyLayoutControlType]: ControlType | null;
[keyLayoutController]: LayoutSizeController | null;
[keyElementActionType]: ElementActionType | null;
[keyHoverElement]: Element | null;
[keyMaterialActionType]: MaterialActionType | null;
[keyHoverMaterial]: Material | null;
[keyLayoutIsHoverContent]: boolean | null;
[keyLayoutIsHoverController]: boolean | null;
[keyLayoutIsSelected]: boolean | null;

View file

@ -2,13 +2,13 @@ import type {
ViewContext2D,
LayoutSizeController,
ViewRectVertexes,
PointSize,
ElementSize,
MiddlewareLayoutSelectorStyle
Point,
MaterialSize,
MiddlewareLayoutSelectorStyles,
} from '@idraw/types';
function drawControllerBox(ctx: ViewContext2D, boxVertexes: ViewRectVertexes, style: MiddlewareLayoutSelectorStyle) {
const { activeColor } = style;
function drawControllerBox(ctx: ViewContext2D, boxVertexes: ViewRectVertexes, styles: MiddlewareLayoutSelectorStyles) {
const { activeColor } = styles;
ctx.setLineDash([]);
ctx.fillStyle = '#FFFFFF';
ctx.beginPath();
@ -32,10 +32,10 @@ function drawControllerBox(ctx: ViewContext2D, boxVertexes: ViewRectVertexes, st
function drawControllerLine(
ctx: ViewContext2D,
opts: { start: PointSize; end: PointSize; centerVertexes: ViewRectVertexes; style: MiddlewareLayoutSelectorStyle }
opts: { start: Point; end: Point; centerVertexes: ViewRectVertexes; styles: MiddlewareLayoutSelectorStyles }
) {
const { start, end, style } = opts;
const { activeColor } = style;
const { start, end, styles } = opts;
const { activeColor } = styles;
const lineWidth = 2;
const strokeStyle = activeColor;
ctx.setLineDash([]);
@ -52,56 +52,56 @@ export function drawLayoutController(
ctx: ViewContext2D,
opts: {
controller: LayoutSizeController;
style: MiddlewareLayoutSelectorStyle;
styles: MiddlewareLayoutSelectorStyles;
}
) {
const { controller, style } = opts;
const { controller, styles } = opts;
const { topLeft, topRight, bottomLeft, bottomRight, topMiddle, rightMiddle, bottomMiddle, leftMiddle } = controller;
drawControllerLine(ctx, { start: topLeft.center, end: topRight.center, centerVertexes: topMiddle.vertexes, style });
drawControllerLine(ctx, { start: topLeft.center, end: topRight.center, centerVertexes: topMiddle.vertexes, styles });
drawControllerLine(ctx, {
start: topRight.center,
end: bottomRight.center,
centerVertexes: rightMiddle.vertexes,
style
styles,
});
drawControllerLine(ctx, {
start: bottomRight.center,
end: bottomLeft.center,
centerVertexes: bottomMiddle.vertexes,
style
styles,
});
drawControllerLine(ctx, {
start: bottomLeft.center,
end: topLeft.center,
centerVertexes: leftMiddle.vertexes,
style
styles,
});
drawControllerBox(ctx, topLeft.vertexes, style);
drawControllerBox(ctx, topRight.vertexes, style);
drawControllerBox(ctx, bottomRight.vertexes, style);
drawControllerBox(ctx, bottomLeft.vertexes, style);
drawControllerBox(ctx, topLeft.vertexes, styles);
drawControllerBox(ctx, topRight.vertexes, styles);
drawControllerBox(ctx, bottomRight.vertexes, styles);
drawControllerBox(ctx, bottomLeft.vertexes, styles);
}
export function drawLayoutHover(
ctx: ViewContext2D,
opts: {
layoutSize: ElementSize;
style: MiddlewareLayoutSelectorStyle;
layoutSize: MaterialSize;
styles: MiddlewareLayoutSelectorStyles;
}
) {
const { layoutSize, style } = opts;
const { activeColor } = style;
const { x, y, w, h } = layoutSize;
const { layoutSize, styles } = opts;
const { activeColor } = styles;
const { x, y, width, height } = layoutSize;
ctx.setLineDash([]);
ctx.strokeStyle = activeColor;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + w, y);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x, y + h);
ctx.lineTo(x + width, y);
ctx.lineTo(x + width, y + height);
ctx.lineTo(x, y + height);
ctx.lineTo(x, y);
ctx.closePath();
ctx.stroke();

View file

@ -0,0 +1,206 @@
import type { StylesProps, Point, ViewScaleInfo, MiddlewarePathCreatorStyles } from '@idraw/types';
import {
injectStyles,
removeStyles,
createHTMLElement,
setHTMLCSSProps,
createId,
calcViewPoint,
ATTR_VALID_WATCH,
} from '@idraw/util';
import {
ATTR_X,
ATTR_Y,
ATTR_AHCHOR_CMD_TYPE,
ATTR_AHCHOR_INDEX,
ATTR_AHCHOR_ID,
ATTR_HELPER_TYPE,
HELPER_ANCHOR,
classNameMap,
} from './static';
export function initStyles(rootClassName: string, styles: MiddlewarePathCreatorStyles) {
const stylesProps: StylesProps = {
display: 'flex',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
overflow: 'hidden',
justifyContent: 'center',
alignItems: 'center',
[`.${classNameMap.anchor}`]: {
position: 'absolute',
width: styles.anchorSize,
height: styles.anchorSize,
background: styles.anchorBackground,
border: `${styles.anchorBorderWidth}px solid ${styles.anchorBorderColor}`,
borderRadius: '50%',
overflow: 'hidden',
['&:hover']: {
borderColor: styles.anchorHoverBorderColor,
background: styles.anchorHoverBackground,
},
['&:active']: {
borderColor: styles.anchorActiveBorderColor,
background: styles.anchorActiveBackground,
},
[`&.${classNameMap.selected}`]: {
borderColor: styles.anchorActiveBorderColor,
background: styles.anchorActiveBackground,
},
},
};
injectStyles({
styles: stylesProps,
rootClassName,
type: 'element',
});
}
export function destroyStyles(rootClassName: string) {
removeStyles({ rootClassName, type: 'element' });
}
export function initRoot(container: HTMLElement, opts: { id: string; rootClassName: string }) {
const { id, rootClassName } = opts;
if (!container) {
return;
}
const root = createHTMLElement('div', {
id,
className: [classNameMap.hide, rootClassName].join(' '),
[ATTR_VALID_WATCH]: 'true',
});
if (!container.contains(root)) {
container.appendChild(root);
}
return root;
}
const getAnchorPosition = (opts: { x: number; y: number; size: number; borderWidth: number }) => {
const { x, y, size, borderWidth } = opts;
return {
left: x - size / 2 - borderWidth,
top: y - size / 2 - borderWidth,
};
};
export function createAnchorElement(opts: {
id: string;
index: number;
point: Point;
commandType: string;
viewScaleInfo: ViewScaleInfo;
styles: MiddlewarePathCreatorStyles;
}) {
const { id, index, point, commandType, viewScaleInfo, styles } = opts;
const viewPoint = calcViewPoint(point, { viewScaleInfo });
const $anchor: HTMLElement = createHTMLElement('div', {
[ATTR_HELPER_TYPE]: HELPER_ANCHOR,
[ATTR_AHCHOR_CMD_TYPE]: commandType,
[ATTR_AHCHOR_INDEX]: index,
[ATTR_AHCHOR_ID]: id,
[ATTR_VALID_WATCH]: 'true',
[ATTR_X]: point.x,
[ATTR_Y]: point.y,
className: classNameMap.anchor,
style: {
...getAnchorPosition({
x: viewPoint.x,
y: viewPoint.y,
size: styles.anchorSize,
borderWidth: styles.anchorBorderWidth,
}),
display: 'block',
},
});
return $anchor;
}
export function appendAnchorElement(
root: HTMLElement,
opts: {
point: Point;
viewScaleInfo: ViewScaleInfo;
styles: MiddlewarePathCreatorStyles;
}
) {
const { point, viewScaleInfo, styles } = opts;
const $existedAnchors = Array.from(root.querySelectorAll(`[${ATTR_HELPER_TYPE}="${HELPER_ANCHOR}"]`));
const index = $existedAnchors.length;
const id = createId();
const $anchor = createAnchorElement({
index,
id,
point,
styles,
viewScaleInfo,
commandType: index === 0 ? 'M' : 'C',
});
if (index === 0) {
root.appendChild($anchor);
} else {
const $lastAnchor = $existedAnchors[$existedAnchors.length - 1];
$lastAnchor.after($anchor);
}
return { id };
}
const getAnchorElementInfo = (elem: HTMLElement) => {
const id = elem.getAttribute(ATTR_AHCHOR_ID) || '';
const type = elem.getAttribute(ATTR_HELPER_TYPE) || '';
const x = parseFloat(elem.getAttribute(ATTR_X) || '0');
const y = parseFloat(elem.getAttribute(ATTR_Y) || '0');
const info = { id, type, x, y };
return info;
};
export function updateAnchorsStyle(
root: HTMLDivElement,
opts: {
viewScaleInfo: ViewScaleInfo;
styles: MiddlewarePathCreatorStyles;
}
) {
const { viewScaleInfo, styles } = opts;
const $anchors = Array.from(root.querySelectorAll(`[${ATTR_HELPER_TYPE}="${HELPER_ANCHOR}"]`)) as HTMLElement[];
$anchors.forEach(($anchor) => {
const info = getAnchorElementInfo($anchor);
const viewPoint = calcViewPoint({ x: info.x, y: info.y }, { viewScaleInfo });
setHTMLCSSProps(
$anchor,
getAnchorPosition({
...viewPoint,
size: styles.anchorSize,
borderWidth: styles.anchorBorderWidth,
})
);
});
}
export function isAnchorElement(elem: HTMLElement) {
return elem.getAttribute(ATTR_HELPER_TYPE) === HELPER_ANCHOR;
}
export function getIndexFromAnchorElement(elem: HTMLElement): number | null {
const index = elem.getAttribute(ATTR_AHCHOR_INDEX);
if (typeof index === 'string') {
return parseInt(index);
}
return index;
}
export function clearRoot(root: HTMLElement | null) {
if (!root) {
return;
}
const children = Array.from(root.children);
children.forEach((child) => {
child.remove();
});
}

View file

@ -0,0 +1,274 @@
import type {
Point,
Middleware,
CoreEventMap,
StrictMaterial,
ModifyRecord,
MiddlewarePathCreatorConfig,
} from '@idraw/types';
import {
createId,
getHTMLElementRectInPage,
calcPointFromView,
createUUID,
convertLineToExactCurveCommand,
updateMaterialInList,
refinePathMaterial,
deepClone,
} from '@idraw/util';
import { coreEventKeys } from '../../static';
import type { PathSharedStorage } from './types';
import {
initRoot,
clearRoot,
appendAnchorElement,
updateAnchorsStyle,
isAnchorElement,
getIndexFromAnchorElement,
initStyles,
destroyStyles,
} from './dom';
import { defaultConfig, getRootClassName, getMiddlewarePathCreatorStyles } from './static';
import { triggerChangeEvent } from '../common';
export { getMiddlewarePathCreatorStyles };
export const MiddlewarePathCreator: Middleware<PathSharedStorage, CoreEventMap, MiddlewarePathCreatorConfig> = (
opts,
config
) => {
const innerConfig = { ...defaultConfig, ...config };
const { defaultStrokeWidth, defaultStroke } = innerConfig;
const styles = getMiddlewarePathCreatorStyles(innerConfig);
const rootClassName = getRootClassName();
const { viewer, eventHub, sharer, calculator } = opts;
const container = opts.container;
const id = rootClassName;
let root: HTMLDivElement | null = null;
let pathCommandIndex: number = -1;
let createdPathMaterial: StrictMaterial<'path'> | null = null;
let prevPoint: Point | null = null;
const clearData = () => {
clearRoot(root);
prevPoint = null;
createdPathMaterial = null;
pathCommandIndex = -1;
};
const refineData = () => {
if (!createdPathMaterial) {
return;
}
createdPathMaterial = refinePathMaterial(createdPathMaterial);
const data = sharer.getActiveStorage('data');
updateMaterialInList(
createdPathMaterial.id,
{
x: createdPathMaterial.x,
y: createdPathMaterial.y,
width: createdPathMaterial.width,
height: createdPathMaterial.height,
commands: createdPathMaterial.commands,
},
data.materials
);
calculator.modifyVirtualAttributes(createdPathMaterial, {
viewScaleInfo: sharer.getActiveViewScaleInfo(),
viewSizeInfo: sharer.getActiveViewSizeInfo(),
groupQueue: [],
});
calculator.forceVisiable(createdPathMaterial.id);
viewer.drawFrame();
};
const refreshMaterials = () => {
if (!createdPathMaterial) {
return;
}
const data = sharer.getActiveStorage('data');
if (pathCommandIndex > 0) {
updateMaterialInList(
createdPathMaterial.id,
{
x: createdPathMaterial.x,
y: createdPathMaterial.y,
width: createdPathMaterial.width,
height: createdPathMaterial.height,
commands: createdPathMaterial.commands,
},
data.materials
);
} else {
data.materials.push(createdPathMaterial);
const modifyRecord: ModifyRecord<'addMaterial'> = {
type: 'addMaterial',
time: Date.now(),
content: {
method: 'addMaterial',
id: createdPathMaterial.id,
position: [data.materials?.length],
material: deepClone(createdPathMaterial),
},
};
triggerChangeEvent(eventHub, { data, type: 'addMaterial', modifyRecord });
}
calculator.modifyVirtualAttributes(createdPathMaterial, {
viewScaleInfo: sharer.getActiveViewScaleInfo(),
viewSizeInfo: sharer.getActiveViewSizeInfo(),
groupQueue: [],
});
calculator.forceVisiable(createdPathMaterial.id);
viewer.drawFrame();
};
const mouseDownEvent = (e: MouseEvent) => {
const $target = e.target as HTMLElement;
if (isAnchorElement($target)) {
const index = getIndexFromAnchorElement($target);
if (index === 0 && pathCommandIndex > 1 && createdPathMaterial) {
createdPathMaterial.commands.push({
id: createId(),
type: 'Z',
params: [],
});
refineData();
clearData();
}
return;
}
const rootRect = getHTMLElementRectInPage(root as HTMLDivElement);
const viewPoint = {
x: e.pageX - rootRect.pageX,
y: e.pageY - rootRect.pageY,
};
const viewScaleInfo = sharer.getActiveViewScaleInfo();
const viewSizeInfo = sharer.getActiveViewSizeInfo();
const point = calcPointFromView(viewPoint, {
viewScaleInfo,
});
const { id } = appendAnchorElement(root as HTMLElement, { point, viewScaleInfo, styles });
if (pathCommandIndex < 0 || !createdPathMaterial) {
pathCommandIndex = 0;
createdPathMaterial = {
id: createUUID(),
type: 'path',
x: 0,
y: 0,
width: viewSizeInfo.width,
height: viewSizeInfo.height,
strokeWidth: defaultStrokeWidth,
stroke: defaultStroke,
commands: [{ id, type: 'M', params: [point.x, point.y] }],
};
} else {
pathCommandIndex++;
(createdPathMaterial as StrictMaterial<'path'>).commands.push({
...convertLineToExactCurveCommand(prevPoint as Point, point),
id,
});
}
// createdPathMaterial = refinePathMaterial(createdPathMaterial);
prevPoint = point;
refreshMaterials();
};
const mouseMoveEvent = () => {
// TODO
};
const mouseUpEvent = () => {
window.removeEventListener('mousemove', mouseMoveEvent);
};
const mouseLeaveEvent = () => {
window.removeEventListener('mousemove', mouseMoveEvent);
// TODO
};
const onEvents = () => {
root?.addEventListener('mousedown', mouseDownEvent);
// window.addEventListener('mousemove', mouseMoveEvent);
window.addEventListener('mouseup', mouseUpEvent);
window.addEventListener('mouseleave', mouseLeaveEvent);
};
const offEvents = () => {
root?.removeEventListener('mousedown', mouseDownEvent);
// window.removeEventListener('mousemove', mouseMoveEvent);
window.removeEventListener('mouseup', mouseUpEvent);
window.removeEventListener('mouseleave', mouseLeaveEvent);
};
const init = () => {
if (!container) {
return;
}
root = initRoot(container, { id, rootClassName }) as HTMLDivElement;
if (!container.contains(root)) {
container.appendChild(root);
}
};
const destroy = () => {
offEvents();
root?.remove();
root = null;
};
const pathCreateCallback = () => {
// TODO: reset root doms
onEvents();
viewer.drawFrame();
};
const clearPathCreateCallback = () => {
refineData();
offEvents();
clearData();
};
const clear = () => {
clearData();
viewer.drawFrame();
};
return {
name: '@middleware/pen-create',
use() {
initStyles(rootClassName, styles);
eventHub.on(coreEventKeys.PATH_CREATE, pathCreateCallback);
eventHub.on(coreEventKeys.CLEAR_PATH_CREATE, clearPathCreateCallback);
init();
},
disuse() {
destroyStyles(rootClassName);
eventHub.off(coreEventKeys.PATH_CREATE, pathCreateCallback);
eventHub.off(coreEventKeys.CLEAR_PATH_CREATE, clearPathCreateCallback);
clear();
destroy();
},
beforeDrawFrame() {
updateAnchorsStyle(root as HTMLDivElement, {
viewScaleInfo: sharer.getActiveViewScaleInfo(),
styles,
});
},
hover() {
eventHub.trigger(coreEventKeys.CURSOR, {
type: 'pen',
});
return false;
},
};
};

View file

@ -0,0 +1,61 @@
import { createId, getMiddlewareValidStyles } from '@idraw/util';
import type { MiddlewarePathCreatorConfig, MiddlewarePathCreatorStyles } from '@idraw/types';
export const key = 'PATH-CREATOR';
const prefix = `idraw-middleware-path-creator`;
export const getRootClassName = () => `${prefix}-${createId()}`;
export const classNameMap = {
hide: `${prefix}-hide`,
anchor: `${prefix}-anchor`,
director: `${prefix}-director`,
directorLines: `${prefix}-director-lines`,
pathLine: `${prefix}-path-line`,
selected: `${prefix}-selected`,
};
export const ATTR_X = `data-x`;
export const ATTR_Y = `data-y`;
export const ATTR_ANGLE = `data-angle`;
export const ATTR_TYPE = `data-type`;
export const ATTR_HELPER_TYPE = `data-helper-type`;
export const ATTR_AHCHOR_CMD_TYPE = `data-anchor-cmd-type`;
export const ATTR_AHCHOR_INDEX = `data-anchor-index`;
export const ATTR_AHCHOR_ID = `data-anchor-id`;
export const HELPER_ROOT = 'root';
export const HELPER_ANCHOR = 'anchor';
export const defaultConfig: MiddlewarePathCreatorConfig = {
anchorSize: 8,
anchorBorderWidth: 2,
anchorBorderColor: '#157ed1',
anchorBackground: '#ffffff',
anchorHoverBorderColor: '#1671b8',
anchorHoverBackground: '#cfe4f4',
anchorActiveBorderColor: '#0d548c',
anchorActiveBackground: '#88c0ec',
defaultStroke: '#a0a0a0',
defaultStrokeWidth: 2,
};
export function getMiddlewarePathCreatorStyles<C = MiddlewarePathCreatorConfig, S = MiddlewarePathCreatorStyles>(
config: C
): S {
const styles: S = getMiddlewareValidStyles<C, S>(config, [
'anchorSize',
'anchorBorderWidth',
'anchorBorderColor',
'anchorBackground',
'anchorHoverBorderColor',
'anchorHoverBackground',
'anchorActiveBorderColor',
'anchorActiveBackground',
'defaultStroke',
'defaultStrokeWidth',
]);
return styles;
}

View file

@ -0,0 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export type PathSharedStorage = {
// [keySelectedPathElement]: null | Element<'path'>; // TODO
};

View file

@ -0,0 +1,77 @@
/**
* Get the "visible" bounding box of an SVG path element (including stroke, linecap, and linejoin effects).
* Returns the bbox in the SVG user coordinate system (x, y, width, height).
*
* Compatibility strategy:
* 1) Try SVG2: getBBox({ stroke: true, fill: false, markers: false, clipped: true })
* 2) Fallback: use getBoundingClientRect() (bounding box in screen coordinates) + getScreenCTM() inverse transform back to SVG user coordinates
*
* Notes:
* - This method is practical for "visible geometry", automatically accounting for miter/round/bevel joins,
* butt/round/square caps, and stroke-width effects.
* - If the element has filters, shadows, glows, etc., the visual bounding box will be enlarged by them;
* this function will include them as well (since they affect the visual footprint).
* - If you only want the geometric path without stroke, use path.getBBox() (old version without parameters).
*/
export function calcVisibleBBoxOfPath(path: SVGPathElement) {
const svg = path.ownerSVGElement;
if (!svg) {
throw new Error('The path is not inside an <svg> element.');
}
// 1) SVG2: Some browsers implement getBBox with options
try {
const fancyBBox = path.getBBox({ stroke: true, fill: false, markers: true, clipped: true });
if (fancyBBox && Number.isFinite(fancyBBox.width) && Number.isFinite(fancyBBox.height)) {
return {
x: fancyBBox.x,
y: fancyBBox.y,
width: fancyBBox.width,
height: fancyBBox.height,
};
}
} catch {
// Ignore and fall back
}
// 2) Fallback: use visible bounding box in screen coordinates and transform to SVG user coordinates
const ctm = svg.getScreenCTM();
if (!ctm) throw new Error('Failed to get screen CTM from the <svg>.');
const inv = ctm.inverse(); // Screen coordinates → SVG user coordinates
// Visible bounding box in screen coordinates (usually includes stroke, linecap, linejoin effects)
const rect = path.getBoundingClientRect();
// Convert the four rectangle corners from screen coordinates to SVG user coordinates
const corners = [
{ x: rect.left, y: rect.top },
{ x: rect.right, y: rect.top },
{ x: rect.right, y: rect.bottom },
{ x: rect.left, y: rect.bottom },
];
const svgPoint = svg.createSVGPoint();
let minX = Infinity,
minY = Infinity,
maxX = -Infinity,
maxY = -Infinity;
for (const c of corners) {
svgPoint.x = c.x;
svgPoint.y = c.y;
// Transform screen → user coordinates
const p = svgPoint.matrixTransform(inv);
if (p.x < minX) minX = p.x;
if (p.y < minY) minY = p.y;
if (p.x > maxX) maxX = p.x;
if (p.y > maxY) maxY = p.y;
}
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}

View file

@ -0,0 +1,793 @@
import type {
MaterialSize,
StrictMaterial,
ViewScaleInfo,
HTMLProps,
ViewCalculator,
VirtualPathAttributes,
StylesProps,
PathAnchorCommand,
MiddlewarePathEditorStyles,
} from '@idraw/types';
import {
createHTMLElement,
limitAngle,
setHTMLCSSProps,
scalePathCommands,
injectStyles,
removeStyles,
removeClassName,
parseHTMLStr,
convertPathCommandsToStr,
assembleHTMLElement,
ATTR_VALID_WATCH,
} from '@idraw/util';
import {
ATTR_UUID,
ATTR_X,
ATTR_Y,
ATTR_W,
ATTR_H,
ATTR_ANGLE,
ATTR_TYPE,
ATTR_AHCHOR_CMD_TYPE,
ATTR_AHCHOR_INDEX,
ATTR_AHCHOR_ID,
ATTR_HELPER_TYPE,
ATTR_DIRECTOR_FROM_AHCHOR_ID,
ATTR_DIRECTOR_OPENED_BY_AHCHOR_ID,
ATTR_DIRECTOR_CONTROL_TYPE,
HELPER_ELEMENT,
HELPER_GROUP,
HELPER_ANCHOR,
HELPER_DIRECTOR,
HELPER_DIRECTOR_LINE,
HELPER_PATH_PREVIEW,
HELPER_PATH_DEFINITION,
// anchorSize,
// anchorSelectedSize,
// anchorBorder,
// anchorStyle,
// anchorHoverStyle,
// anchorActiveStyle,
// directorSize,
// directorBorder,
// directorStyle,
// directorLineStyle,
// directorHoverStyle,
// directorActiveStyle,
classNameMap,
} from './static';
import type { Directioner, AnchorInfo, DirectorInfo } from './types';
// import { calcVisibleBBoxOfPath } from './calc';
export function initStyles(rootClassName: string, styles: MiddlewarePathEditorStyles) {
const stylesProps: StylesProps = {
display: 'flex',
position: 'absolute',
zIndex: styles.zIndex,
top: 0,
left: 0,
right: 0,
bottom: 0,
overflow: 'hidden',
justifyContent: 'center',
alignItems: 'center',
[`&.${classNameMap.hide}`]: {
display: 'none',
},
[`.${classNameMap.anchor}`]: {
position: 'absolute',
width: styles.anchorSize,
height: styles.anchorSize,
background: styles.anchorBackground,
border: `${styles.anchorBorderWidth}px solid ${styles.anchorBorderColor}`,
borderRadius: '50%',
overflow: 'hidden',
['&:hover']: {
borderColor: styles.anchorHoverBorderColor,
background: styles.anchorHoverBackground,
},
['&:active']: {
borderColor: styles.anchorActiveBorderColor,
background: styles.anchorActiveBackground,
},
[`&.${classNameMap.selected}`]: {
borderColor: styles.anchorActiveBorderColor,
background: styles.anchorActiveBackground,
},
},
[`.${classNameMap.director}`]: {
position: 'absolute',
width: styles.directorSize,
height: styles.directorSize,
background: styles.directorBackground,
border: `${styles.directorBorderWidth}px solid ${styles.directorBorderColor}`,
overflow: 'hidden',
['&:hover']: {
borderColor: styles.directorHoverBorderColor,
background: styles.directorHoverBackground,
},
['&:active']: {
borderColor: styles.directorActiveBorderColor,
background: styles.directorActiveBackground,
},
[`&.${classNameMap.selected}`]: {
borderColor: styles.directorActiveBorderColor,
background: styles.directorActiveBackground,
},
},
[`.${classNameMap.directorLines}`]: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
};
injectStyles({ styles: stylesProps, rootClassName, type: 'element' });
}
export function destroyStyles(rootClassName: string) {
removeStyles({ rootClassName, type: 'element' });
}
export function initRoot(container: HTMLElement, opts: { id: string; rootClassName: string }) {
const { id, rootClassName } = opts;
if (!container) {
return;
}
const root = createHTMLElement('div', {
id,
className: [classNameMap.hide, rootClassName].join(' '),
[ATTR_VALID_WATCH]: 'true',
});
if (!container.contains(root)) {
container.appendChild(root);
}
return root;
}
const createBox = (opts: { size: MaterialSize; parent: HTMLDivElement }, props: HTMLProps) => {
const { size, parent } = opts;
const { x, y, width, height } = size;
const angle = limitAngle(size.angle || 0);
const div = createHTMLElement('div', {
[ATTR_VALID_WATCH]: 'true',
style: {
// ...defaultStyle,
position: 'absolute',
left: x,
top: y,
width,
height,
transform: `rotate(${angle}deg)`,
},
...props,
});
parent.appendChild(div);
return div;
};
const getBoxMaterialInfo = (box: HTMLElement) => {
const id = box.getAttribute(ATTR_UUID) || '';
const type = box.getAttribute(ATTR_TYPE) || '';
const x = parseFloat(box.getAttribute(ATTR_X) || '0');
const y = parseFloat(box.getAttribute(ATTR_Y) || '0');
const w = parseFloat(box.getAttribute(ATTR_W) || '0');
const h = parseFloat(box.getAttribute(ATTR_H) || '0');
const angle = parseFloat(box.getAttribute(ATTR_ANGLE) || '0');
const info = { id, type, x, y, w, h, angle };
return info;
};
const getAnchorPosition = (opts: { x: number; y: number; size: number; borderWidth: number }) => {
const { x, y, size, borderWidth } = opts;
return {
left: x - size / 2 - borderWidth,
top: y - size / 2 - borderWidth,
};
};
const getDirectorPosition = (opts: { x: number; y: number; size: number; borderWidth: number }) => {
const { x, y, size, borderWidth } = opts;
return {
left: x - size / 2 - borderWidth,
top: y - size / 2 - borderWidth,
};
};
export const getAnchorHandlerInfo = (handler: HTMLElement) => {
const id = handler.getAttribute(ATTR_AHCHOR_ID) || '';
const index = parseFloat(handler.getAttribute(ATTR_AHCHOR_INDEX) || '0');
const info: AnchorInfo = {
index,
id,
};
return info;
};
export const getDirectorHandlerInfo = (handler: HTMLElement) => {
const type = handler.getAttribute(ATTR_DIRECTOR_CONTROL_TYPE) as DirectorInfo['type'];
const fromAnchorId = handler.getAttribute(ATTR_DIRECTOR_FROM_AHCHOR_ID) || '';
const openedAnchorId = handler.getAttribute(ATTR_DIRECTOR_OPENED_BY_AHCHOR_ID) || '';
const info: DirectorInfo = {
type,
fromAnchorId,
openedAnchorId,
};
return info;
};
export const resetRoot = (
root: HTMLElement | null,
opts: {
material: StrictMaterial<'path'> | null;
calculator: ViewCalculator;
viewScaleInfo: ViewScaleInfo;
groupQueue: StrictMaterial<'group'>[];
styles: MiddlewarePathEditorStyles;
}
) => {
const { material, calculator, viewScaleInfo, groupQueue, styles } = opts;
if (!root || !material) {
return;
}
const { scale, offsetTop, offsetLeft } = viewScaleInfo;
if (root?.children) {
Array.from(root.children).forEach((child) => {
child.remove();
});
}
removeClassName(root, [classNameMap.hide]);
let parent = root as HTMLDivElement;
for (let i = 0; i < groupQueue.length; i++) {
const group = groupQueue[i];
const { x, y, width, height } = group;
const angle = limitAngle(group.angle || 0);
const size = {
x: x * scale,
y: y * scale,
width: width * scale,
height: height * scale,
angle,
};
if (i === 0) {
size.x += offsetLeft;
size.y += offsetTop;
}
parent = createBox(
{ size, parent },
{
[ATTR_UUID]: group.id,
[ATTR_X]: group.x,
[ATTR_Y]: group.y,
[ATTR_W]: group.width,
[ATTR_H]: group.height,
[ATTR_ANGLE]: group.angle,
[ATTR_TYPE]: group.type,
[ATTR_HELPER_TYPE]: HELPER_GROUP,
}
);
}
let mtrlX = material.x * scale + offsetLeft;
let mtrlY = material.y * scale + offsetTop;
let mtrlW = material.width * scale;
let mtrlH = material.height * scale;
if (groupQueue.length > 0) {
mtrlX = material.x * scale;
mtrlY = material.y * scale;
mtrlW = material.width * scale;
mtrlH = material.height * scale;
}
const targetBox = createHTMLElement('div', {
[ATTR_UUID]: material.id,
[ATTR_X]: material.x,
[ATTR_Y]: material.y,
[ATTR_W]: material.width,
[ATTR_H]: material.height,
[ATTR_ANGLE]: material.angle,
[ATTR_TYPE]: material.type,
[ATTR_HELPER_TYPE]: HELPER_ELEMENT,
[ATTR_VALID_WATCH]: 'true',
style: {
display: 'inline-flex',
flexDirection: 'column',
position: 'absolute',
left: mtrlX,
top: mtrlY,
width: mtrlW,
height: mtrlH,
transform: `rotate(${limitAngle(material.angle || 0)}deg)`,
boxSizing: 'border-box',
overflow: 'visible',
padding: '0',
margin: '0',
outline: 'none',
},
});
const flatItem: VirtualPathAttributes = calculator.getVirtualItem(material.id) as VirtualPathAttributes;
const cmds = scalePathCommands(flatItem.anchorCommands || [], scale, scale);
cmds.forEach((cmd, i) => {
const $anchor: HTMLElement = createHTMLElement('div', {
[ATTR_HELPER_TYPE]: HELPER_ANCHOR,
[ATTR_AHCHOR_CMD_TYPE]: cmd.type,
[ATTR_AHCHOR_INDEX]: i,
[ATTR_AHCHOR_ID]: cmd.id,
[ATTR_VALID_WATCH]: 'true',
// [ATTR_X]: cmd.start.x,
// [ATTR_Y]: cmd.start.y,
className: classNameMap.anchor,
style: {
...getAnchorPosition({
x: cmd.start.x,
y: cmd.start.y,
size: styles.anchorSize,
borderWidth: styles.anchorBorderWidth,
}),
display: cmd.type === 'M' ? 'none' : 'block',
},
});
targetBox.appendChild($anchor);
});
parent.appendChild(targetBox);
resetPathLine(root, {
anchorCommands: cmds,
material,
viewScaleInfo,
styles,
});
};
export const resetAnchorStyle = (
root: HTMLElement | null,
opts: {
selectedAnchorId?: string;
material: StrictMaterial<'path'> | null;
viewScaleInfo: ViewScaleInfo;
calculator: ViewCalculator;
styles: MiddlewarePathEditorStyles;
}
) => {
if (!root) {
return;
}
const { material, viewScaleInfo, calculator, selectedAnchorId, styles } = opts;
if (!material) {
return;
}
const { scale, offsetTop, offsetLeft } = viewScaleInfo;
let current: SVGElement | HTMLElement | null = root.children[0] as HTMLElement;
let index = 0;
while (['group', 'material'].includes(current?.getAttribute(ATTR_HELPER_TYPE) as string)) {
if (current?.getAttribute(ATTR_HELPER_TYPE) === 'material') {
setHTMLCSSProps(current, {
width: material.width,
height: material.height,
left: material.x,
top: material.y,
});
assembleHTMLElement(current, {
[ATTR_W]: material.width,
[ATTR_H]: material.height,
[ATTR_X]: material.x,
[ATTR_Y]: material.y,
});
}
const { x, y, w, h, angle } = getBoxMaterialInfo(current);
const size = {
x: x * scale,
y: y * scale,
w: w * scale,
h: h * scale,
angle,
};
if (index === 0) {
size.x += offsetLeft;
size.y += offsetTop;
}
setHTMLCSSProps(current, {
left: size.x,
top: size.y,
width: size.w,
height: size.h,
transform: `rotate(${size.angle}deg)`,
});
if (current?.children?.[0]?.getAttribute(ATTR_HELPER_TYPE) !== 'material') {
break;
}
current = current?.children?.[0] as HTMLElement;
index++;
}
const { id } = material as StrictMaterial<'path'>;
const flatItem: VirtualPathAttributes = calculator.getVirtualItem(id) as VirtualPathAttributes;
const cmds = scalePathCommands(flatItem.anchorCommands || [], scale, scale);
{
// render anchor style
const $anchors: HTMLElement[] = Array.from(current.querySelectorAll(`[${ATTR_HELPER_TYPE}="${HELPER_ANCHOR}"]`));
$anchors.forEach(($anchor, i) => {
const cmd = cmds[i];
const id = $anchor.getAttribute(ATTR_AHCHOR_ID);
const size = id === selectedAnchorId ? styles.anchorSelectedSize : styles.anchorSize;
setHTMLCSSProps($anchor, {
width: size,
height: size,
left: cmd.start.x - size / 2 - styles.anchorBorderWidth,
top: cmd.start.y - size / 2 - styles.anchorBorderWidth,
});
});
}
// render director style
if (typeof selectedAnchorId === 'string' && selectedAnchorId) {
const anchorIndex = cmds.findIndex((cmd) => cmd.id === selectedAnchorId);
const curveCmd: PathAnchorCommand | undefined = cmds[anchorIndex as number];
const prevCurveCmd: PathAnchorCommand | undefined = cmds[(anchorIndex as number) - 1];
let currentDirector: Directioner | null = null;
let prevDirector: Directioner | null = null;
if (curveCmd.type === 'C') {
currentDirector = {
openedByAnchorId: selectedAnchorId,
anchorId: curveCmd.id,
anchorPoint: { x: curveCmd.start.x, y: curveCmd.start.y },
directPoint: { x: curveCmd.params[0], y: curveCmd.params[1] },
};
}
if (prevCurveCmd.type === 'C') {
prevDirector = {
openedByAnchorId: selectedAnchorId,
anchorId: prevCurveCmd.id,
anchorPoint: { x: prevCurveCmd.end.x, y: prevCurveCmd.end.y },
directPoint: { x: prevCurveCmd.params[2], y: prevCurveCmd.params[3] },
};
}
if (currentDirector || prevDirector) {
resetDirectionerStyle(root, { selectedAnchorId, currentDirector, prevDirector, styles });
resetDirectorLine(root, { currentDirector, prevDirector, styles });
} else {
clearDirectioner(root);
}
}
resetPathPreviewStyle(root, { anchorCommands: cmds, viewScaleInfo, styles });
};
const createDirectorLines = (
directors: (Directioner | null)[],
opts: {
styles: MiddlewarePathEditorStyles;
}
) => {
const { styles } = opts;
const svg = `
<svg
width="100%"
height="100%"
overflow="visible"
xmlns="http://www.w3.org/2000/svg"
${ATTR_VALID_WATCH}="true"
>
${directors
.map((director) => {
if (!director) {
return '';
}
const { anchorPoint, directPoint } = director;
const x1 = anchorPoint.x;
const y1 = anchorPoint.y;
const x2 = directPoint.x;
const y2 = directPoint.y;
return `<line
x1="${x1}"
x2="${x2}"
y1="${y1}"
y2="${y2}"
stroke="${styles.directorLineColor}"
stroke-width="2"
${ATTR_VALID_WATCH}="true"
/>`;
})
.join('')}
</svg>
`;
const $lines = createHTMLElement(
'div',
{
width: '100%',
height: '100%',
className: classNameMap.directorLines,
[ATTR_HELPER_TYPE]: HELPER_DIRECTOR_LINE,
[ATTR_VALID_WATCH]: 'true',
},
[parseHTMLStr(svg)]
);
return $lines;
};
const clearDirectorLine = (root: HTMLElement) => {
const existedLines = root.querySelectorAll(`[${ATTR_HELPER_TYPE}="${HELPER_DIRECTOR_LINE}"]`);
Array.from(existedLines).forEach((line) => {
line?.remove();
});
};
const resetDirectorLine = (
root: HTMLElement,
opts: {
currentDirector: Directioner | null;
prevDirector: Directioner | null;
styles: MiddlewarePathEditorStyles;
}
) => {
const $material = root.querySelector(`[${ATTR_HELPER_TYPE}="${HELPER_ELEMENT}"]`);
const $pathPreview = root.querySelector(`[${ATTR_HELPER_TYPE}="${HELPER_PATH_PREVIEW}"]`);
if (!($material && $pathPreview)) {
return;
}
clearDirectorLine(root);
const { currentDirector, prevDirector, styles } = opts;
if (prevDirector || currentDirector) {
const $lines = createDirectorLines([prevDirector, currentDirector], { styles });
if ($material.firstElementChild) {
$material.insertBefore($lines, $pathPreview);
}
}
};
export const clearDirectioner = (root: HTMLElement | null) => {
if (!root) {
return;
}
const existedDirectors = root.querySelectorAll(`[${ATTR_HELPER_TYPE}="${HELPER_DIRECTOR}"]`);
Array.from(existedDirectors).forEach((director) => {
director?.remove();
});
clearDirectorLine(root);
};
export const resetDirectionerStyle = (
root: HTMLElement,
opts: {
selectedAnchorId: string;
currentDirector: Directioner | null;
prevDirector: Directioner | null;
styles: MiddlewarePathEditorStyles;
}
) => {
const { selectedAnchorId, prevDirector, currentDirector, styles } = opts;
const directors: Directioner[] = [];
if (prevDirector) {
directors.push(prevDirector);
}
if (currentDirector) {
directors.push(currentDirector);
}
const $directors: HTMLElement[] = Array.from(root.querySelectorAll(`[${ATTR_HELPER_TYPE}="${HELPER_DIRECTOR}"]`));
let needResetAll = false;
if (directors.length === $directors.length) {
for (let i = 0; i < $directors.length; i++) {
const $director = $directors[i];
const director = directors[i];
const info = getDirectorHandlerInfo($directors[i]);
if (info.openedAnchorId === selectedAnchorId && info.fromAnchorId === director.anchorId) {
setHTMLCSSProps(
$director,
getDirectorPosition({
x: director.directPoint.x,
y: director.directPoint.y,
size: styles.directorSize,
borderWidth: styles.directorBorderWidth,
})
);
} else {
needResetAll = true;
break;
}
}
} else {
needResetAll = true;
}
if (needResetAll) {
resetDirectioner(root, { prevDirector, currentDirector, styles });
}
};
const resetDirectioner = (
root: HTMLElement | null,
opts: {
currentDirector: Directioner | null;
prevDirector: Directioner | null;
styles: MiddlewarePathEditorStyles;
}
) => {
if (!root) {
return;
}
const $material = root.querySelector(`[${ATTR_HELPER_TYPE}="${HELPER_ELEMENT}"]`);
if (!$material) {
return;
}
clearDirectioner(root);
resetDirectorLine(root, opts);
const { currentDirector, prevDirector, styles } = opts;
if (prevDirector) {
const $director: HTMLElement = createHTMLElement('div', {
[ATTR_HELPER_TYPE]: HELPER_DIRECTOR,
[ATTR_DIRECTOR_CONTROL_TYPE]: 'curve-ctrl2',
[ATTR_DIRECTOR_FROM_AHCHOR_ID]: prevDirector.anchorId,
[ATTR_DIRECTOR_OPENED_BY_AHCHOR_ID]: prevDirector.openedByAnchorId,
// [ATTR_X]: prevDirector.directPoint.x,
// [ATTR_Y]: prevDirector.directPoint.y,
[ATTR_VALID_WATCH]: 'true',
className: classNameMap.director,
style: {
...getDirectorPosition({
x: prevDirector.directPoint.x,
y: prevDirector.directPoint.y,
size: styles.directorSize,
borderWidth: styles.directorBorderWidth,
}),
},
});
$material.appendChild($director);
}
if (currentDirector) {
const $director: HTMLElement = createHTMLElement('div', {
[ATTR_HELPER_TYPE]: HELPER_DIRECTOR,
[ATTR_DIRECTOR_CONTROL_TYPE]: 'curve-ctrl1',
[ATTR_DIRECTOR_FROM_AHCHOR_ID]: currentDirector.anchorId,
[ATTR_DIRECTOR_OPENED_BY_AHCHOR_ID]: currentDirector.openedByAnchorId,
// [ATTR_X]: currentDirector.directPoint.x,
// [ATTR_Y]: currentDirector.directPoint.y,
[ATTR_VALID_WATCH]: 'true',
className: classNameMap.director,
style: {
...getDirectorPosition({
x: currentDirector.directPoint.x,
y: currentDirector.directPoint.y,
size: styles.directorSize,
borderWidth: styles.directorBorderWidth,
}),
},
});
$material.appendChild($director);
}
};
const resetPathLine = (
root: HTMLElement,
opts: {
anchorCommands: PathAnchorCommand[];
material: StrictMaterial<'path'> | null;
viewScaleInfo: ViewScaleInfo;
styles: MiddlewarePathEditorStyles;
}
) => {
if (!root) {
return;
}
const $material = root.querySelector(`[${ATTR_HELPER_TYPE}="${HELPER_ELEMENT}"]`);
if (!$material) {
return;
}
const $pathPreview = createHTMLElement('div', {
className: classNameMap.pathLine,
[ATTR_HELPER_TYPE]: HELPER_PATH_PREVIEW,
[ATTR_VALID_WATCH]: 'true',
});
if ($material.firstElementChild) {
$material.insertBefore($pathPreview, $material.firstElementChild);
}
resetPathPreview(root, opts);
};
const resetPathPreviewStyle = (
root: HTMLElement,
opts: {
anchorCommands: PathAnchorCommand[];
// material: StrictMaterial<'path'> | null;
viewScaleInfo: ViewScaleInfo;
styles: MiddlewarePathEditorStyles;
}
) => {
const $pathPreview = root.querySelector(`[${ATTR_HELPER_TYPE}="${HELPER_PATH_PREVIEW}"]`);
if (!$pathPreview) {
return;
}
const $pathDefinition = $pathPreview.querySelector(`[${ATTR_HELPER_TYPE}="${HELPER_PATH_DEFINITION}"]`);
if ($pathDefinition) {
const { anchorCommands } = opts;
const definition = convertPathCommandsToStr(anchorCommands);
assembleHTMLElement($pathDefinition, {
d: definition,
});
} else {
resetPathPreview(root, opts);
}
};
const resetPathPreview = (
root: HTMLElement,
opts: {
viewScaleInfo: ViewScaleInfo;
anchorCommands: PathAnchorCommand[];
styles: MiddlewarePathEditorStyles;
}
) => {
const $pathPreview = root.querySelector(`[${ATTR_HELPER_TYPE}="${HELPER_PATH_PREVIEW}"]`);
if (!$pathPreview) {
return;
}
if ($pathPreview?.children) {
Array.from($pathPreview.children).forEach((child) => {
child.remove();
});
}
const { anchorCommands, styles } = opts;
const $svg = parseHTMLStr(`
<svg
width="100%"
height="100%"
overflow="visible"
fill="transparent"
${ATTR_VALID_WATCH}="true"
>
<path
${ATTR_HELPER_TYPE}="${HELPER_PATH_DEFINITION}"
d="${convertPathCommandsToStr(anchorCommands)}"
stroke="${styles.helperStrokeColor}"
stroke-width="${styles.helperStrokeWidth}"
${ATTR_VALID_WATCH}="true"
/>
</svg>
`);
assembleHTMLElement($pathPreview, {}, [$svg]);
};
export function calcPathSize(root: HTMLElement | null) {
// TODO
if (!root) {
return null;
}
// TODO
}

View file

@ -0,0 +1,166 @@
import type { ViewContext2D, StrictMaterial, ViewScaleInfo, ViewSizeInfo, Point } from '@idraw/types';
import {
convertPathCommandsToContext2DCommands,
calcViewPoint,
rotateMaterial,
calcViewMaterialSize,
} from '@idraw/util';
import { parseBezierCurveTo, parseMoveTo, parseEllipse } from './parse';
import type { CommandItem } from './types';
export function drawAncor(
ctx: ViewContext2D,
center: Point
// opts: { borderColor: string; borderWidth: number; background: string; lineDash: number[] }
) {
const { x, y } = center;
const w = 12;
const h = 12;
// const { borderColor, borderWidth, background, lineDash } = opts;
const borderColor = '#0000ff'; // TODO
const borderWidth = 2; // TODO
const background = '#ffffffaf'; // TODO
const lineDash: number[] = []; // TODO
ctx.setLineDash([]);
ctx.lineWidth = borderWidth;
ctx.strokeStyle = borderColor;
ctx.fillStyle = background;
ctx.setLineDash(lineDash);
ctx.beginPath();
ctx.moveTo(x - w / 2, y - h / 2);
ctx.lineTo(x + w / 2, y - h / 2);
ctx.lineTo(x + w / 2, y + h / 2);
ctx.lineTo(x - w / 2, y + h / 2);
ctx.lineTo(x - w / 2, y - h / 2);
ctx.closePath();
ctx.stroke();
ctx.fill('nonzero');
}
export function drawBreakpoint(
ctx: ViewContext2D,
center: Point
// opts: { borderColor: string; borderWidth: number; background: string; lineDash: number[] }
) {
// const { x, y } = center;
const w = 12;
const h = 12;
// const { borderColor, borderWidth, background, lineDash } = opts;
const borderColor = '#ff0000'; // TODO
const borderWidth = 2; // TODO
const background = '#ffffffaf'; // TODO
const lineDash: number[] = []; // TODO
ctx.setLineDash([]);
ctx.lineWidth = borderWidth;
ctx.strokeStyle = borderColor;
ctx.fillStyle = background;
ctx.setLineDash(lineDash);
ctx.beginPath();
// ctx.moveTo(x - w / 2, y - h / 2);
ctx.circle(center.x, center.y, w / 2, h / 2, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
export function drawPathAnchor(
ctx: ViewContext2D,
material: StrictMaterial<'path'> | null,
opts: {
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
}
) {
if (!(material?.type === 'path' && Array.isArray(material?.commands))) {
return;
}
const { x, y, commands } = material;
const viewElemSize = calcViewMaterialSize(material, opts);
const ctxCmds = convertPathCommandsToContext2DCommands(commands);
ctx.strokeStyle = 'blue'; // TODO
ctx.lineWidth = 1; // TODO
const scalePoint = (p: Point) => ({
x: p.x * opts.viewScaleInfo.scale,
y: p.y * opts.viewScaleInfo.scale,
});
// const start = calcViewPoint({ x, y }, opts);
const movePoint = (p: Point) => ({
x: p.x + x,
y: p.y + y,
});
let current: Point | null = null;
const cmdItems: CommandItem[] = [];
rotateMaterial(ctx, viewElemSize, () => {
ctxCmds.forEach((cmd) => {
if (cmd.name === 'moveTo') {
const p = calcViewPoint(movePoint({ x: cmd.params.x, y: cmd.params.y }), opts);
ctx.moveTo(p.x, p.y);
cmdItems.push(parseMoveTo({ ...cmd, params: { ...p } }));
current = { x: p.x, y: p.y };
} else if (cmd.name === 'bezierCurveTo') {
const cp1 = calcViewPoint(movePoint({ x: cmd.params.cp1x, y: cmd.params.cp1y }), opts);
const cp2 = calcViewPoint(movePoint({ x: cmd.params.cp2x, y: cmd.params.cp2y }), opts);
const p = calcViewPoint(movePoint({ x: cmd.params.x, y: cmd.params.y }), opts);
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y);
cmdItems.push(
parseBezierCurveTo(current as Point, {
...cmd,
params: {
cp1x: cp1.x,
cp1y: cp1.y,
cp2x: cp2.x,
cp2y: cp2.y,
x: p.x,
y: p.y,
},
})
);
current = { x: p.x, y: p.y };
} else if (cmd.name === 'ellipse') {
const center = calcViewPoint(movePoint({ x: cmd.params.centerX, y: cmd.params.centerY }), opts);
const radius = scalePoint({ x: cmd.params.radiusX, y: cmd.params.radiusY });
ctx.ellipse(
center.x,
center.y,
radius.x,
radius.y,
cmd.params.rotation,
cmd.params.startRadian,
cmd.params.endRadian,
cmd.params.anticlockwise
);
cmdItems.push(
parseEllipse(current as Point, {
...cmd,
params: {
centerX: center.x,
centerY: center.y,
radiusX: radius.x,
radiusY: radius.y,
rotation: cmd.params.rotation,
startRadian: cmd.params.startRadian,
endRadian: cmd.params.endRadian,
anticlockwise: cmd.params.anticlockwise,
},
})
);
} else if (cmd.name === 'beginPath') {
ctx.beginPath();
} else if (cmd.name === 'closePath') {
ctx.closePath();
}
});
ctx.stroke();
cmdItems.forEach((item) => {
drawBreakpoint(ctx, item.end);
});
});
}

View file

@ -0,0 +1,406 @@
import type {
Middleware,
CoreEventMap,
StrictMaterial,
MaterialSize,
Point,
PathAnchorCommand,
PathCommand,
MiddlewarePathEditorConfig,
} from '@idraw/types';
import {
createId,
calcPointMoveMaterialInGroup,
getMaterialSize,
updateMaterialInList,
moveInAnchorCommands,
moveCurveCtrlInAnchorCommands,
addClassName,
removeClassName,
refinePathMaterial,
getMaterialAndGroupQueueFromList,
} from '@idraw/util';
import { coreEventKeys } from '../../static';
import {
ATTR_HELPER_TYPE,
HELPER_ANCHOR,
HELPER_DIRECTOR,
classNameMap,
defaultStyles,
getRootClassName,
getMiddlewarePathEditorStyles,
} from './static';
import type { PathEditorSharedStorage, DirectorInfo, AnchorInfo } from './types';
import { resetRoot, resetAnchorStyle, getAnchorHandlerInfo, getDirectorHandlerInfo } from './dom';
import { calcPointInCanvas, getPathAnchorCommands } from './util';
import { calcPathSize, initRoot, initStyles, destroyStyles } from './dom';
export { getMiddlewarePathEditorStyles };
export const MiddlewarePathEditor: Middleware<PathEditorSharedStorage, CoreEventMap, MiddlewarePathEditorConfig> = (
opts,
config
) => {
const { viewer, eventHub, sharer, calculator } = opts;
const innerConfig = {
...defaultStyles,
...config,
};
const { afterClickAway } = innerConfig;
const rootClassName = getRootClassName();
const styles = getMiddlewarePathEditorStyles(innerConfig);
const container = opts.container;
const id = `idraw-middleware-path-editor-${createId()}`;
let root: HTMLDivElement | null = null;
let showEditor = false;
let hasInitedEvent = false;
let handlerStatus: 'dragging-anchor' | 'dragging-director' | null = null;
let selectedPathMaterial: StrictMaterial<'path'> | null = null;
let selectedGroupQueue: StrictMaterial<'group'>[] | null = null;
let prevPoint: Point | null = null;
let moveOriginalStartMaterialSize: MaterialSize | null = null;
let selectedAnchorHandler: HTMLElement | null = null;
let selectedAnchorHandlerInfo: AnchorInfo | null = null;
let selectedPathAnchorCommands: PathAnchorCommand[] | null = null;
let selectedDirectorHandler: HTMLElement | null = null;
let selectedDirectorHandlerInfo: DirectorInfo | null = null;
const clearData = () => {
selectedPathMaterial = null;
clearMoveData();
clearSelectedAnchorData();
};
const clearSelectedAnchorData = () => {
clearSelectedDirectorData();
selectedAnchorHandler = null;
selectedAnchorHandlerInfo = null;
selectedPathAnchorCommands = null;
};
const clearSelectedDirectorData = () => {
selectedDirectorHandler = null;
selectedDirectorHandlerInfo = null;
};
const clearSelectedStatus = () => {
if (!root) {
return;
}
const $selectedHandlers: HTMLElement[] = Array.from(
root.getElementsByClassName(classNameMap.selected)
) as HTMLElement[];
$selectedHandlers.forEach(($handler) => {
removeClassName($handler, [classNameMap.selected]);
});
};
const clearMoveData = () => {
handlerStatus = null;
prevPoint = null;
moveOriginalStartMaterialSize = null;
};
const mouseDownEvent = (e: MouseEvent) => {
const handler = e.target as HTMLElement;
const helperType = handler?.getAttribute(ATTR_HELPER_TYPE);
if (helperType === HELPER_ANCHOR && selectedPathMaterial) {
e.stopPropagation();
e.preventDefault();
clearSelectedAnchorData();
moveOriginalStartMaterialSize = getMaterialSize(selectedPathMaterial);
const start = calcPointInCanvas(e, root as HTMLElement);
prevPoint = start;
handlerStatus = 'dragging-anchor';
selectedAnchorHandler = handler;
selectedAnchorHandlerInfo = getAnchorHandlerInfo(handler);
selectedPathAnchorCommands = getPathAnchorCommands(selectedPathMaterial, { calculator });
window.addEventListener('mousemove', mouseMoveEvent);
addClassName(selectedAnchorHandler, [classNameMap.selected]);
viewer.drawFrame();
} else if (helperType === HELPER_DIRECTOR && selectedPathMaterial) {
e.stopPropagation();
e.preventDefault();
clearSelectedDirectorData();
moveOriginalStartMaterialSize = getMaterialSize(selectedPathMaterial);
const start = calcPointInCanvas(e, root as HTMLElement);
prevPoint = start;
handlerStatus = 'dragging-director';
selectedDirectorHandler = handler;
selectedDirectorHandlerInfo = getDirectorHandlerInfo(handler);
selectedPathAnchorCommands = getPathAnchorCommands(selectedPathMaterial, { calculator });
window.addEventListener('mousemove', mouseMoveEvent);
addClassName(selectedDirectorHandler, [classNameMap.selected]);
viewer.drawFrame();
} else {
clearPathEditCallback();
afterClickAway?.();
}
};
const mouseMoveEvent = (e: MouseEvent) => {
if (prevPoint && selectedPathMaterial && moveOriginalStartMaterialSize && selectedPathAnchorCommands) {
const current = calcPointInCanvas(e, root as HTMLElement);
const queue: StrictMaterial<'group'>[] = [
...(selectedGroupQueue || []),
{
...moveOriginalStartMaterialSize,
type: 'group',
id: selectedPathMaterial.id,
angle: selectedPathMaterial.angle,
children: [],
},
];
const { moveX, moveY } = calcPointMoveMaterialInGroup(prevPoint, current, queue);
const scale = sharer.getActiveStorage('scale') || 1;
const totalMoveX = calculator.toGridNum(moveX / scale);
const totalMoveY = calculator.toGridNum(moveY / scale);
const acmds = [...selectedPathAnchorCommands];
if (selectedAnchorHandler && selectedAnchorHandlerInfo && handlerStatus === 'dragging-anchor') {
const newAcmds = moveInAnchorCommands(acmds, {
type: 'start',
index: selectedAnchorHandlerInfo.index,
moveX: totalMoveX,
moveY: totalMoveY,
});
const data = sharer.getActiveStorage('data');
const newCommands: PathCommand[] = newAcmds.map(({ id, type, params }) => ({ id, type, params }));
updateMaterialInList(
selectedPathMaterial.id,
{
commands: newCommands,
},
data.materials
);
// calculator
selectedPathMaterial.commands = newCommands;
calculator.modifyVirtualAttributes(selectedPathMaterial, {
viewScaleInfo: sharer.getActiveViewScaleInfo(),
viewSizeInfo: sharer.getActiveViewSizeInfo(),
groupQueue: selectedGroupQueue || [],
});
viewer.drawFrame();
} else if (selectedDirectorHandler && selectedDirectorHandlerInfo && handlerStatus === 'dragging-director') {
const { type, fromAnchorId } = selectedDirectorHandlerInfo;
const updatedCmdIndex = acmds.findIndex((item) => item.id === fromAnchorId);
const newAcmds = moveCurveCtrlInAnchorCommands(acmds, {
type,
index: updatedCmdIndex,
moveX: totalMoveX,
moveY: totalMoveY,
});
const data = sharer.getActiveStorage('data') || { materials: [] };
const newCommands: PathCommand[] = newAcmds.map(({ id, type, params }) => ({ id, type, params }));
updateMaterialInList(
selectedPathMaterial.id,
{
commands: newCommands,
},
data.materials
);
// calculator
selectedPathMaterial.commands = newCommands;
calculator.modifyVirtualAttributes(selectedPathMaterial, {
viewScaleInfo: sharer.getActiveViewScaleInfo(),
viewSizeInfo: sharer.getActiveViewSizeInfo(),
groupQueue: selectedGroupQueue || [],
});
viewer.drawFrame();
}
}
};
const resetPathSize = () => {
// TODO
calcPathSize(root);
};
const refineAction = () => {
if (!selectedPathMaterial) {
return;
}
selectedPathMaterial = refinePathMaterial(selectedPathMaterial);
const data = sharer.getActiveStorage('data') || { materials: [] };
updateMaterialInList(
selectedPathMaterial.id,
{
x: selectedPathMaterial.x,
y: selectedPathMaterial.y,
width: selectedPathMaterial.width,
height: selectedPathMaterial.height,
commands: selectedPathMaterial.commands,
},
data.materials
);
calculator.modifyVirtualAttributes(selectedPathMaterial, {
viewScaleInfo: sharer.getActiveViewScaleInfo(),
viewSizeInfo: sharer.getActiveViewSizeInfo(),
groupQueue: selectedGroupQueue || [],
});
viewer.drawFrame();
};
const mouseUpEvent = () => {
window.removeEventListener('mousemove', mouseMoveEvent);
refineAction();
clearSelectedStatus();
clearMoveData();
resetPathSize();
};
const mouseLeaveEvent = () => {
window.removeEventListener('mousemove', mouseMoveEvent);
refineAction();
clearSelectedStatus();
clearMoveData();
resetPathSize();
};
const onEvents = () => {
if (hasInitedEvent) {
return;
}
root?.addEventListener('mousedown', mouseDownEvent);
window.addEventListener('mouseup', mouseUpEvent);
window.addEventListener('mouseleave', mouseLeaveEvent);
hasInitedEvent = true;
};
const offEvents = () => {
root?.removeEventListener('mousedown', mouseDownEvent);
window.removeEventListener('mouseup', mouseUpEvent);
window.removeEventListener('mouseleave', mouseLeaveEvent);
hasInitedEvent = false;
};
const init = () => {
if (!container) {
return;
}
root = initRoot(container, { id, rootClassName }) as HTMLDivElement;
if (!container.contains(root)) {
container.appendChild(root);
}
showEditor = true;
};
const destroy = () => {
offEvents();
root?.remove();
root = null;
showEditor = false;
};
const pathEditCallback = (e: CoreEventMap[typeof coreEventKeys.PATH_EDIT]) => {
init();
const { id } = e;
if (typeof id === 'string' && id) {
const data = sharer.getActiveStorage('data');
const { groupQueue, material } = getMaterialAndGroupQueueFromList(id, data.materials);
if (material?.type === 'path') {
selectedPathMaterial = material as StrictMaterial<'path'>;
selectedGroupQueue = [...groupQueue];
resetRoot(root, {
material: material as StrictMaterial<'path'> | null,
groupQueue,
calculator,
viewScaleInfo: sharer.getActiveViewScaleInfo(),
styles,
});
onEvents();
const map = sharer.getActiveOverrideMaterialMap() || {};
map[material.id] = {
operations: { renderPathTrace: true },
};
sharer.setActiveOverrideMaterialMap(map);
viewer.drawFrame();
}
}
};
const clearPathEditCallback = () => {
const map = sharer.getActiveOverrideMaterialMap() || {};
delete map[(selectedPathMaterial as StrictMaterial<'path'>)?.id];
sharer.setActiveOverrideMaterialMap(map);
clearData();
destroy();
viewer.drawFrame();
};
return {
name: '@middleware/pen-edit',
use() {
initStyles(rootClassName, styles);
eventHub.on(coreEventKeys.PATH_EDIT, pathEditCallback);
eventHub.on(coreEventKeys.CLEAR_PATH_EDIT, clearPathEditCallback);
},
disuse() {
destroyStyles(rootClassName);
eventHub.off(coreEventKeys.PATH_EDIT, pathEditCallback);
eventHub.off(coreEventKeys.CLEAR_PATH_EDIT, clearPathEditCallback);
},
beforeDrawFrame() {
resetAnchorStyle(root, {
selectedAnchorId: selectedAnchorHandlerInfo?.id,
material: selectedPathMaterial,
viewScaleInfo: sharer.getActiveViewScaleInfo(),
calculator,
styles,
});
},
hover() {
return !showEditor;
},
pointStart() {
return !showEditor;
},
pointMove() {
return !showEditor;
},
pointEnd() {
return !showEditor;
},
pointLeave() {
return !showEditor;
},
doubleClick() {
return !showEditor;
},
contextMenu() {
return !showEditor;
},
wheel() {
return !showEditor;
},
wheelScale() {
return !showEditor;
},
scrollX() {
return !showEditor;
},
scrollY() {
return !showEditor;
},
resize() {
return !showEditor;
},
};
};

View file

@ -0,0 +1,42 @@
import { rotatePoint } from '@idraw/util';
import type { Point, Context2DMoveToCommand, Context2DBezierCurveCommand, Context2DEllipseCommand } from '@idraw/types';
import type { CommandItem } from './types';
export function parseMoveTo(cmd: Context2DMoveToCommand): CommandItem {
const { id, name, params } = cmd;
const { x, y } = params;
const item: CommandItem = {
id,
name,
start: { x, y },
end: { x, y },
};
return item;
}
export function parseBezierCurveTo(prevPoint: Point, cmd: Context2DBezierCurveCommand) {
const { id, name, params } = cmd;
const { cp1x, cp1y, cp2x, cp2y, x, y } = params;
const item: CommandItem = {
id,
name,
start: { x: prevPoint.x, y: prevPoint.y },
end: { x, y },
ctrl1: { x: cp1x, y: cp1y },
ctrl2: { x: cp2x, y: cp2y },
};
return item;
}
export function parseEllipse(prevPoint: Point, cmd: Context2DEllipseCommand) {
const { id, name, params } = cmd;
const { centerX, centerY, endRadian, startRadian } = params;
const item: CommandItem = {
id,
name,
start: { x: prevPoint.x, y: prevPoint.y },
end: rotatePoint({ x: centerX, y: centerY }, prevPoint, endRadian - startRadian),
center: { x: centerX, y: centerY },
};
return item;
}

View file

@ -0,0 +1,95 @@
import { createId, getMiddlewareValidStyles } from '@idraw/util';
import type { MiddlewarePathEditorStyles, MiddlewarePathEditorConfig } from '@idraw/types';
export const key = 'PATH-EDITOR';
const prefix = `idraw-middleware-path-creator`;
export const getRootClassName = () => `${prefix}-${createId()}`;
export const classNameMap = {
hide: `${prefix}-hide`,
anchor: `${prefix}-anchor`,
director: `${prefix}-director`,
directorLines: `${prefix}-director-lines`,
pathLine: `${prefix}-path-line`,
selected: `${prefix}-selected`,
};
export const ATTR_UUID = `data-uuid`;
export const ATTR_X = `data-x`;
export const ATTR_Y = `data-y`;
export const ATTR_W = `data-w`;
export const ATTR_H = `data-h`;
export const ATTR_ANGLE = `data-angle`;
export const ATTR_TYPE = `data-type`;
export const ATTR_HELPER_TYPE = `data-helper-type`;
export const ATTR_AHCHOR_CMD_TYPE = `data-anchor-cmd-type`;
export const ATTR_AHCHOR_INDEX = `data-anchor-index`;
export const ATTR_AHCHOR_ID = `data-anchor-id`;
export const ATTR_DIRECTOR_FROM_AHCHOR_ID = `data-director-from-anchor-id`;
export const ATTR_DIRECTOR_CONTROL_TYPE = `data-director-control-type`;
export const ATTR_DIRECTOR_OPENED_BY_AHCHOR_ID = `data-director-opened-by-anchor-id`;
export const HELPER_GROUP = 'group';
export const HELPER_ELEMENT = 'material';
export const HELPER_ANCHOR = 'anchor';
export const HELPER_DIRECTOR = 'director';
export const HELPER_DIRECTOR_LINE = 'director-line';
export const HELPER_PATH_PREVIEW = 'path-preview';
export const HELPER_PATH_DEFINITION = 'path-definition';
export const defaultStyles: MiddlewarePathEditorStyles = {
zIndex: 2,
anchorSize: 8,
anchorSelectedSize: 12,
anchorBorderWidth: 2,
anchorBorderColor: '#0c8ce9',
anchorBackground: '#ffffff',
anchorHoverBorderColor: '#1671b8',
anchorHoverBackground: '#cfe4f4',
anchorActiveBorderColor: '#0d548c',
anchorActiveBackground: '#88c0ec',
directorSize: 10,
directorBorderWidth: 2,
directorBorderColor: '#7315d1ff',
directorBackground: '#ffffff',
directorHoverBorderColor: '#4716b8ff',
directorHoverBackground: '#ebcff4ff',
directorActiveBorderColor: '#510d8cff',
directorActiveBackground: '#c988ecff',
directorLineColor: '#7315d1ff',
helperStrokeColor: '#0c8ce9',
helperStrokeWidth: 1,
};
export function getMiddlewarePathEditorStyles<C = MiddlewarePathEditorConfig, S = MiddlewarePathEditorStyles>(
config: C
): S {
const styles: S = getMiddlewareValidStyles<C, S>(config, [
'zIndex',
'anchorSize',
'anchorSelectedSize',
'anchorBorderWidth',
'anchorBorderColor',
'anchorBackground',
'anchorHoverBorderColor',
'anchorHoverBackground',
'anchorActiveBorderColor',
'anchorActiveBackground',
'directorSize',
'directorBorderWidth',
'directorBorderColor',
'directorBackground',
'directorHoverBorderColor',
'directorHoverBackground',
'directorActiveBorderColor',
'directorActiveBackground',
'directorLineColor',
'helperStrokeColor',
'helperStrokeWidth',
]);
return styles;
}

View file

@ -0,0 +1,35 @@
import type { Point, Context2DCommand } from '@idraw/types';
// import { keySelectedMaterialList } from '../selector/static';
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export type PathEditorSharedStorage = {
// [keySelectedMaterialList]: null | Material[];
};
export type CommandItem = {
id: string;
name: Context2DCommand['name'];
start: Point;
end: Point;
ctrl1?: Point;
ctrl2?: Point;
center?: Point;
};
export type Directioner = {
anchorId: string;
openedByAnchorId: string;
anchorPoint: Point;
directPoint: Point;
};
export type AnchorInfo = {
id: string;
index: number;
};
export type DirectorInfo = {
type: 'curve-ctrl1' | 'curve-ctrl2';
fromAnchorId: string;
openedAnchorId: string;
};

View file

@ -0,0 +1,34 @@
import type { StrictMaterial, Point, PathAnchorCommand, ViewCalculator, VirtualPathAttributes } from '@idraw/types';
export function calcPointInCanvas(e: MouseEvent, container: HTMLElement): Point {
const { pageX, pageY } = e;
const rect = container.getBoundingClientRect();
const scrollLeft = window.scrollX || document.documentElement.scrollLeft;
const scrollTop = window.scrollY || document.documentElement.scrollTop;
const containerPageX = rect.left + scrollLeft;
const containerPageY = rect.top + scrollTop;
const containerX = pageX - containerPageX;
const containerY = pageY - containerPageY;
return {
x: containerX,
y: containerY,
};
}
export function getPathAnchorCommands(
material: StrictMaterial<'path'> | null,
opts: {
calculator: ViewCalculator;
}
): PathAnchorCommand[] {
const { calculator } = opts;
const { id } = material as StrictMaterial<'path'>;
const flatItem: VirtualPathAttributes = calculator.getVirtualItem(id) as VirtualPathAttributes;
const cmds = [...(flatItem.anchorCommands || [])];
return cmds;
}

View file

@ -1,7 +1,7 @@
import type { Middleware, CoreEventMap } from '@idraw/types';
import type { DeepPointerSharedStorage } from './types';
import { keySelectedElementList } from '../selector';
import { coreEventKeys } from '../../config';
import { keySelectedMaterialList } from '../selector';
import { coreEventKeys } from '../../static';
export const MiddlewarePointer: Middleware<DeepPointerSharedStorage, CoreEventMap> = (opts) => {
const { boardContent, eventHub, sharer } = opts;
@ -39,11 +39,11 @@ export const MiddlewarePointer: Middleware<DeepPointerSharedStorage, CoreEventMa
contextMenuPointer.style.left = `${left + point.x}px`;
contextMenuPointer.style.top = `${top + point.y}px`;
const selectedElements = sharer.getSharedStorage(keySelectedElementList);
const selectedMaterials = sharer.getSharedStorage(keySelectedMaterialList);
eventHub.trigger(coreEventKeys.CONTEXT_MENU, {
pointerContainer: contextMenuPointer,
selectedElements: selectedElements || []
selectedMaterials: selectedMaterials || [],
});
}
},
};
};

View file

@ -1,4 +1,4 @@
import { keySelectedElementList } from '../selector';
import { keySelectedMaterialList } from '../selector';
import type { DeepSelectorSharedStorage } from '../selector';
export type DeepPointerSharedStorage = Pick<DeepSelectorSharedStorage, typeof keySelectedElementList>;
export type DeepPointerSharedStorage = Pick<DeepSelectorSharedStorage, typeof keySelectedMaterialList>;

View file

@ -1,25 +0,0 @@
import type { MiddlewareRulerStyle } from '@idraw/types';
export const rulerSize = 16;
export const fontSize = 10;
export const fontWeight = 100;
export const lineSize = 1;
export const fontFamily = 'monospace';
const background = '#FFFFFFA8';
const borderColor = '#00000080';
const scaleColor = '#000000';
const textColor = '#00000080';
const gridColor = '#AAAAAA20';
const gridPrimaryColor = '#AAAAAA40';
const selectedAreaColor = '#19609780';
export const defaultStyle: MiddlewareRulerStyle = {
background,
borderColor,
scaleColor,
textColor,
gridColor,
gridPrimaryColor,
selectedAreaColor
};

View file

@ -7,11 +7,13 @@ import {
calcXRulerScaleList,
calcYRulerScaleList,
drawGrid,
drawScrollerSelectedArea
drawScrollerSelectedArea,
} from './util';
import type { DeepRulerSharedStorage } from './types';
import { defaultStyle } from './config';
import { coreEventKeys } from '../../config';
import { defaultStyle, getMiddlewareRulerStyles } from './static';
import { coreEventKeys } from '../../static';
export { getMiddlewareRulerStyles };
export const MiddlewareRuler: Middleware<DeepRulerSharedStorage, CoreEventMap, MiddlewareRulerConfig> = (
opts,
@ -21,9 +23,11 @@ export const MiddlewareRuler: Middleware<DeepRulerSharedStorage, CoreEventMap, M
const { overlayContext, underlayContext } = boardContent;
let innerConfig = {
...defaultStyle,
...config
...config,
};
let styles = getMiddlewareRulerStyles(innerConfig);
let show: boolean = true;
let showGrid: boolean = true;
@ -53,34 +57,23 @@ export const MiddlewareRuler: Middleware<DeepRulerSharedStorage, CoreEventMap, M
resetConfig(config) {
innerConfig = { ...innerConfig, ...config };
styles = getMiddlewareRulerStyles(innerConfig);
},
beforeDrawFrame: ({ snapshot }) => {
const { background, borderColor, scaleColor, textColor, gridColor, gridPrimaryColor, selectedAreaColor } =
innerConfig;
const style = {
background,
borderColor,
scaleColor,
textColor,
gridColor,
gridPrimaryColor,
selectedAreaColor
};
if (show === true) {
const viewScaleInfo = getViewScaleInfoFromSnapshot(snapshot);
const viewSizeInfo = getViewSizeInfoFromSnapshot(snapshot);
drawRulerBackground(overlayContext, { viewScaleInfo, viewSizeInfo, style });
drawRulerBackground(overlayContext, { viewScaleInfo, viewSizeInfo, styles });
drawScrollerSelectedArea(overlayContext, { snapshot, calculator, style });
drawScrollerSelectedArea(overlayContext, { snapshot, calculator, styles });
const { list: xList, rulerUnit } = calcXRulerScaleList({ viewScaleInfo, viewSizeInfo });
drawXRuler(overlayContext, { scaleList: xList, style });
drawXRuler(overlayContext, { scaleList: xList, styles });
const { list: yList } = calcYRulerScaleList({ viewScaleInfo, viewSizeInfo });
drawYRuler(overlayContext, { scaleList: yList, style });
drawYRuler(overlayContext, { scaleList: yList, styles });
if (showGrid === true) {
const ctx = rulerUnit === 1 ? overlayContext : underlayContext;
@ -89,10 +82,10 @@ export const MiddlewareRuler: Middleware<DeepRulerSharedStorage, CoreEventMap, M
yList,
viewScaleInfo,
viewSizeInfo,
style
styles,
});
}
}
}
},
};
};

View file

@ -0,0 +1,39 @@
import type { MiddlewareRulerConfig, MiddlewareRulerStyles } from '@idraw/types';
import { getMiddlewareValidStyles } from '@idraw/util';
export const rulerSize = 16;
export const fontSize = 10;
export const fontWeight = 100;
export const lineSize = 1;
export const fontFamily = 'monospace';
const background = '#FFFFFFA8';
const stroke = '#00000080';
const scaleColor = '#000000';
const textColor = '#00000080';
const gridColor = '#AAAAAA20';
const gridPrimaryColor = '#AAAAAA40';
const selectedAreaColor = '#19609780';
export const defaultStyle: MiddlewareRulerStyles = {
background,
stroke,
scaleColor,
textColor,
gridColor,
gridPrimaryColor,
selectedAreaColor,
};
export function getMiddlewareRulerStyles<C = MiddlewareRulerConfig, S = MiddlewareRulerStyles>(config: C): S {
const styles: S = getMiddlewareValidStyles<C, S>(config, [
'background',
'stroke',
'scaleColor',
'textColor',
'gridColor',
'gridPrimaryColor',
'selectedAreaColor',
]);
return styles;
}

View file

@ -1,4 +1,7 @@
import { keySelectedElementList, keyActionType } from '../selector';
import { keySelectedMaterialList, keyActionType } from '../selector';
import type { DeepSelectorSharedStorage } from '../selector';
export type DeepRulerSharedStorage = Pick<DeepSelectorSharedStorage, typeof keySelectedElementList | typeof keyActionType>;
export type DeepRulerSharedStorage = Pick<
DeepSelectorSharedStorage,
typeof keySelectedMaterialList | typeof keyActionType
>;

View file

@ -1,17 +1,17 @@
import type {
Element,
Material,
ViewScaleInfo,
ViewSizeInfo,
ViewContext2D,
BoardViewerFrameSnapshot,
ViewRectInfo,
BoundingInfo,
ViewCalculator,
MiddlewareRulerStyle
MiddlewareRulerStyles,
} from '@idraw/types';
import { formatNumber, rotateByCenter, getViewScaleInfoFromSnapshot, getViewSizeInfoFromSnapshot } from '@idraw/util';
import type { DeepRulerSharedStorage } from './types';
import { keySelectedElementList, keyActionType } from '../selector';
import { rulerSize, fontSize, fontWeight, lineSize, fontFamily } from './config';
import { keySelectedMaterialList, keyActionType } from '../selector';
import { rulerSize, fontSize, fontWeight, lineSize, fontFamily } from './static';
// const rulerUnit = 10;
// const rulerKeyUnit = 100;
@ -81,7 +81,7 @@ function calcRulerScaleList(opts: { axis: 'X' | 'Y'; scale: number; viewLength:
position,
showNum: num % rulerKeyUnit === 0,
isKeyNum: num % rulerKeyUnit === 0,
isSubKeyNum: num % rulerSubKeyUnit === 0
isSubKeyNum: num % rulerSubKeyUnit === 0,
};
list.push(rulerScale);
index++;
@ -101,7 +101,7 @@ export function calcXRulerScaleList(opts: { viewScaleInfo: ViewScaleInfo; viewSi
axis: 'X',
scale,
viewLength: width,
viewOffset: offsetLeft
viewOffset: offsetLeft,
});
}
@ -116,7 +116,7 @@ export function calcYRulerScaleList(opts: { viewScaleInfo: ViewScaleInfo; viewSi
axis: 'Y',
scale,
viewLength: height,
viewOffset: offsetTop
viewOffset: offsetTop,
});
}
@ -124,11 +124,11 @@ export function drawXRuler(
ctx: ViewContext2D,
opts: {
scaleList: RulerScale[];
style: MiddlewareRulerStyle;
styles: MiddlewareRulerStyles;
}
) {
const { scaleList, style } = opts;
const { scaleColor, textColor } = style;
const { scaleList, styles } = opts;
const { scaleColor, textColor } = styles;
const scaleDrawStart = rulerSize;
const scaleDrawEnd = (rulerSize * 4) / 5;
const subKeyScaleDrawEnd = (rulerSize * 2) / 5;
@ -153,7 +153,7 @@ export function drawXRuler(
ctx.$setFont({
fontWeight,
fontSize,
fontFamily
fontFamily,
});
ctx.fillText(`${item.num}`, item.position + fontStart, fontStart);
}
@ -164,11 +164,11 @@ export function drawYRuler(
ctx: ViewContext2D,
opts: {
scaleList: RulerScale[];
style: MiddlewareRulerStyle;
styles: MiddlewareRulerStyles;
}
) {
const { scaleList, style } = opts;
const { scaleColor, textColor } = style;
const { scaleList, styles } = opts;
const { scaleColor, textColor } = styles;
const scaleDrawStart = rulerSize;
const scaleDrawEnd = (rulerSize * 4) / 5;
const subKeyScaleDrawEnd = (rulerSize * 2) / 5;
@ -197,7 +197,7 @@ export function drawYRuler(
ctx.$setFont({
fontWeight,
fontSize,
fontFamily
fontFamily,
});
ctx.fillText(numText, fontStart + fontSize, item.position + fontStart);
});
@ -210,13 +210,13 @@ export function drawRulerBackground(
opts: {
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
style: MiddlewareRulerStyle;
styles: MiddlewareRulerStyles;
}
) {
const { viewSizeInfo, style } = opts;
const { viewSizeInfo, styles } = opts;
const { width, height } = viewSizeInfo;
const { background, borderColor } = style;
const { background, stroke } = styles;
ctx.beginPath();
// const basePosition = 0;
@ -234,7 +234,7 @@ export function drawRulerBackground(
ctx.fill('nonzero');
ctx.lineWidth = lineSize;
ctx.setLineDash([]);
ctx.strokeStyle = borderColor;
ctx.strokeStyle = stroke;
ctx.stroke();
}
@ -245,12 +245,12 @@ export function drawGrid(
yList: RulerScale[];
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
style: MiddlewareRulerStyle;
styles: MiddlewareRulerStyles;
}
) {
const { xList, yList, viewSizeInfo, style } = opts;
const { xList, yList, viewSizeInfo, styles } = opts;
const { width, height } = viewSizeInfo;
const { gridColor, gridPrimaryColor } = style;
const { gridColor, gridPrimaryColor } = styles;
for (let i = 0; i < xList.length; i++) {
const item = xList[i];
ctx.beginPath();
@ -289,41 +289,41 @@ export function drawScrollerSelectedArea(
opts: {
snapshot: BoardViewerFrameSnapshot<DeepRulerSharedStorage>;
calculator: ViewCalculator;
style: MiddlewareRulerStyle;
styles: MiddlewareRulerStyles;
}
) {
const { snapshot, calculator, style } = opts;
const { snapshot, calculator, styles } = opts;
const { sharedStore } = snapshot;
const { selectedAreaColor } = style;
const selectedElementList = sharedStore[keySelectedElementList];
const { selectedAreaColor } = styles;
const selectedMaterialList = sharedStore[keySelectedMaterialList];
const actionType = sharedStore[keyActionType];
if (
['select', 'drag', 'drag-list', 'drag-list-end'].includes(actionType as string) &&
selectedElementList.length > 0
selectedMaterialList.length > 0
) {
const viewScaleInfo = getViewScaleInfoFromSnapshot(snapshot);
const viewSizeInfo = getViewSizeInfoFromSnapshot(snapshot);
const rangeRectInfoList: ViewRectInfo[] = [];
const rangeBoundingInfoList: BoundingInfo[] = [];
const xAreaStartList: number[] = [];
const xAreaEndList: number[] = [];
const yAreaStartList: number[] = [];
const yAreaEndList: number[] = [];
selectedElementList.forEach((elem: Element) => {
const rectInfo = calculator.calcViewRectInfoFromRange(elem.uuid, {
selectedMaterialList.forEach((mtrl: Material) => {
const boundingBox = calculator.calcViewBoundingInfoFromRange(mtrl.id, {
viewScaleInfo,
viewSizeInfo
viewSizeInfo,
});
if (rectInfo) {
rangeRectInfoList.push(rectInfo);
xAreaStartList.push(rectInfo.left.x);
xAreaEndList.push(rectInfo.right.x);
yAreaStartList.push(rectInfo.top.y);
yAreaEndList.push(rectInfo.bottom.y);
if (boundingBox) {
rangeBoundingInfoList.push(boundingBox);
xAreaStartList.push(boundingBox.left.x);
xAreaEndList.push(boundingBox.right.x);
yAreaStartList.push(boundingBox.top.y);
yAreaEndList.push(boundingBox.bottom.y);
}
});
if (!(rangeRectInfoList.length > 0)) {
if (!(rangeBoundingInfoList.length > 0)) {
return;
}

View file

@ -1,6 +1,6 @@
import type { Middleware, CoreEventMap } from '@idraw/types';
import { formatNumber } from '@idraw/util';
import { coreEventKeys } from '../../config';
import { coreEventKeys } from '../../static';
export const MiddlewareScaler: Middleware<Record<string, any>, CoreEventMap> = (opts) => {
const { viewer, sharer, eventHub } = opts;
@ -27,6 +27,6 @@ export const MiddlewareScaler: Middleware<Record<string, any>, CoreEventMap> = (
viewer.drawFrame();
const scaleNum = formatNumber(scale);
eventHub.trigger(coreEventKeys.SCALE, { scale: scaleNum });
}
},
};
};

View file

@ -1,19 +0,0 @@
import type { MiddlewareScrollerStyle } from '@idraw/types';
export const key = 'SCROLL';
export const keyXThumbRect = Symbol(`${key}_xThumbRect`);
export const keyYThumbRect = Symbol(`${key}_yThumbRect`);
export const keyHoverXThumbRect = Symbol(`${key}_hoverXThumbRect`);
export const keyHoverYThumbRect = Symbol(`${key}_hoverYThumbRect`);
export const keyPrevPoint = Symbol(`${key}_prevPoint`);
export const keyActivePoint = Symbol(`${key}_activePoint`);
export const keyActiveThumbType = Symbol(`${key}_activeThumbType`);
export const defaultStyle: MiddlewareScrollerStyle = {
thumbBackground: '#0000003A',
thumbBorderColor: '#0000008A',
hoverThumbBackground: '#0000005F',
hoverThumbBorderColor: '#000000EE',
activeThumbBackground: '#0000005E',
activeThumbBorderColor: '#000000F0'
};

View file

@ -0,0 +1,73 @@
import { ATTR_VALID_WATCH, createHTMLElement, setHTMLCSSProps } from '@idraw/util';
import { classNameMap, ATTR_THUMB_TYPE, THUMB_X, THUMB_Y } from './static';
import type { ScrollbarStyles } from './types';
export function initRoot(opts: { rootClassName: string; $container: HTMLElement }) {
const { rootClassName, $container } = opts;
const create = createHTMLElement;
const $horizontal = create(
'div',
{
className: `${rootClassName} ${classNameMap.horizontal}`,
[ATTR_VALID_WATCH]: 'true',
},
[create('div', { className: classNameMap.thumb, [ATTR_VALID_WATCH]: 'true', [ATTR_THUMB_TYPE]: THUMB_X })]
);
const $vertical = create(
'div',
{
className: `${rootClassName} ${classNameMap.vertical}`,
[ATTR_VALID_WATCH]: 'true',
},
[create('div', { className: classNameMap.thumb, [ATTR_VALID_WATCH]: 'true', [ATTR_THUMB_TYPE]: THUMB_Y })]
);
$container.appendChild($horizontal);
$container.appendChild($vertical);
return {
$horizontal,
$vertical,
};
}
export function isInScrollbar(e: Event) {
const $target = e.target as HTMLElement;
if (
$target?.classList?.contains(classNameMap.thumb) ||
$target?.classList?.contains(classNameMap.horizontal) ||
$target?.classList?.contains(classNameMap.vertical)
) {
return true;
}
return false;
}
export function updateScrollbarStyles(
opts: ScrollbarStyles & {
$horizontal: HTMLElement | null;
$vertical: HTMLElement | null;
}
) {
const { xThumbStyle, yThumbStyle, $horizontal, $vertical } = opts;
if ($horizontal && xThumbStyle) {
const $thumb = $horizontal.getElementsByClassName(classNameMap.thumb)[0] as HTMLElement;
if ($thumb) {
setHTMLCSSProps($thumb, xThumbStyle);
}
}
if ($vertical && yThumbStyle) {
const $thumb = $vertical.getElementsByClassName(classNameMap.thumb)[0] as HTMLElement;
if ($thumb) {
setHTMLCSSProps($thumb, yThumbStyle);
}
}
}
export function getThumbType(e: Event) {
const $target = e?.target as HTMLElement;
if ($target?.classList?.contains(classNameMap.thumb) && $target.hasAttribute(ATTR_THUMB_TYPE)) {
return $target.getAttribute(ATTR_THUMB_TYPE) as null | 'X' | 'Y';
}
return null;
}

View file

@ -3,51 +3,118 @@ import type {
Middleware,
PointWatcherEvent,
BoardWatherWheelEvent,
MiddlewareScrollerConfig
MiddlewareScrollerConfig,
} from '@idraw/types';
import { drawScroller, isPointInScrollThumb } from './util';
// import type { ScrollbarThumbType } from './util';
import { coreEventKeys } from '../../static';
import {
keyXThumbRect,
keyYThumbRect,
keyXThumbStyle,
keyYThumbStyle,
keyPrevPoint,
keyActivePoint,
keyActiveThumbType,
keyHoverXThumbRect,
keyHoverYThumbRect,
defaultStyle
} from './config';
defaultStyles,
getRootClassName,
scrollbarTrackSize,
scrollbarThumbLength,
} from './static';
import type { DeepScrollerSharedStorage } from './types';
import { coreEventKeys } from '../../config';
import { initStyles, destroyStyles, getMiddlewareScrollerStyles } from './styles';
import { initRoot, isInScrollbar, updateScrollbarStyles, getThumbType } from './dom';
import { calcScrollbarStyles } from './util';
export { getMiddlewareScrollerStyles };
export const MiddlewareScroller: Middleware<DeepScrollerSharedStorage, any, MiddlewareScrollerConfig> = (
opts,
config
) => {
const { viewer, boardContent, sharer, eventHub } = opts;
const { overlayContext } = boardContent;
sharer.setSharedStorage(keyXThumbRect, null); // null | ElementSize
sharer.setSharedStorage(keyYThumbRect, null); // null | ElementSize
const { viewer, sharer, eventHub } = opts;
let isBusy: boolean = false;
let innerConfig = {
...defaultStyle,
...config
...defaultStyles,
...config,
};
// viewer.drawFrame();
const styles = getMiddlewareScrollerStyles(innerConfig);
const rootClassName = getRootClassName();
let $horizontal: HTMLDivElement | null = null;
let $vertical: HTMLDivElement | null = null;
const clear = () => {
sharer.setSharedStorage(keyPrevPoint, null); // null | Point;
sharer.setSharedStorage(keyActivePoint, null); // null | Point;
sharer.setSharedStorage(keyActiveThumbType, null); // null | 'X' | 'Y'
sharer.setSharedStorage(keyHoverXThumbRect, null); // null | boolean
sharer.setSharedStorage(keyHoverYThumbRect, null); // null | boolean
isBusy = false;
};
clear();
// let activeThumbType: ScrollbarThumbType | null = null;
const updateScrollbar = () => {
const { xThumbStyle, yThumbStyle } = calcScrollbarStyles({
viewScaleInfo: sharer.getActiveViewScaleInfo(),
viewSizeInfo: sharer.getActiveViewSizeInfo(),
});
sharer.setSharedStorage(keyXThumbStyle, xThumbStyle);
sharer.setSharedStorage(keyYThumbStyle, yThumbStyle);
};
const updateMovingScrollbar = (opts: { thumbMoveX: number; thumbMoveY: number }) => {
const { thumbMoveX, thumbMoveY } = opts;
const xThumbStyle = sharer.getSharedStorage(keyXThumbStyle);
const yThumbStyle = sharer.getSharedStorage(keyYThumbStyle);
const viewSizeInfo = sharer.getActiveViewSizeInfo();
if (xThumbStyle && (thumbMoveX > 0 || thumbMoveX < 0)) {
const maxScrollWidth = viewSizeInfo.width - scrollbarTrackSize * 2;
const minLeft = scrollbarTrackSize;
let left = (xThumbStyle.left as number) - thumbMoveX;
left = Math.min(
viewSizeInfo.width - scrollbarTrackSize - scrollbarThumbLength,
Math.max(scrollbarTrackSize, left)
);
let width = xThumbStyle.width as number;
if (left + width >= maxScrollWidth || left <= minLeft) {
if (thumbMoveX < 0) {
width += thumbMoveX;
} else {
width -= thumbMoveX;
}
}
width = Math.min(maxScrollWidth, Math.max(scrollbarThumbLength, width));
xThumbStyle.left = left;
xThumbStyle.width = width;
sharer.setSharedStorage(keyXThumbStyle, xThumbStyle);
}
if (yThumbStyle && (thumbMoveY > 0 || thumbMoveY < 0)) {
const maxScrollHeight = viewSizeInfo.height - scrollbarTrackSize * 2;
const minTop = scrollbarTrackSize;
let top = (yThumbStyle.top as number) - thumbMoveY;
top = Math.min(
viewSizeInfo.height - scrollbarTrackSize - scrollbarThumbLength,
Math.max(scrollbarTrackSize, top)
);
let height = yThumbStyle.height as number;
if (top + height >= maxScrollHeight || top <= minTop) {
if (thumbMoveY < 0) {
height += thumbMoveY;
} else {
height -= thumbMoveY;
}
}
height = Math.min(maxScrollHeight, Math.max(scrollbarThumbLength, height));
yThumbStyle.top = top;
yThumbStyle.height = height;
sharer.setSharedStorage(keyYThumbStyle, yThumbStyle);
}
};
const scrollX = (p: Point) => {
const prevPoint: null | Point = sharer.getSharedStorage(keyPrevPoint);
@ -58,6 +125,7 @@ export const MiddlewareScroller: Middleware<DeepScrollerSharedStorage, any, Midd
const totalWidth = width + Math.abs(offsetLeft) + Math.abs(offsetRight);
const moveX = (thumbMoveX * totalWidth) / width;
viewer.scroll({ moveX });
updateMovingScrollbar({ thumbMoveX, thumbMoveY: 0 });
viewer.drawFrame();
}
};
@ -71,20 +139,39 @@ export const MiddlewareScroller: Middleware<DeepScrollerSharedStorage, any, Midd
const totalHeight = height + Math.abs(offsetTop) + Math.abs(offsetBottom);
const moveY = (thumbMoveY * totalHeight) / height;
viewer.scroll({ moveY });
updateMovingScrollbar({ thumbMoveX: 0, thumbMoveY });
viewer.drawFrame();
}
};
const getThumbType = (p: Point) => {
return isPointInScrollThumb(overlayContext, p, {
xThumbRect: sharer.getSharedStorage(keyXThumbRect),
yThumbRect: sharer.getSharedStorage(keyYThumbRect)
});
};
return {
name: '@middleware/scroller',
use() {
initStyles(rootClassName, styles);
const initedResult = initRoot({ rootClassName, $container: opts.container as HTMLElement });
$horizontal = initedResult.$horizontal;
$vertical = initedResult.$vertical;
// init styles
updateScrollbar();
updateScrollbarStyles({
xThumbStyle: sharer.getSharedStorage(keyXThumbStyle),
yThumbStyle: sharer.getSharedStorage(keyYThumbStyle),
$horizontal,
$vertical,
});
},
disuse() {
destroyStyles(rootClassName);
// clear dom
$horizontal?.remove();
$horizontal = null;
$vertical?.remove();
$vertical = null;
},
resetConfig(config) {
innerConfig = { ...innerConfig, ...config };
},
@ -92,8 +179,9 @@ export const MiddlewareScroller: Middleware<DeepScrollerSharedStorage, any, Midd
wheel: (e: BoardWatherWheelEvent) => {
viewer.scroll({
moveX: 0 - e.deltaX,
moveY: 0 - e.deltaY
moveY: 0 - e.deltaY,
});
updateScrollbar();
viewer.drawFrame();
},
@ -101,36 +189,35 @@ export const MiddlewareScroller: Middleware<DeepScrollerSharedStorage, any, Midd
if (isBusy === true) {
return false;
}
const { point } = e;
const thumbType = getThumbType(point);
const { nativeEvent } = e;
const thumbType = getThumbType(nativeEvent);
if (thumbType === 'X' || thumbType === 'Y') {
if (thumbType === 'X') {
sharer.setSharedStorage(keyHoverXThumbRect, true);
sharer.setSharedStorage(keyHoverYThumbRect, false);
} else {
sharer.setSharedStorage(keyHoverXThumbRect, false);
sharer.setSharedStorage(keyHoverYThumbRect, true);
}
eventHub.trigger(coreEventKeys.CURSOR, { type: 'default' });
return false;
}
sharer.setSharedStorage(keyHoverXThumbRect, false);
sharer.setSharedStorage(keyHoverYThumbRect, false);
if (isInScrollbar(nativeEvent)) {
return false;
}
},
pointStart: (e: PointWatcherEvent) => {
const { point } = e;
const thumbType = getThumbType(point);
const { point, nativeEvent } = e;
const thumbType = getThumbType(nativeEvent);
if (thumbType === 'X' || thumbType === 'Y') {
isBusy = true;
sharer.setSharedStorage(keyActiveThumbType, thumbType);
sharer.setSharedStorage(keyPrevPoint, point);
return false;
}
if (isInScrollbar(nativeEvent)) {
return false;
}
},
pointMove: (e: PointWatcherEvent) => {
const { point } = e;
const { point, nativeEvent } = e;
const activeThumbType = sharer.getSharedStorage(keyActiveThumbType);
if (activeThumbType === 'X' || activeThumbType === 'Y') {
sharer.setSharedStorage(keyActivePoint, point);
@ -142,6 +229,9 @@ export const MiddlewareScroller: Middleware<DeepScrollerSharedStorage, any, Midd
sharer.setSharedStorage(keyPrevPoint, point);
return false;
}
if (isInScrollbar(nativeEvent)) {
return false;
}
},
pointEnd: () => {
isBusy = false;
@ -149,31 +239,18 @@ export const MiddlewareScroller: Middleware<DeepScrollerSharedStorage, any, Midd
clear();
if (activeThumbType === 'X' || activeThumbType === 'Y') {
viewer.scroll({ moveX: 0, moveY: 0 });
updateScrollbar();
viewer.drawFrame();
return false;
}
},
beforeDrawFrame({ snapshot }) {
const {
thumbBackground,
thumbBorderColor,
hoverThumbBackground,
hoverThumbBorderColor,
activeThumbBackground,
activeThumbBorderColor
} = innerConfig;
const style = {
thumbBackground,
thumbBorderColor,
hoverThumbBackground,
hoverThumbBorderColor,
activeThumbBackground,
activeThumbBorderColor
};
const { xThumbRect, yThumbRect } = drawScroller(overlayContext, { snapshot, style });
sharer.setSharedStorage(keyXThumbRect, xThumbRect);
sharer.setSharedStorage(keyYThumbRect, yThumbRect);
}
beforeDrawFrame() {
updateScrollbarStyles({
$horizontal,
$vertical,
xThumbStyle: sharer.getSharedStorage(keyXThumbStyle),
yThumbStyle: sharer.getSharedStorage(keyYThumbStyle),
});
},
};
};

View file

@ -0,0 +1,38 @@
import type { MiddlewareScrollerStyles } from '@idraw/types';
import { createId } from '@idraw/util';
export const key = 'SCROLL';
export const keyXThumbStyle = Symbol(`${key}_xThumbStyle`);
export const keyYThumbStyle = Symbol(`${key}_yThumbStyle`);
export const keyPrevPoint = Symbol(`${key}_prevPoint`);
export const keyActivePoint = Symbol(`${key}_activePoint`);
export const keyActiveThumbType = Symbol(`${key}_activeThumbType`);
export const prefix = `idraw-middleware-scroller`;
export const getRootClassName = () => `${prefix}-${createId()}`;
export const scrollbarTrackSize = 16;
export const scrollbarThumbLength = scrollbarTrackSize * 2.5;
export const scrollbarThumbSize = scrollbarTrackSize * 0.5;
export const ATTR_THUMB_TYPE = 'data-idraw-thumb-type';
export const THUMB_X = 'X';
export const THUMB_Y = 'Y';
export const defaultStyles: MiddlewareScrollerStyles = {
zIndex: 2,
thumbBackground: '#0000003A',
thumbBorderColor: '#0000008A',
hoverThumbBackground: '#0000005F',
hoverThumbBorderColor: '#000000EE',
activeThumbBackground: '#0000005E',
activeThumbBorderColor: '#000000F0',
};
export const classNameMap = {
horizontal: `${prefix}-horizontal`,
vertical: `${prefix}-vertical`,
thumb: `${prefix}-thumb`,
};

View file

@ -0,0 +1,82 @@
import type { MiddlewareScrollerStyles, MiddlewareScrollerConfig, StylesProps } from '@idraw/types';
import { injectStyles, removeStyles, getMiddlewareValidStyles } from '@idraw/util';
import { classNameMap, scrollbarTrackSize, scrollbarThumbLength, scrollbarThumbSize } from './static';
export function initStyles(rootClassName: string, styles: MiddlewareScrollerStyles) {
const cls = (str: string) => `.${str}`;
const stylesProps: StylesProps = {
zIndex: styles.zIndex,
position: 'absolute',
background: 'transparent',
[cls(classNameMap.thumb)]: {
position: 'absolute',
background: styles.thumbBackground,
border: `1px solid ${styles.thumbBorderColor}`,
borderRadius: `${scrollbarThumbSize / 2}px`,
boxSizing: 'border-box',
[`&:hover`]: {
background: styles.hoverThumbBackground,
border: `1px solid ${styles.hoverThumbBorderColor}`,
},
[`&:active`]: {
background: styles.activeThumbBackground,
border: `1px solid ${styles.activeThumbBorderColor}`,
},
},
[`&${cls(classNameMap.vertical)}`]: {
top: 0,
bottom: 0,
right: 0,
left: 'unset',
width: scrollbarTrackSize,
overflow: 'hidden',
[cls(classNameMap.thumb)]: {
top: scrollbarTrackSize,
bottom: 'unset',
left: scrollbarThumbSize / 2,
right: 'unset',
height: scrollbarThumbLength,
width: scrollbarThumbSize,
},
},
[`&${cls(classNameMap.horizontal)}`]: {
left: 0,
right: 0,
top: 'unset',
bottom: 0,
height: scrollbarTrackSize,
overflow: 'hidden',
[cls(classNameMap.thumb)]: {
top: scrollbarThumbSize / 2,
bottom: 'unset',
left: scrollbarTrackSize,
right: 'unset',
height: scrollbarThumbSize,
width: scrollbarThumbLength,
},
},
};
injectStyles({ styles: stylesProps, rootClassName, type: 'element' });
}
export function destroyStyles(rootClassName: string) {
removeStyles({ rootClassName, type: 'element' });
}
export function getMiddlewareScrollerStyles<C = MiddlewareScrollerConfig, S = MiddlewareScrollerStyles>(config: C): S {
const styles: S = getMiddlewareValidStyles<C, S>(config, [
'zIndex',
'thumbBackground',
'thumbBorderColor',
'hoverThumbBackground',
'hoverThumbBorderColor',
'activeThumbBackground',
'activeThumbBorderColor',
]);
return styles;
}

View file

@ -1,12 +1,16 @@
import type { Point, ElementSize } from '@idraw/types';
import { keyXThumbRect, keyYThumbRect, keyPrevPoint, keyActivePoint, keyActiveThumbType, keyHoverXThumbRect, keyHoverYThumbRect } from './config';
import type { Point, HTMLCSSProps } from '@idraw/types';
import { keyXThumbStyle, keyYThumbStyle, keyPrevPoint, keyActivePoint, keyActiveThumbType } from './static';
export type DeepScrollerSharedStorage = {
[keyXThumbRect]: null | ElementSize;
[keyYThumbRect]: null | ElementSize;
[keyHoverXThumbRect]: boolean | null;
[keyHoverYThumbRect]: boolean | null;
[keyXThumbStyle]: null | HTMLCSSProps;
[keyYThumbStyle]: null | HTMLCSSProps;
[keyPrevPoint]: null | Point;
[keyActivePoint]: null | Point;
[keyActiveThumbType]: null | 'X' | 'Y';
};
export type ScrollbarStyles = {
xThumbStyle: HTMLCSSProps | null;
yThumbStyle: HTMLCSSProps | null;
};

View file

@ -1,95 +1,19 @@
import type {
Point,
BoardViewerFrameSnapshot,
ViewScaleInfo,
ViewSizeInfo,
ViewContext2D,
ElementSize,
MiddlewareScrollerStyle
} from '@idraw/types';
import { getViewScaleInfoFromSnapshot, getViewSizeInfoFromSnapshot } from '@idraw/util';
import {
keyActivePoint,
keyActiveThumbType,
keyPrevPoint,
keyXThumbRect,
keyYThumbRect,
keyHoverXThumbRect,
keyHoverYThumbRect
} from './config';
import type { ViewScaleInfo, ViewSizeInfo, HTMLCSSProps } from '@idraw/types';
import { scrollbarTrackSize, scrollbarThumbLength } from './static';
import type { ScrollbarStyles } from './types';
const scrollerLineWidth = 16;
const minThumbLength = scrollerLineWidth * 2.5;
export type ScrollbarThumbType = 'X' | 'Y';
function isPointAtRect(overlayContext: ViewContext2D, p: Point, rect: ElementSize): boolean {
const ctx = overlayContext;
const { x, y, w, h } = rect;
ctx.beginPath();
ctx.rect(x, y, w, h);
ctx.closePath();
if (ctx.isPointInPath(p.x, p.y)) {
return true;
}
return false;
}
export function isPointInScrollThumb(
overlayContext: ViewContext2D,
p: Point,
opts: {
xThumbRect?: ElementSize | null;
yThumbRect?: ElementSize | null;
}
): ScrollbarThumbType | null {
let thumbType: ScrollbarThumbType | null = null;
const { xThumbRect, yThumbRect } = opts;
if (xThumbRect && isPointAtRect(overlayContext, p, xThumbRect)) {
thumbType = 'X';
} else if (yThumbRect && isPointAtRect(overlayContext, p, yThumbRect)) {
thumbType = 'Y';
}
return thumbType;
}
interface ScrollInfo {
activePoint: Point | null;
prevPoint: Point | null;
activeThumbType: ScrollbarThumbType | null;
xThumbRect: ElementSize | null;
yThumbRect: ElementSize | null;
hoverXThumb: boolean | null;
hoverYThumb: boolean | null;
}
function getScrollInfoFromSnapshot(snapshot: BoardViewerFrameSnapshot): ScrollInfo {
const { sharedStore } = snapshot;
const info: ScrollInfo = {
activePoint: sharedStore[keyActivePoint] || null,
prevPoint: sharedStore[keyPrevPoint] || null,
activeThumbType: sharedStore[keyActiveThumbType] || null,
xThumbRect: sharedStore[keyXThumbRect] || null,
yThumbRect: sharedStore[keyYThumbRect] || null,
hoverXThumb: sharedStore[keyHoverXThumbRect],
hoverYThumb: sharedStore[keyHoverYThumbRect]
};
return info;
}
function calcScrollerInfo(opts: {
export function calcScrollbarStyles(opts: {
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
hoverXThumb: boolean | null;
hoverYThumb: boolean | null;
style: MiddlewareScrollerStyle;
}) {
const { viewScaleInfo, viewSizeInfo, hoverXThumb, hoverYThumb, style } = opts;
}): ScrollbarStyles {
const { viewScaleInfo, viewSizeInfo } = opts;
const { width, height } = viewSizeInfo;
const { offsetTop, offsetBottom, offsetLeft, offsetRight } = viewScaleInfo;
const scrollerLineWidth = scrollbarTrackSize;
const minThumbLength = scrollbarThumbLength;
const sliderMinSize = minThumbLength;
const lineSize = scrollerLineWidth;
const { thumbBackground, thumbBorderColor, hoverThumbBackground, hoverThumbBorderColor } = style;
let xSize = 0;
let ySize = 0;
xSize = Math.max(sliderMinSize, width - lineSize * 2 - (Math.abs(offsetLeft) + Math.abs(offsetRight)));
@ -127,170 +51,17 @@ function calcScrollerInfo(opts: {
translateY = yStart + ((height - ySize) * Math.abs(offsetTop)) / (Math.abs(offsetTop) + Math.abs(offsetBottom));
translateY = Math.min(Math.max(0, translateY - yStart), height - ySize);
}
const xThumbRect: ElementSize = {
x: translateX,
y: height - lineSize,
w: xSize,
h: lineSize
const xThumbStyle: HTMLCSSProps = {
left: translateX,
width: xSize,
};
const yThumbRect: ElementSize = {
x: width - lineSize,
y: translateY,
w: lineSize,
h: ySize
const yThumbStyle: HTMLCSSProps = {
top: translateY,
height: ySize,
};
const scrollWrapper = {
lineSize,
xSize,
ySize,
translateY,
translateX,
xThumbBackground: hoverXThumb ? hoverThumbBackground : thumbBackground,
yThumbBackground: hoverYThumb ? hoverThumbBackground : thumbBackground,
xThumbBorderColor: hoverXThumb ? hoverThumbBorderColor : thumbBorderColor,
yThumbBorderColor: hoverYThumb ? hoverThumbBorderColor : thumbBorderColor,
// scrollBarColor: scrollConfig.scrollBarColor,
xThumbRect,
yThumbRect
const scrollbarInfo: ScrollbarStyles = {
xThumbStyle,
yThumbStyle,
};
return scrollWrapper;
}
function drawScrollerThumb(
ctx: ViewContext2D,
opts: {
axis: ScrollbarThumbType;
x: number;
y: number;
w: number;
h: number;
r: number;
background: string;
borderColor: string;
}
): void {
let { x, y, h, w } = opts;
const { background, borderColor } = opts;
ctx.save();
ctx.shadowColor = '#FFFFFF';
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = 1;
{
const { axis } = opts;
if (axis === 'X') {
y = y + h / 4 + 0;
h = h / 2;
} else if (axis === 'Y') {
x = x + w / 4 + 0;
w = w / 2;
}
let r = opts.r;
r = Math.min(r, w / 2, h / 2);
if (w < r * 2 || h < r * 2) {
r = 0;
}
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.fillStyle = background;
ctx.fill('nonzero');
ctx.beginPath();
ctx.lineWidth = 1;
ctx.strokeStyle = borderColor;
ctx.setLineDash([]);
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.stroke();
}
ctx.restore();
}
function drawScrollerInfo(
overlayContext: ViewContext2D,
opts: {
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
scrollInfo: ScrollInfo;
style: MiddlewareScrollerStyle;
}
) {
const ctx = overlayContext;
const { viewScaleInfo, viewSizeInfo, scrollInfo, style } = opts;
const { activeThumbType, prevPoint, activePoint, hoverXThumb, hoverYThumb } = scrollInfo;
// const { width, height } = viewSizeInfo;
const wrapper = calcScrollerInfo({ viewScaleInfo, viewSizeInfo, hoverXThumb, hoverYThumb, style });
let xThumbRect: ElementSize = { ...wrapper.xThumbRect };
let yThumbRect: ElementSize = { ...wrapper.yThumbRect };
if (activeThumbType && prevPoint && activePoint) {
if (activeThumbType === 'X' && scrollInfo.xThumbRect) {
xThumbRect = { ...scrollInfo.xThumbRect };
xThumbRect.x = xThumbRect.x + (activePoint.x - prevPoint.x);
} else if (activeThumbType === 'Y' && scrollInfo.yThumbRect) {
yThumbRect = { ...scrollInfo.yThumbRect };
yThumbRect.y = yThumbRect.y + (activePoint.y - prevPoint.y);
}
}
// // x-bar
// if (scrollConfig.showScrollBar === true) {
// ctx.fillStyle = wrapper.scrollBarColor;
// // x-line
// ctx.fillRect(0, height - wrapper.lineSize, width, wrapper.lineSize);
// }
// x-thumb
drawScrollerThumb(ctx, {
axis: 'X',
...xThumbRect,
r: wrapper.lineSize / 2,
background: wrapper.xThumbBackground,
borderColor: wrapper.xThumbBorderColor
});
// // y-bar
// if (scrollConfig.showScrollBar === true) {
// ctx.fillStyle = wrapper.scrollBarColor;
// // y-line
// ctx.fillRect(width - wrapper.lineSize, 0, wrapper.lineSize, height);
// }
// y-thumb
drawScrollerThumb(ctx, {
axis: 'Y',
...yThumbRect,
r: wrapper.lineSize / 2,
background: wrapper.yThumbBackground,
borderColor: wrapper.yThumbBorderColor
});
return {
xThumbRect,
yThumbRect
};
}
export function drawScroller(
ctx: ViewContext2D,
opts: { snapshot: BoardViewerFrameSnapshot; style: MiddlewareScrollerStyle }
) {
const { snapshot, style } = opts;
const viewSizeInfo = getViewSizeInfoFromSnapshot(snapshot);
const viewScaleInfo = getViewScaleInfoFromSnapshot(snapshot);
const scrollInfo = getScrollInfoFromSnapshot(snapshot);
const { xThumbRect, yThumbRect } = drawScrollerInfo(ctx, { viewSizeInfo, viewScaleInfo, scrollInfo, style });
return { xThumbRect, yThumbRect };
return scrollbarInfo;
}

View file

@ -1,49 +0,0 @@
import type { MiddlewareSelectorStyle } from '@idraw/types';
export const key = 'SELECT';
// export const keyHoverElement = Symbol(`${key}_hoverElementSize`);
export const keyActionType = Symbol(`${key}_actionType`); // 'select' | 'drag-list' | 'drag-list-end' | 'drag' | 'hover' | 'resize' | 'area' | null = null;
export const keyResizeType = Symbol(`${key}_resizeType`); // ResizeType | null;
export const keyAreaStart = Symbol(`${key}_areaStart`); // Point
export const keyAreaEnd = Symbol(`${key}_areaEnd`); // Point
export const keyHoverElement = Symbol(`${key}_hoverElement`); // Element<ElementType> | []
export const keyHoverElementVertexes = Symbol(`${key}_hoverElementVertexes`); // ViewRectVertexes | null
export const keySelectedElementList = Symbol(`${key}_selectedElementList`); // Array<Element<ElementType>> | []
export const keySelectedElementListVertexes = Symbol(`${key}_selectedElementListVertexes`); // ViewRectVertexes | null
export const keySelectedElementController = Symbol(`${key}_selectedElementController`); // ElementSizeController
export const keySelectedElementPosition = Symbol(`${key}_selectedElementPosition`); // ElementPosition | []
export const keyGroupQueue = Symbol(`${key}_groupQueue`); // Array<Element<'group'>> | []
export const keyGroupQueueVertexesList = Symbol(`${key}_groupQueueVertexesList`); // Array<ViewRectVertexes> | []
export const keyIsMoving = Symbol(`${key}_isMoving`); // boolean | null
export const keyEnableSelectInGroup = Symbol(`${key}_enableSelectInGroup`);
export const keyEnableSnapToGrid = Symbol(`${key}_enableSnapToGrid`);
export const keyDebugElemCenter = Symbol(`${key}_debug_elemCenter`);
export const keyDebugStartVertical = Symbol(`${key}_debug_startVertical`);
export const keyDebugEndVertical = Symbol(`${key}_debug_endVertical`);
export const keyDebugStartHorizontal = Symbol(`${key}_debug_startHorizontal`);
export const keyDebugEndHorizontal = Symbol(`${key}_debug_endHorizontal`);
export const keyDebugEnd0 = Symbol(`${key}_debug_end0`);
export const selectWrapperBorderWidth = 2;
export const resizeControllerBorderWidth = 4;
export const areaBorderWidth = 1;
export const controllerSize = 10;
// export const rotateControllerSize = 16;
export const rotateControllerSize = 20;
export const rotateControllerPosition = 22;
const activeColor = '#1973ba';
const activeAreaColor = '#1976d21c';
const lockedColor = '#5b5959b5';
const referenceColor = '#f7276e';
export const defaultStyle: MiddlewareSelectorStyle = {
activeColor,
activeAreaColor,
lockedColor,
referenceColor
};

View file

@ -0,0 +1,559 @@
import type { RenderMaterialHelperOptions, MaterialSize, Point, Material, StrictMaterial } from '@idraw/types';
import {
ATTR_VALID_WATCH,
createHTMLElement,
calcViewMaterialSize,
assembleHTMLElement,
addClassName,
removeClassName,
getMaterialSize,
calcMaterialListSize,
calcViewPoint,
setHTMLCSSProps,
bubbleHTMLElement,
} from '@idraw/util';
import {
classNameMap,
ATTR_BOX_TYPE,
ATTR_MATERIAL_ID,
ATTR_HANDLER_TYPE,
BOX_TARGET,
BOX_GROUP,
cornerHandlerSize,
} from './static';
type StrictMaterialSize = Required<MaterialSize>;
function createNestedBox(opts: {
viewMaterialSize: StrictMaterialSize | null;
viewGroupSizeQueue: StrictMaterialSize[];
className: string;
targetClassName: string;
}) {
const { viewMaterialSize, viewGroupSizeQueue, className, targetClassName } = opts;
let $target: HTMLDivElement | null = null;
if (viewMaterialSize) {
$target = createHTMLElement('div', {
[ATTR_BOX_TYPE]: BOX_TARGET,
[ATTR_VALID_WATCH]: 'true',
[ATTR_MATERIAL_ID]: viewMaterialSize.id,
className: classNameMap.materialBox,
style: {
display: 'block',
position: 'absolute',
top: viewMaterialSize.y,
left: viewMaterialSize.x,
width: viewMaterialSize.width,
height: viewMaterialSize.height,
transform: `rotate(${viewMaterialSize.angle || 0}deg)`,
},
});
addClassName($target, [targetClassName]);
}
let $result = $target;
for (let i = viewGroupSizeQueue.length - 1; i >= 0; i--) {
const groupSize = viewGroupSizeQueue[i];
const children = [];
if ($result) {
children.push($result);
}
$result = createHTMLElement(
'div',
{
[ATTR_BOX_TYPE]: BOX_GROUP,
[ATTR_VALID_WATCH]: 'true',
[ATTR_MATERIAL_ID]: groupSize.id,
className: `${classNameMap.materialBox} ${classNameMap.groupBox}`,
style: {
position: 'absolute',
top: groupSize.y,
left: groupSize.x,
width: groupSize.width,
height: groupSize.height,
transform: `rotate(${groupSize.angle || 0}deg)`,
},
},
children
);
}
if ($result) {
addClassName($result, [className]);
}
return $result;
}
function calcBoxSizes(opts: RenderMaterialHelperOptions): {
viewMaterialSize: StrictMaterialSize | null;
viewGroupSizeQueue: StrictMaterialSize[];
} {
const { material, groupQueue, viewScaleInfo } = opts;
let viewMaterialSize = material ? getMaterialSize(material) : null;
const viewGroupSizeQueue = groupQueue.map((group) => getMaterialSize(group));
if (Array.isArray(viewGroupSizeQueue) && viewGroupSizeQueue.length > 0) {
viewMaterialSize = viewMaterialSize
? calcViewMaterialSize(viewMaterialSize, { viewScaleInfo: { scale: viewScaleInfo.scale } })
: null;
viewGroupSizeQueue[0] = calcViewMaterialSize(viewGroupSizeQueue[0], { viewScaleInfo });
for (let i = 1; i < viewGroupSizeQueue.length; i++) {
viewGroupSizeQueue[i] = calcViewMaterialSize(viewGroupSizeQueue[i], {
viewScaleInfo: { scale: viewScaleInfo.scale },
});
}
} else {
viewMaterialSize = viewMaterialSize ? calcViewMaterialSize(viewMaterialSize, { viewScaleInfo }) : null;
}
return {
viewMaterialSize: viewMaterialSize as StrictMaterialSize | null,
viewGroupSizeQueue: viewGroupSizeQueue as StrictMaterialSize[],
};
}
function generateBoxsBySizes(
$root: HTMLDivElement | null,
opts: {
viewMaterialSize: StrictMaterialSize | null;
viewGroupSizeQueue: StrictMaterialSize[];
className: string;
targetClassName: string;
}
) {
if (!$root) {
return null;
}
const { className, targetClassName, viewMaterialSize, viewGroupSizeQueue } = opts;
const $box = createNestedBox({
viewMaterialSize,
viewGroupSizeQueue,
className,
targetClassName,
});
if ($box) {
assembleHTMLElement($root, {}, [$box]);
}
return $box;
}
function generateBoxs(
$root: HTMLDivElement | null,
opts: RenderMaterialHelperOptions & {
className: string;
targetClassName: string;
}
) {
if (!$root) {
return null;
}
const { className, targetClassName } = opts;
const { viewMaterialSize, viewGroupSizeQueue } = calcBoxSizes(opts);
return generateBoxsBySizes($root, {
viewMaterialSize,
viewGroupSizeQueue,
className,
targetClassName,
});
}
function resetBoxs(
$root: HTMLDivElement | null,
opts: RenderMaterialHelperOptions & {
className: string;
targetClassName: string;
renderTargetInner?: ($target: HTMLElement) => void;
destoryTargetInner?: ($target: HTMLElement) => void;
afterRender?: (opts: {
$rootBox: HTMLElement | null;
viewMaterialSize: Required<MaterialSize> | null;
viewGroupSizeQueue: Required<MaterialSize>[];
}) => void;
}
) {
if (!$root) {
return null;
}
const { className, targetClassName, renderTargetInner, destoryTargetInner, afterRender } = opts;
const $boxs = $root.getElementsByClassName(className);
const { viewMaterialSize, viewGroupSizeQueue } = calcBoxSizes(opts);
const remove = () => {
Array.from($boxs).forEach(($box) => {
$box.remove();
});
};
if (!viewMaterialSize && !viewGroupSizeQueue.length) {
remove();
}
if ($boxs.length === 1) {
const $box = $boxs[0] as HTMLDivElement;
addClassName($box, [className]);
if (viewGroupSizeQueue.length > 0) {
let index = 0;
let $current: HTMLDivElement | undefined = $boxs[0] as HTMLDivElement;
let $parent: HTMLElement | null = $current.parentElement;
while (index < viewGroupSizeQueue.length) {
const groupSize = viewGroupSizeQueue[index];
if ($current) {
removeClassName($current as HTMLDivElement, [targetClassName]);
assembleHTMLElement($current, {
[ATTR_BOX_TYPE]: BOX_GROUP,
[ATTR_VALID_WATCH]: 'true',
[ATTR_MATERIAL_ID]: groupSize.id,
style: {
position: 'absolute',
top: groupSize.y,
left: groupSize.x,
width: groupSize.width,
height: groupSize.height,
transform: `rotate(${groupSize.angle || 0}deg)`,
},
});
} else {
$current = createHTMLElement('div', {
[ATTR_BOX_TYPE]: BOX_GROUP,
[ATTR_VALID_WATCH]: 'true',
[ATTR_MATERIAL_ID]: groupSize.id,
className: `${classNameMap.materialBox} ${classNameMap.groupBox}`,
style: {
position: 'absolute',
top: groupSize.y,
left: groupSize.x,
width: groupSize.width,
height: groupSize.height,
transform: `rotate(${groupSize.angle || 0}deg)`,
},
});
$parent?.appendChild($current);
}
$parent = $current;
if (index + 1 === viewGroupSizeQueue.length) {
// TODO
break;
}
// next
$current = $current?.children?.[0] as HTMLDivElement | undefined;
index++;
}
if (viewMaterialSize) {
let $target: HTMLElement | undefined = $current?.children?.[0] as HTMLElement | undefined;
if (!$target) {
$target = createHTMLElement('div');
$parent?.appendChild($target);
}
assembleHTMLElement($target as HTMLDivElement, {
[ATTR_BOX_TYPE]: BOX_TARGET,
[ATTR_VALID_WATCH]: 'true',
[ATTR_MATERIAL_ID]: viewMaterialSize.id,
// className: classNameMap.materialBox,
style: {
display: 'block',
position: 'absolute',
top: viewMaterialSize.y,
left: viewMaterialSize.x,
width: viewMaterialSize.width,
height: viewMaterialSize.height,
transform: `rotate(${viewMaterialSize.angle || 0}deg)`,
},
});
renderTargetInner?.($target);
} else {
destoryTargetInner?.($current as HTMLDivElement);
}
} else {
if (viewMaterialSize) {
destoryTargetInner?.($box);
assembleHTMLElement($box, {
[ATTR_BOX_TYPE]: BOX_TARGET,
[ATTR_VALID_WATCH]: 'true',
[ATTR_MATERIAL_ID]: viewMaterialSize.id,
style: {
display: 'block',
position: 'absolute',
top: viewMaterialSize.y,
left: viewMaterialSize.x,
width: viewMaterialSize.width,
height: viewMaterialSize.height,
transform: `rotate(${viewMaterialSize.angle || 0}deg)`,
},
});
addClassName($box, [targetClassName]);
renderTargetInner?.($box);
} else {
remove();
}
}
afterRender?.({ $rootBox: $box, viewGroupSizeQueue, viewMaterialSize });
return $box;
} else {
remove();
const $box = generateBoxsBySizes($root, {
viewMaterialSize,
viewGroupSizeQueue,
className,
targetClassName,
}) as HTMLDivElement;
addClassName($box, [targetClassName]);
renderTargetInner?.($box);
afterRender?.({ $rootBox: $box, viewGroupSizeQueue, viewMaterialSize });
}
}
function destroyBoxs($root: HTMLDivElement | null, opts: { className: string }) {
if (!$root) {
return;
}
const { className } = opts;
// clear existed hover box
const $prevBoxs = Array.from($root.getElementsByClassName(className));
$prevBoxs.forEach(($box) => {
$box.remove();
});
}
export function initRoot(opts: { rootClassName: string; $container: HTMLElement }) {
const { rootClassName, $container } = opts;
const create = createHTMLElement;
const $root = create('div', {
className: rootClassName,
[ATTR_VALID_WATCH]: 'true',
});
$container.appendChild($root);
return $root;
}
// nested box for in-group
function clearMaterialNestedBox($root: HTMLDivElement | null) {
return destroyBoxs($root, { className: classNameMap.nestedBox });
}
export function resetMaterialNestedBox($root: HTMLDivElement | null, opts: RenderMaterialHelperOptions) {
const { groupQueue } = opts;
if (Array.isArray(groupQueue) && groupQueue.length) {
resetBoxs($root, {
...opts,
className: classNameMap.nestedBox,
targetClassName: classNameMap.nestedTargetBox,
});
} else {
clearMaterialNestedBox($root);
}
}
// Hover
export function clearMaterialHoverBox($root: HTMLDivElement | null) {
return destroyBoxs($root, { className: classNameMap.hoverBox });
}
export function renderMaterialHoverBox($root: HTMLDivElement | null, opts: RenderMaterialHelperOptions) {
clearMaterialHoverBox($root);
resetBoxs($root, {
...opts,
className: classNameMap.hoverBox,
targetClassName: classNameMap.hoverTargetBox,
});
}
// Locked
export function clearMaterialLockedBox($root: HTMLDivElement | null) {
return destroyBoxs($root, { className: classNameMap.lockedBox });
}
export function renderMaterialLockedBox($root: HTMLDivElement | null, opts: RenderMaterialHelperOptions) {
clearMaterialLockedBox($root);
generateBoxs($root, {
...opts,
className: classNameMap.lockedBox,
targetClassName: classNameMap.lockedTargetBox,
});
}
// selected
function clearMaterialSelectedBox($root: HTMLDivElement | null) {
return destroyBoxs($root, { className: classNameMap.selectedBox });
}
function renderSelectedBoxInnerHandlers($target: HTMLElement) {
const $existHandlers = $target.querySelectorAll(`[${ATTR_HANDLER_TYPE}]`);
if ($existHandlers.length > 0) {
return;
}
const create = createHTMLElement;
const baseAttrs = {
[ATTR_VALID_WATCH]: 'true',
};
assembleHTMLElement($target, {}, [
create('div', {
[ATTR_HANDLER_TYPE]: 'left',
...baseAttrs,
className: `${classNameMap.edgeHandler} ${classNameMap.edgeLeftHandler}`,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'top',
...baseAttrs,
className: `${classNameMap.edgeHandler} ${classNameMap.edgeTopHandler}`,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'right',
...baseAttrs,
className: `${classNameMap.edgeHandler} ${classNameMap.edgeRightHandler}`,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'bottom',
...baseAttrs,
className: `${classNameMap.edgeHandler} ${classNameMap.edgeBottomHandler}`,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'top-left',
...baseAttrs,
className: `${classNameMap.cornerHandler} ${classNameMap.cornerTopLeftHandler}`,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'top-right',
...baseAttrs,
className: `${classNameMap.cornerHandler} ${classNameMap.cornerTopRightHandler}`,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'bottom-left',
...baseAttrs,
className: `${classNameMap.cornerHandler} ${classNameMap.cornerBottomLeftHandler}`,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'bottom-right',
...baseAttrs,
className: `${classNameMap.cornerHandler} ${classNameMap.cornerBottomRightHandler}`,
}),
create('div', {
[ATTR_HANDLER_TYPE]: 'rotate',
...baseAttrs,
className: classNameMap.rotateHandler,
}),
]);
}
export function resetMaterialSelectedBox($root: HTMLDivElement | null, opts: RenderMaterialHelperOptions) {
const { material } = opts;
if (material) {
resetBoxs($root, {
...opts,
className: classNameMap.selectedBox,
targetClassName: classNameMap.selectedTargetBox,
renderTargetInner: renderSelectedBoxInnerHandlers,
destoryTargetInner: ($target) => ($target.innerHTML = ''),
afterRender: ({ $rootBox, viewMaterialSize }) => {
if (viewMaterialSize && $rootBox) {
const { width, height } = viewMaterialSize;
const size = Math.min(width, height);
if (size > cornerHandlerSize * 4) {
removeClassName($rootBox, [classNameMap.hideHandler]);
} else {
addClassName($rootBox, [classNameMap.hideHandler]);
}
}
},
});
} else {
clearMaterialSelectedBox($root);
}
}
// selection area
export function clearMaterialSelectionAreaBox($root: HTMLDivElement | null) {
return destroyBoxs($root, { className: classNameMap.selectionAreaBox });
}
function getSelectionAreaBox($root: HTMLDivElement) {
const $boxs = $root.getElementsByClassName(classNameMap.selectionAreaBox);
if ($boxs[0]) {
return $boxs[0] as HTMLElement;
}
const $box = createHTMLElement('div', { [ATTR_VALID_WATCH]: 'true', className: classNameMap.selectionAreaBox });
assembleHTMLElement($root, {}, [$box]);
return $box as HTMLElement;
}
export function resetMaterialSelectionAreaBox(
$root: HTMLDivElement | null,
opts: RenderMaterialHelperOptions & {
areaStart: Point | null;
areaEnd: Point | null;
selectedMaterials: Material[];
}
) {
if (!$root) {
return;
}
const { areaStart, areaEnd, selectedMaterials, viewScaleInfo } = opts;
let start: Point | null = null;
let end: Point | null = null;
let needCalcInView = false;
if (selectedMaterials.length > 1 || (selectedMaterials.length === 1 && areaStart && areaEnd)) {
const listSize = calcMaterialListSize(selectedMaterials);
const { x, y, width, height } = listSize;
start = { x, y };
end = {
x: x + width,
y: y + height,
};
needCalcInView = true;
} else if (areaStart && areaEnd) {
start = { ...areaStart };
end = { ...areaEnd };
}
if (start && end) {
const $box = getSelectionAreaBox($root);
if (needCalcInView) {
start = calcViewPoint(start, { viewScaleInfo });
end = calcViewPoint(end, { viewScaleInfo });
}
setHTMLCSSProps($box, {
left: Math.min(start.x, end.x),
top: Math.min(start.y, end.y),
width: Math.abs(end.x - start.x),
height: Math.abs(end.y - start.y),
});
} else {
clearMaterialSelectionAreaBox($root);
}
}
export function isPointInActiveGroup(
e: Event,
opts: {
$root: HTMLElement | null;
groupQueue: StrictMaterial<'group'>[] | null;
}
): boolean {
const { groupQueue, $root } = opts;
if (!groupQueue || !(groupQueue?.length > 0) || !$root) {
return false;
}
const id = groupQueue[groupQueue.length - 1].id;
const $target = e.target as HTMLElement;
if (typeof id === 'string' && id) {
if ($target?.getAttribute(ATTR_BOX_TYPE) === BOX_GROUP && $target?.getAttribute(ATTR_MATERIAL_ID) === id) {
return true;
}
const $targetGroup = bubbleHTMLElement($target, $root, {
[ATTR_BOX_TYPE]: BOX_GROUP,
});
if (
$targetGroup?.getAttribute(ATTR_BOX_TYPE) === BOX_GROUP &&
$targetGroup?.getAttribute(ATTR_MATERIAL_ID) === id
) {
return true;
}
}
return false;
}

View file

@ -1,83 +0,0 @@
import type { ViewContext2D, Element, ViewScaleInfo, ViewSizeInfo, ViewCalculator, ViewRectInfo } from '@idraw/types';
// import { auxiliaryColor } from './config';
import { drawLine, drawCrossByCenter } from './draw-base';
interface ViewBoxInfo {
minX: number;
minY: number;
maxX: number;
maxY: number;
midX: number;
midY: number;
}
function getViewBoxInfo(rectInfo: ViewRectInfo): ViewBoxInfo {
const boxInfo: ViewBoxInfo = {
minX: rectInfo.topLeft.x,
minY: rectInfo.topLeft.y,
maxX: rectInfo.bottomRight.x,
maxY: rectInfo.bottomRight.y,
midX: rectInfo.center.x,
midY: rectInfo.center.y
};
return boxInfo;
}
// export function drawAuxiliaryExperimentBox(
// ctx: ViewContext2D,
// opts: {
// calculator: ViewCalculator;
// element: Element | null;
// viewScaleInfo: ViewScaleInfo;
// viewSizeInfo: ViewSizeInfo;
// }
// ) {
// const { element, viewScaleInfo, viewSizeInfo, calculator } = opts;
// if (!element) {
// return;
// }
// const viewRectInfo = calculator.calcViewRectInfoFromRange(element.uuid, { viewScaleInfo, viewSizeInfo });
// if (!viewRectInfo) {
// return;
// }
// const lineOpts = {
// borderColor: auxiliaryColor,
// borderWidth: 1,
// lineDash: []
// };
// // drawLine(ctx, viewRectInfo.topLeft, viewRectInfo.topRight, lineOpts);
// // drawLine(ctx, viewRectInfo.topRight, viewRectInfo.bottomRight, lineOpts);
// // drawLine(ctx, viewRectInfo.bottomRight, viewRectInfo.bottomLeft, lineOpts);
// // drawLine(ctx, viewRectInfo.bottomLeft, viewRectInfo.topLeft, lineOpts);
// // // vLine
// // drawLine(ctx, { x: viewRectInfo.topLeft.x, y: 0 }, { x: viewRectInfo.topLeft.x, y: viewSizeInfo.height }, lineOpts);
// // drawLine(ctx, { x: viewRectInfo.center.x, y: 0 }, { x: viewRectInfo.center.x, y: viewSizeInfo.height }, lineOpts);
// // drawLine(ctx, { x: viewRectInfo.bottomRight.x, y: 0 }, { x: viewRectInfo.bottomRight.x, y: viewSizeInfo.height }, lineOpts);
// // // hLine
// // drawLine(ctx, { x: 0, y: viewRectInfo.topLeft.y }, { x: viewSizeInfo.width, y: viewRectInfo.topLeft.y }, lineOpts);
// // drawLine(ctx, { x: 0, y: viewRectInfo.center.y }, { x: viewSizeInfo.width, y: viewRectInfo.center.y }, lineOpts);
// // drawLine(ctx, { x: 0, y: viewRectInfo.bottomRight.y }, { x: viewSizeInfo.width, y: viewRectInfo.bottomRight.y }, lineOpts);
// const boxInfo = getViewBoxInfo(viewRectInfo);
// const { width, height } = viewSizeInfo;
// // vLine
// drawLine(ctx, { x: boxInfo.minX, y: 0 }, { x: boxInfo.minX, y: height }, lineOpts);
// drawLine(ctx, { x: boxInfo.midX, y: 0 }, { x: boxInfo.midX, y: height }, lineOpts);
// drawLine(ctx, { x: boxInfo.maxX, y: 0 }, { x: boxInfo.maxX, y: height }, lineOpts);
// // hLine
// drawLine(ctx, { x: 0, y: boxInfo.minY }, { x: width, y: boxInfo.minY }, lineOpts);
// drawLine(ctx, { x: 0, y: boxInfo.midY }, { x: width, y: boxInfo.midY }, lineOpts);
// drawLine(ctx, { x: 0, y: boxInfo.maxY }, { x: width, y: boxInfo.maxY }, lineOpts);
// const crossOpts = { ...lineOpts, size: 6 };
// drawCrossByCenter(ctx, viewRectInfo.center, crossOpts);
// drawCrossByCenter(ctx, viewRectInfo.topLeft, crossOpts);
// drawCrossByCenter(ctx, viewRectInfo.topRight, crossOpts);
// drawCrossByCenter(ctx, viewRectInfo.bottomLeft, crossOpts);
// drawCrossByCenter(ctx, viewRectInfo.bottomRight, crossOpts);
// drawCrossByCenter(ctx, viewRectInfo.top, crossOpts);
// drawCrossByCenter(ctx, viewRectInfo.right, crossOpts);
// drawCrossByCenter(ctx, viewRectInfo.bottom, crossOpts);
// drawCrossByCenter(ctx, viewRectInfo.left, crossOpts);
// }

View file

@ -1,14 +1,14 @@
import type { PointSize, ViewContext2D, ViewRectVertexes } from '@idraw/types';
import type { Point, ViewContext2D, ViewRectVertexes } from '@idraw/types';
export function drawVertexes(
ctx: ViewContext2D,
vertexes: ViewRectVertexes,
opts: { borderColor: string; borderWidth: number; background: string; lineDash: number[] }
opts: { stroke: string; strokeWidth: number; background: string; lineDash: number[] }
) {
const { borderColor, borderWidth, background, lineDash } = opts;
const { stroke, strokeWidth, background, lineDash } = opts;
ctx.setLineDash([]);
ctx.lineWidth = borderWidth;
ctx.strokeStyle = borderColor;
ctx.lineWidth = strokeWidth;
ctx.strokeStyle = stroke;
ctx.fillStyle = background;
ctx.setLineDash(lineDash);
ctx.beginPath();
@ -24,14 +24,14 @@ export function drawVertexes(
export function drawLine(
ctx: ViewContext2D,
start: PointSize,
end: PointSize,
opts: { borderColor: string; borderWidth: number; lineDash: number[] }
start: Point,
end: Point,
opts: { stroke: string; strokeWidth: number; lineDash: number[] }
) {
const { borderColor, borderWidth, lineDash } = opts;
const { stroke, strokeWidth, lineDash } = opts;
ctx.setLineDash([]);
ctx.lineWidth = borderWidth;
ctx.strokeStyle = borderColor;
ctx.lineWidth = strokeWidth;
ctx.strokeStyle = stroke;
ctx.setLineDash(lineDash);
ctx.beginPath();
ctx.moveTo(start.x, start.y);
@ -40,65 +40,15 @@ export function drawLine(
ctx.stroke();
}
export function drawCircleController(
ctx: ViewContext2D,
circleCenter: PointSize,
opts: { borderColor: string; borderWidth: number; background: string; lineDash: number[]; size: number }
) {
const { size, borderColor, borderWidth, background } = opts;
const center = circleCenter;
const r = size / 2;
const a = r;
const b = r;
// 'content-box'
if (a >= 0 && b >= 0) {
// draw border
if (typeof borderWidth === 'number' && borderWidth > 0) {
const ba = borderWidth / 2 + a;
const bb = borderWidth / 2 + b;
ctx.beginPath();
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
ctx.circle(center.x, center.y, ba, bb, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
}
// draw content
ctx.beginPath();
ctx.fillStyle = background;
ctx.circle(center.x, center.y, a, b, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill('nonzero');
}
// ctx.setLineDash([]);
// ctx.lineWidth = borderWidth;
// ctx.strokeStyle = borderColor;
// ctx.fillStyle = background;
// ctx.setLineDash(lineDash);
// ctx.beginPath();
// ctx.moveTo(vertexes[0].x, vertexes[0].y);
// ctx.lineTo(vertexes[1].x, vertexes[1].y);
// ctx.lineTo(vertexes[2].x, vertexes[2].y);
// ctx.lineTo(vertexes[3].x, vertexes[3].y);
// ctx.lineTo(vertexes[0].x, vertexes[0].y);
// ctx.closePath();
// ctx.stroke();
// ctx.fill('nonzero');
}
export function drawCrossVertexes(
function drawCrossVertexes(
ctx: ViewContext2D,
vertexes: ViewRectVertexes,
opts: { borderColor: string; borderWidth: number; lineDash: number[] }
opts: { stroke: string; strokeWidth: number; lineDash: number[] }
) {
const { borderColor, borderWidth, lineDash } = opts;
const { stroke, strokeWidth, lineDash } = opts;
ctx.setLineDash([]);
ctx.lineWidth = borderWidth;
ctx.strokeStyle = borderColor;
ctx.lineWidth = strokeWidth;
ctx.strokeStyle = stroke;
// ctx.fillStyle = background;
ctx.setLineDash(lineDash);
ctx.beginPath();
@ -115,10 +65,10 @@ export function drawCrossVertexes(
export function drawCrossByCenter(
ctx: ViewContext2D,
center: PointSize,
opts: { size: number; borderColor: string; borderWidth: number; lineDash: number[] }
center: Point,
opts: { size: number; stroke: string; strokeWidth: number; lineDash: number[] }
) {
const { size, borderColor, borderWidth, lineDash } = opts;
const { size, stroke, strokeWidth, lineDash } = opts;
const minX = center.x - size / 2;
const maxX = center.x + size / 2;
const minY = center.y - size / 2;
@ -126,24 +76,24 @@ export function drawCrossByCenter(
const vertexes: ViewRectVertexes = [
{
x: minX,
y: minY
y: minY,
},
{
x: maxX,
y: minY
y: minY,
},
{
x: maxX,
y: maxY
y: maxY,
},
{
x: minX,
y: maxY
}
y: maxY,
},
];
drawCrossVertexes(ctx, vertexes, {
borderColor,
borderWidth,
lineDash
stroke,
strokeWidth,
lineDash,
});
}

View file

@ -1,49 +0,0 @@
import type { ViewRectVertexes, ElementSizeController, ViewContext2D, ViewSizeInfo, ViewScaleInfo } from '@idraw/types';
import { calcViewPointSize } from '@idraw/util';
function drawDebugControllerVertexes(opts: {
ctx: ViewContext2D;
vertexes: ViewRectVertexes;
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
}): boolean {
const { ctx, viewScaleInfo, vertexes } = opts;
const v0 = calcViewPointSize(vertexes[0], { viewScaleInfo });
const v1 = calcViewPointSize(vertexes[1], { viewScaleInfo });
const v2 = calcViewPointSize(vertexes[2], { viewScaleInfo });
const v3 = calcViewPointSize(vertexes[3], { viewScaleInfo });
ctx.beginPath();
ctx.fillStyle = '#FF0000A1';
ctx.moveTo(v0.x, v0.y);
ctx.lineTo(v1.x, v1.y);
ctx.lineTo(v2.x, v2.y);
ctx.lineTo(v3.x, v3.y);
ctx.lineTo(v0.x, v0.y);
ctx.closePath();
ctx.fill('nonzero');
return false;
}
export function drawDebugStoreSelectedElementController(
ctx: ViewContext2D,
controller: ElementSizeController | null,
opts: {
viewSizeInfo: ViewSizeInfo;
viewScaleInfo: ViewScaleInfo;
}
) {
if (!controller) {
return;
}
const { viewSizeInfo, viewScaleInfo } = opts;
const { left, right, top, bottom, topLeft, topRight, bottomLeft, bottomRight, rotate } = controller;
const ctrls = [left, right, top, bottom, topLeft, topRight, bottomLeft, bottomRight, rotate];
for (let i = 0; i < ctrls.length; i++) {
const ctrl = ctrls[i];
drawDebugControllerVertexes({ ctx, vertexes: ctrl.vertexes, viewSizeInfo, viewScaleInfo });
}
}

View file

@ -1,21 +1,20 @@
import type { ViewContext2D, PointSize } from '@idraw/types';
import { MiddlewareSelectorStyle } from './types';
import type { ViewContext2D, Point, MiddlewareSelectorStyles } from '@idraw/types';
import { drawLine, drawCrossByCenter } from './draw-base';
export function drawReferenceLines(
ctx: ViewContext2D,
opts: {
xLines?: Array<PointSize[]>;
yLines?: Array<PointSize[]>;
style: MiddlewareSelectorStyle;
xLines?: Array<Point[]>;
yLines?: Array<Point[]>;
styles: MiddlewareSelectorStyles;
}
) {
const { xLines, yLines, style } = opts;
const { referenceColor } = style;
const { xLines, yLines, styles } = opts;
const { referenceColor } = styles;
const lineOpts = {
borderColor: referenceColor,
borderWidth: 1,
lineDash: []
stroke: referenceColor,
strokeWidth: 1,
lineDash: [],
};
const crossOpts = { ...lineOpts, size: 6 };

View file

@ -1,244 +0,0 @@
import type {
Element,
ElementType,
PointSize,
RendererDrawElementOptions,
ViewContext2D,
ViewRectVertexes,
ViewScaleInfo,
ViewSizeInfo,
ElementSizeController,
ViewCalculator,
MiddlewareSelectorStyle
} from '@idraw/types';
import { rotateElementVertexes, calcViewPointSize, calcViewVertexes, calcViewElementSize } from '@idraw/util';
import type { AreaSize } from './types';
import { resizeControllerBorderWidth, areaBorderWidth, selectWrapperBorderWidth } from './config';
import { drawVertexes, drawCircleController, drawCrossVertexes } from './draw-base';
// import { drawAuxiliaryExperimentBox } from './draw-auxiliary';
export function drawHoverVertexesWrapper(
ctx: ViewContext2D,
vertexes: ViewRectVertexes | null,
opts: {
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
style: MiddlewareSelectorStyle;
}
) {
if (!vertexes) {
return;
}
const { style } = opts;
const { activeColor } = style;
const wrapperOpts = { borderColor: activeColor, borderWidth: 1, background: 'transparent', lineDash: [] };
drawVertexes(ctx, calcViewVertexes(vertexes, opts), wrapperOpts);
}
export function drawLockedVertexesWrapper(
ctx: ViewContext2D,
vertexes: ViewRectVertexes | null,
opts: {
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
controller?: ElementSizeController | null;
style: MiddlewareSelectorStyle;
}
) {
if (!vertexes) {
return;
}
const { style } = opts;
const { lockedColor } = style;
const wrapperOpts = { borderColor: lockedColor, borderWidth: 1, background: 'transparent', lineDash: [] };
drawVertexes(ctx, calcViewVertexes(vertexes, opts), wrapperOpts);
const { controller } = opts;
if (controller) {
const { topLeft, topRight, bottomLeft, bottomRight, topMiddle, bottomMiddle, leftMiddle, rightMiddle } = controller;
const ctrlOpts = { ...wrapperOpts, borderWidth: 1, background: lockedColor };
drawCrossVertexes(ctx, calcViewVertexes(topMiddle.vertexes, opts), ctrlOpts);
drawCrossVertexes(ctx, calcViewVertexes(bottomMiddle.vertexes, opts), ctrlOpts);
drawCrossVertexes(ctx, calcViewVertexes(leftMiddle.vertexes, opts), ctrlOpts);
drawCrossVertexes(ctx, calcViewVertexes(rightMiddle.vertexes, opts), ctrlOpts);
drawCrossVertexes(ctx, calcViewVertexes(topLeft.vertexes, opts), ctrlOpts);
drawCrossVertexes(ctx, calcViewVertexes(topRight.vertexes, opts), ctrlOpts);
drawCrossVertexes(ctx, calcViewVertexes(bottomLeft.vertexes, opts), ctrlOpts);
drawCrossVertexes(ctx, calcViewVertexes(bottomRight.vertexes, opts), ctrlOpts);
}
}
export function drawSelectedElementControllersVertexes(
ctx: ViewContext2D,
controller: ElementSizeController | null,
opts: {
hideControllers: boolean;
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
element: Element | null;
calculator: ViewCalculator;
style: MiddlewareSelectorStyle;
rotateControllerPattern: ViewContext2D;
}
) {
if (!controller) {
return;
}
const {
hideControllers,
style,
rotateControllerPattern,
viewSizeInfo,
element
// calculator, viewScaleInfo, viewSizeInfo
} = opts;
const { devicePixelRatio = 1 } = viewSizeInfo;
const { activeColor } = style;
const { elementWrapper, topLeft, topRight, bottomLeft, bottomRight, rotate } = controller;
const wrapperOpts = {
borderColor: activeColor,
borderWidth: selectWrapperBorderWidth,
background: 'transparent',
lineDash: []
};
const ctrlOpts = { ...wrapperOpts, borderWidth: resizeControllerBorderWidth, background: '#FFFFFF' };
drawVertexes(ctx, calcViewVertexes(elementWrapper, opts), wrapperOpts);
// drawVertexes(ctx, calcViewVertexes(left.vertexes, opts), ctrlOpts);
// drawVertexes(ctx, calcViewVertexes(right.vertexes, opts), ctrlOpts);
// drawVertexes(ctx, calcViewVertexes(top.vertexes, opts), ctrlOpts);
// drawVertexes(ctx, calcViewVertexes(bottom.vertexes, opts), ctrlOpts);
if (!hideControllers) {
// drawLine(ctx, calcViewPointSize(top.center, opts), calcViewPointSize(rotate.center, opts), { ...ctrlOpts, borderWidth: 2 });
drawVertexes(ctx, calcViewVertexes(topLeft.vertexes, opts), ctrlOpts);
drawVertexes(ctx, calcViewVertexes(topRight.vertexes, opts), ctrlOpts);
drawVertexes(ctx, calcViewVertexes(bottomLeft.vertexes, opts), ctrlOpts);
drawVertexes(ctx, calcViewVertexes(bottomRight.vertexes, opts), ctrlOpts);
if (element?.operations?.rotatable !== false) {
drawCircleController(ctx, calcViewPointSize(rotate.center, opts), {
...ctrlOpts,
size: rotate.size,
borderWidth: 0
});
const rotateCenter = calcViewPointSize(rotate.center, opts);
ctx.drawImage(
rotateControllerPattern.canvas,
0,
0,
rotateControllerPattern.canvas.width / devicePixelRatio,
rotateControllerPattern.canvas.height / devicePixelRatio,
rotateCenter.x - rotate.size / 2,
rotateCenter.y - rotate.size / 2,
rotate.size,
rotate.size
);
}
}
// drawAuxiliaryExperimentBox(ctx, {
// calculator,
// element,
// viewScaleInfo,
// viewSizeInfo
// });
}
export function drawElementListShadows(
ctx: ViewContext2D,
elements: Element<ElementType>[],
opts?: Omit<RendererDrawElementOptions, 'loader'>
) {
elements.forEach((elem) => {
let { x, y, w, h } = elem;
const { angle = 0 } = elem;
if (opts?.calculator) {
const size = calcViewElementSize({ x, y, w, h }, opts);
x = size.x;
y = size.y;
w = size.w;
h = size.h;
}
const vertexes = rotateElementVertexes({ x, y, w, h, angle });
if (vertexes.length >= 2) {
ctx.setLineDash([]);
ctx.lineWidth = 1;
ctx.strokeStyle = '#aaaaaa';
ctx.fillStyle = '#0000001A';
ctx.beginPath();
ctx.moveTo(vertexes[0].x, vertexes[0].y);
for (let i = 0; i < vertexes.length; i++) {
const p = vertexes[i];
ctx.lineTo(p.x, p.y);
}
ctx.closePath();
ctx.stroke();
ctx.fill('nonzero');
}
});
}
export function drawArea(
ctx: ViewContext2D,
opts: { start: PointSize; end: PointSize; style: MiddlewareSelectorStyle }
) {
const { start, end, style } = opts;
const { activeColor, activeAreaColor } = style;
ctx.setLineDash([]);
ctx.lineWidth = areaBorderWidth;
ctx.strokeStyle = activeColor;
ctx.fillStyle = activeAreaColor;
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.lineTo(start.x, end.y);
ctx.closePath();
ctx.stroke();
ctx.fill('nonzero');
}
export function drawListArea(ctx: ViewContext2D, opts: { areaSize: AreaSize; style: MiddlewareSelectorStyle }) {
const { areaSize, style } = opts;
const { activeColor, activeAreaColor } = style;
const { x, y, w, h } = areaSize;
ctx.setLineDash([]);
ctx.lineWidth = areaBorderWidth;
ctx.strokeStyle = activeColor;
ctx.fillStyle = activeAreaColor;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + w, y);
ctx.lineTo(x + w, y + h);
ctx.lineTo(x, y + h);
ctx.closePath();
ctx.stroke();
ctx.fill('nonzero');
}
export function drawGroupQueueVertexesWrappers(
ctx: ViewContext2D,
vertexesList: ViewRectVertexes[],
opts: {
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
style: MiddlewareSelectorStyle;
}
) {
const { style } = opts;
const { activeColor } = style;
for (let i = 0; i < vertexesList.length; i++) {
const vertexes = vertexesList[i];
const wrapperOpts = {
borderColor: activeColor,
borderWidth: selectWrapperBorderWidth,
background: 'transparent',
lineDash: [4, 4]
};
drawVertexes(ctx, calcViewVertexes(vertexes, opts), wrapperOpts);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,102 +0,0 @@
import type { Element } from '@idraw/types';
import { createUUID } from '@idraw/util';
export const createIconRotate = (opts?: { fill?: string }) => {
const iconRotate: Element<'path'> = {
uuid: createUUID(),
type: 'path',
x: 0,
y: 0,
w: 200,
h: 200,
detail: {
commands: [
{
type: 'M',
params: [512, 0]
},
{
type: 'c',
params: [282.8, 0, 512, 229.2, 512, 512]
},
{
type: 's',
params: [-229.2, 512, -512, 512]
},
{
type: 'S',
params: [0, 794.8, 0, 512, 229.2, 0, 512, 0]
},
{
type: 'z',
params: []
},
{
type: 'm',
params: [309.8, 253.8]
},
{
type: 'c',
params: [0, -10.5, -6.5, -19.8, -15.7, -23.8, -9.7, -4, -21, -2, -28.2, 5.6]
},
{
type: 'l',
params: [-52.5, 52]
},
{
type: 'c',
params: [
-56.9, -53.7, -133.9, -85.5, -213.4, -85.5, -170.7, 0, -309.8, 139.2, -309.8, 309.8, 0, 170.6, 139.2, 309.8, 309.8, 309.8, 92.4, 0, 179.5, -40.8,
238.4, -111.8, 4, -5.2, 4, -12.9, -0.8, -17.3
]
},
{
type: 'L',
params: [694.3, 637]
},
{
type: 'c',
params: [
-2.8, -2.4, -6.5, -3.6, -10.1, -3.6, -3.6, 0.4, -7.3, 2, -9.3, 4.8, -39.5, 51.2, -98.8, 80.3, -163, 80.3, -113.8, 0, -206.5, -92.8, -206.5, -206.5,
0, -113.8, 92.8, -206.5, 206.5, -206.5, 52.8, 0, 102.9, 20.2, 140.8, 55.3
]
},
{
type: 'L',
params: [597, 416.5]
},
{
type: 'c',
params: [-7.7, 7.3, -9.7, 18.6, -5.6, 27.9, 4, 9.7, 13.3, 16.1, 23.8, 16.1]
},
{
type: 'H',
params: [796]
},
{
type: 'c',
params: [14.1, 0, 25.8, -11.7, 25.8, -25.8]
},
{
type: 'V',
params: [253.8]
},
{
type: 'z',
params: []
}
],
fill: '#2c2c2c',
stroke: 'transparent',
strokeWidth: 0,
originX: 0,
originY: 0,
originW: 1024,
originH: 1024,
opacity: 1,
...opts
}
};
return iconRotate;
};

View file

@ -1,47 +0,0 @@
import type { ViewContext2D } from '@idraw/types';
import { createOffscreenContext2D } from '@idraw/util';
import { drawElement } from '@idraw/renderer';
import { createIconRotate } from './icon-rotate';
export function createRotateControllerPattern(opts: { fill: string; devicePixelRatio: number }): { context2d: ViewContext2D; fill: string } {
const { fill, devicePixelRatio } = opts;
const iconRotate = createIconRotate({ fill });
const { w, h } = iconRotate;
const context2d = createOffscreenContext2D({
width: w,
height: h,
devicePixelRatio
});
// context2d.fillStyle = 'red'; // TODO
// context2d.fillRect(0, 0, size, size);
drawElement(context2d, iconRotate, {
loader: undefined as any,
viewScaleInfo: {
scale: 1,
offsetTop: 0,
offsetBottom: 0,
offsetLeft: 0,
offsetRight: 0
},
viewSizeInfo: {
width: w,
height: h,
devicePixelRatio,
contextWidth: w,
contextHeight: h
},
parentElementSize: {
x: 0,
y: 0,
w,
h
},
parentOpacity: 1
});
// context2d.fill = fill;
return { context2d, fill };
}

View file

@ -1,4 +1,13 @@
import type { Data, Element, PointSize, ViewRectInfo, ViewScaleInfo, ViewSizeInfo, ViewCalculator } from '@idraw/types';
import type {
Data,
Material,
StrictMaterial,
Point,
BoundingInfo,
ViewScaleInfo,
ViewSizeInfo,
ViewCalculator,
} from '@idraw/types';
import { is } from '@idraw/util';
type DotMap = Record<number, number[]>;
@ -24,14 +33,14 @@ interface ViewBoxInfo {
const unitSize = 2; // px
function getViewBoxInfo(rectInfo: ViewRectInfo): ViewBoxInfo {
function getViewBoxInfo(boundingBox: BoundingInfo): ViewBoxInfo {
const boxInfo: ViewBoxInfo = {
minX: rectInfo.topLeft.x,
minY: rectInfo.topLeft.y,
maxX: rectInfo.bottomRight.x,
maxY: rectInfo.bottomRight.y,
midX: rectInfo.center.x,
midY: rectInfo.center.y
minX: boundingBox.topLeft.x,
minY: boundingBox.topLeft.y,
maxX: boundingBox.bottomRight.x,
maxY: boundingBox.bottomRight.y,
midX: boundingBox.center.x,
midY: boundingBox.center.y,
};
return boxInfo;
}
@ -66,187 +75,45 @@ const getClosestNumInSortedKeys = (sortedKeys: number[], target: number) => {
return sortedKeys[left];
}
return Math.abs(sortedKeys[right] - target) <= Math.abs(sortedKeys[left] - target) ? sortedKeys[right] : sortedKeys[left];
return Math.abs(sortedKeys[right] - target) <= Math.abs(sortedKeys[left] - target)
? sortedKeys[right]
: sortedKeys[left];
};
const isEqualNum = (a: number, b: number) => Math.abs(a - b) < 0.00001;
// export function calcSnapOffsetInfo(
// uuid: string,
// opts: {
// data: Data;
// groupQueue: Element<'group'>[];
// calculator: ViewCalculator;
// viewScaleInfo: ViewScaleInfo;
// viewSizeInfo: ViewSizeInfo;
// }
// ) {
// const { data, groupQueue, calculator, viewScaleInfo, viewSizeInfo } = opts;
// let targetElements: Element[] = data.elements || [];
// if (groupQueue?.length > 0) {
// targetElements = (groupQueue[groupQueue.length - 1] as Element<'group'>)?.detail?.children || [];
// }
// const siblingViewRectInfoList: ViewRectInfo[] = [];
// targetElements.forEach((elem: Element) => {
// if (elem.uuid !== uuid) {
// const info = calculator.calcViewRectInfoFromRange(elem.uuid, { checkVisible: true, viewScaleInfo, viewSizeInfo });
// if (info) {
// siblingViewRectInfoList.push(info);
// }
// }
// });
// const targetRectInfo = calculator.calcViewRectInfoFromRange(uuid, { viewScaleInfo, viewSizeInfo });
// if (!targetRectInfo) {
// return null;
// }
// const vTargetLineDotMap: DotMap = {}; // target vertical line dots
// const hTargetLineDotMap: DotMap = {}; // target horizontal line dots
// const vRefLineDotMap: DotMap = {}; // reference vertical line dots
// const hRefLineDotMap: DotMap = {}; // reference horizontal line dots
// let sortedRefXKeys: number[] = []; // hRefLineDotMap key nums
// let sortedRefYKeys: number[] = []; // vRefLineDotMap key nums
// const targetBox = getViewBoxInfo(targetRectInfo);
// vTargetLineDotMap[targetBox.minX] = [targetBox.minY, targetBox.midY, targetBox.maxY];
// vTargetLineDotMap[targetBox.midX] = [targetBox.minY, targetBox.midY, targetBox.maxY];
// vTargetLineDotMap[targetBox.maxX] = [targetBox.minY, targetBox.midY, targetBox.maxY];
// hTargetLineDotMap[targetBox.minY] = [targetBox.minX, targetBox.midX, targetBox.maxX];
// hTargetLineDotMap[targetBox.midY] = [targetBox.minX, targetBox.midX, targetBox.maxX];
// hTargetLineDotMap[targetBox.maxY] = [targetBox.minX, targetBox.midX, targetBox.maxX];
// siblingViewRectInfoList.forEach((info) => {
// const box = getViewBoxInfo(info);
// if (!vRefLineDotMap[box.minX]) {
// vRefLineDotMap[box.minX] = [];
// }
// if (!vRefLineDotMap[box.midX]) {
// vRefLineDotMap[box.midX] = [];
// }
// if (!vRefLineDotMap[box.maxX]) {
// vRefLineDotMap[box.maxX] = [];
// }
// if (!hRefLineDotMap[box.minY]) {
// hRefLineDotMap[box.minY] = [];
// }
// if (!hRefLineDotMap[box.midY]) {
// hRefLineDotMap[box.midY] = [];
// }
// if (!hRefLineDotMap[box.maxY]) {
// hRefLineDotMap[box.maxY] = [];
// }
// vRefLineDotMap[box.minX] = [box.minY, box.midY, box.maxY];
// vRefLineDotMap[box.midX] = [box.minY, box.midY, box.maxY];
// vRefLineDotMap[box.maxX] = [box.minY, box.midY, box.maxY];
// sortedRefXKeys.push(box.minX);
// sortedRefXKeys.push(box.midX);
// sortedRefXKeys.push(box.maxX);
// hRefLineDotMap[box.minY] = [box.minX, box.midX, box.maxX];
// hRefLineDotMap[box.midY] = [box.minX, box.midX, box.maxX];
// hRefLineDotMap[box.maxY] = [box.minX, box.midX, box.maxX];
// sortedRefYKeys.push(box.minY);
// sortedRefYKeys.push(box.midY);
// sortedRefYKeys.push(box.maxY);
// });
// sortedRefXKeys = sortedRefXKeys.sort((a, b) => a - b);
// sortedRefYKeys = sortedRefYKeys.sort((a, b) => a - b);
// let offsetX: number | null = null;
// let offsetY: number | null = null;
// let closestMinX: number | null = null;
// let closestMidX: number | null = null;
// let closestMaxX: number | null = null;
// let closestMinY: number | null = null;
// let closestMidY: number | null = null;
// let closestMaxY: number | null = null;
// if (sortedRefXKeys.length > 0) {
// closestMinX = getClosestNumInSortedKeys(sortedRefXKeys, targetBox.minX);
// closestMidX = getClosestNumInSortedKeys(sortedRefXKeys, targetBox.midX);
// closestMaxX = getClosestNumInSortedKeys(sortedRefXKeys, targetBox.maxX);
// const distMinX = Math.abs(closestMinX - targetBox.minX);
// const distMidX = Math.abs(closestMidX - targetBox.midX);
// const distMaxX = Math.abs(closestMaxX - targetBox.maxX);
// const closestXDist = Math.min(distMinX, distMidX, distMaxX);
// if (closestXDist <= unitSize / viewScaleInfo.scale) {
// if (isEqualNum(closestXDist, distMinX)) {
// offsetX = closestMinX - targetBox.minX;
// } else if (isEqualNum(closestXDist, distMidX)) {
// offsetX = closestMidX - targetBox.midX;
// } else if (isEqualNum(closestXDist, distMaxX)) {
// offsetX = closestMaxX - targetBox.maxX;
// }
// }
// }
// if (sortedRefYKeys.length > 0) {
// closestMinY = getClosestNumInSortedKeys(sortedRefYKeys, targetBox.minY);
// closestMidY = getClosestNumInSortedKeys(sortedRefYKeys, targetBox.midY);
// closestMaxY = getClosestNumInSortedKeys(sortedRefYKeys, targetBox.maxY);
// const distMinY = Math.abs(closestMinY - targetBox.minY);
// const distMidY = Math.abs(closestMidY - targetBox.midY);
// const distMaxY = Math.abs(closestMaxY - targetBox.maxY);
// const closestYDist = Math.min(distMinY, distMidY, distMaxY);
// if (closestYDist <= unitSize / viewScaleInfo.scale) {
// if (isEqualNum(closestYDist, distMinY)) {
// offsetY = closestMinY - targetBox.minY;
// } else if (isEqualNum(closestYDist, distMidY)) {
// offsetY = closestMidY - targetBox.midY;
// } else if (isEqualNum(closestYDist, distMaxY)) {
// offsetY = closestMaxY - targetBox.maxY;
// }
// }
// }
// return {
// offsetX,
// offsetY
// };
// }
export function calcReferenceInfo(
uuid: string,
id: string,
opts: {
data: Data;
groupQueue: Element<'group'>[];
groupQueue: StrictMaterial<'group'>[];
calculator: ViewCalculator;
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
}
) {
const { data, groupQueue, calculator, viewScaleInfo, viewSizeInfo } = opts;
let targetElements: Element[] = data.elements || [];
let targetMaterials: Material[] = data.materials || [];
if (groupQueue?.length > 0) {
targetElements = (groupQueue[groupQueue.length - 1] as Element<'group'>)?.detail?.children || [];
targetMaterials = (groupQueue[groupQueue.length - 1] as StrictMaterial<'group'>)?.children || [];
}
const siblingViewRectInfoList: ViewRectInfo[] = [];
targetElements.forEach((elem: Element) => {
if (elem.uuid !== uuid) {
const info = calculator.calcViewRectInfoFromRange(elem.uuid, { checkVisible: true, viewScaleInfo, viewSizeInfo });
const siblingBoundingInfoList: BoundingInfo[] = [];
targetMaterials.forEach((mtrl: Material) => {
if (mtrl.id !== id) {
const info = calculator.calcViewBoundingInfoFromRange(mtrl.id, {
checkVisible: true,
viewScaleInfo,
viewSizeInfo,
});
if (info) {
siblingViewRectInfoList.push(info);
siblingBoundingInfoList.push(info);
}
}
});
const targetRectInfo = calculator.calcViewRectInfoFromRange(uuid, { viewScaleInfo, viewSizeInfo });
const targetBoundingBox = calculator.calcViewBoundingInfoFromRange(id, { viewScaleInfo, viewSizeInfo });
if (!targetRectInfo) {
if (!targetBoundingBox) {
return null;
}
@ -262,7 +129,7 @@ export function calcReferenceInfo(
let sortedRefXKeys: number[] = []; // hRefLineDotMap key nums
let sortedRefYKeys: number[] = []; // vRefLineDotMap key nums
const targetBox = getViewBoxInfo(targetRectInfo);
const targetBox = getViewBoxInfo(targetBoundingBox);
vTargetLineDotMap[targetBox.minX] = [targetBox.minY, targetBox.midY, targetBox.maxY];
vTargetLineDotMap[targetBox.midX] = [targetBox.minY, targetBox.midY, targetBox.maxY];
@ -272,7 +139,7 @@ export function calcReferenceInfo(
hTargetLineDotMap[targetBox.midY] = [targetBox.minX, targetBox.midX, targetBox.maxX];
hTargetLineDotMap[targetBox.maxY] = [targetBox.minX, targetBox.midX, targetBox.maxX];
siblingViewRectInfoList.forEach((info) => {
siblingBoundingInfoList.forEach((info) => {
const box = getViewBoxInfo(info);
if (!vRefLineDotMap[box.minX]) {
vRefLineDotMap[box.minX] = [];
@ -380,7 +247,7 @@ export function calcReferenceInfo(
if (isEqualNum(offsetX, closestMinX - targetBox.minX)) {
const vLine: YLine = {
x: closestMinX,
yList: []
yList: [],
};
vLine.yList.push(newTargetBox.minY);
// vLine.yList.push(newTargetBox.midY);
@ -392,7 +259,7 @@ export function calcReferenceInfo(
if (isEqualNum(offsetX, closestMidX - targetBox.minX)) {
const vLine: YLine = {
x: closestMidX,
yList: []
yList: [],
};
vLine.yList.push(newTargetBox.minY);
// vLine.yList.push(newTargetBox.midY);
@ -404,7 +271,7 @@ export function calcReferenceInfo(
if (isEqualNum(offsetX, closestMaxX - targetBox.minX)) {
const vLine: YLine = {
x: closestMaxX,
yList: []
yList: [],
};
vLine.yList.push(newTargetBox.minY);
// vLine.yList.push(newTargetBox.midY);
@ -418,7 +285,7 @@ export function calcReferenceInfo(
if (isEqualNum(offsetY, closestMinY - targetBox.minY)) {
const hLine: XLine = {
y: closestMinY,
xList: []
xList: [],
};
hLine.xList.push(newTargetBox.minX);
// hLine.xList.push(newTargetBox.midX);
@ -429,7 +296,7 @@ export function calcReferenceInfo(
if (isEqualNum(offsetY, closestMidY - targetBox.midY)) {
const hLine: XLine = {
y: closestMidY,
xList: []
xList: [],
};
hLine.xList.push(newTargetBox.minX);
// hLine.xList.push(newTargetBox.midX);
@ -440,7 +307,7 @@ export function calcReferenceInfo(
if (isEqualNum(offsetY, closestMaxY - targetBox.maxY)) {
const hLine: XLine = {
y: closestMaxY,
xList: []
xList: [],
};
hLine.xList.push(newTargetBox.minX);
// hLine.xList.push(newTargetBox.midX);
@ -450,27 +317,27 @@ export function calcReferenceInfo(
}
}
const yLines: Array<PointSize[]> = [];
const yLines: Array<Point[]> = [];
if (vHelperLineDotMapList?.length > 0) {
vHelperLineDotMapList.forEach((item, i) => {
yLines.push([]);
item.yList.forEach((y) => {
yLines[i].push({
x: item.x,
y
y,
});
});
});
}
const xLines: Array<PointSize[]> = [];
const xLines: Array<Point[]> = [];
if (hHelperLineDotMapList?.length > 0) {
hHelperLineDotMapList.forEach((item, i) => {
xLines.push([]);
item.xList.forEach((x) => {
xLines[i].push({
x,
y: item.y
y: item.y,
});
});
});
@ -480,6 +347,6 @@ export function calcReferenceInfo(
offsetX,
offsetY,
yLines,
xLines
xLines,
};
}

View file

@ -0,0 +1,175 @@
import type {
Data,
StrictMaterial,
Material,
RenderMaterialHelperOptions,
BoardViewerFrameSnapshot,
BoardMiddlewareOptions,
MiddlewareSelectorStyles,
} from '@idraw/types';
import type { Point, ActionType, DeepSelectorSharedStorage } from './types';
import { drawReferenceLines } from './draw-reference';
import { calcSelectedMaterialsArea } from './util';
import {
// legacy
keyActionType,
keyResizeType,
keyAreaStart,
keyAreaEnd,
keyGroupQueue,
keyHoverMaterial,
keySelectedMaterialList,
keyEnableSnapToGrid,
} from './static';
import { calcReferenceInfo } from './reference';
import {
renderMaterialHoverBox,
clearMaterialHoverBox,
renderMaterialLockedBox,
clearMaterialLockedBox,
resetMaterialNestedBox,
resetMaterialSelectedBox,
resetMaterialSelectionAreaBox,
} from './dom';
export { keySelectedMaterialList, keyHoverMaterial, keyActionType, keyResizeType, keyGroupQueue };
export type { DeepSelectorSharedStorage, ActionType };
export type RenderFrameOptions = Pick<
BoardMiddlewareOptions<DeepSelectorSharedStorage>,
'sharer' | 'calculator' | 'boardContent'
> & {
snapshot: BoardViewerFrameSnapshot<DeepSelectorSharedStorage>;
$root: HTMLDivElement | null;
styles: MiddlewareSelectorStyles;
};
export function renderFrame({ $root, styles, snapshot, sharer, calculator, boardContent }: RenderFrameOptions) {
const { activeStore, sharedStore } = snapshot;
const { overlayContext } = boardContent;
const {
scale,
offsetLeft,
offsetTop,
offsetRight,
offsetBottom,
width,
height,
contextHeight,
contextWidth,
devicePixelRatio,
} = activeStore;
const viewScaleInfo = { scale, offsetLeft, offsetTop, offsetRight, offsetBottom };
const viewSizeInfo = { width, height, contextHeight, contextWidth, devicePixelRatio };
const selectedMaterials = sharedStore[keySelectedMaterialList];
const mtrl = selectedMaterials[0];
const hoverMaterial: Material = sharedStore[keyHoverMaterial] as Material;
const actionType: ActionType = sharedStore[keyActionType] as ActionType;
const areaStart: Point | null = sharedStore[keyAreaStart];
const areaEnd: Point | null = sharedStore[keyAreaEnd];
const groupQueue: StrictMaterial<'group'>[] = sharedStore[keyGroupQueue];
const enableSnapToGrid = sharedStore[keyEnableSnapToGrid];
const isHoverLocked: boolean = !!hoverMaterial?.operations?.locked;
const helperOpts: RenderMaterialHelperOptions = {
material: null,
groupQueue: groupQueue || [],
viewScaleInfo,
viewSizeInfo,
calculator,
};
resetMaterialNestedBox($root, helperOpts);
clearMaterialHoverBox($root);
clearMaterialLockedBox($root);
if (hoverMaterial && hoverMaterial?.id !== selectedMaterials[0]?.id) {
// hover
helperOpts.material = hoverMaterial;
if (isHoverLocked) {
renderMaterialLockedBox($root, helperOpts);
} else {
renderMaterialHoverBox($root, helperOpts);
}
}
// seleced
resetMaterialSelectedBox($root, {
...helperOpts,
material: selectedMaterials.length === 1 ? selectedMaterials[0] : null,
});
// selected area
resetMaterialSelectionAreaBox($root, {
...helperOpts,
areaStart,
areaEnd,
selectedMaterials,
});
// legacy logic
if (groupQueue?.length > 0) {
// in group
if (mtrl && (['select', 'drag', 'resize'] as ActionType[]).includes(actionType)) {
if (actionType === 'drag') {
if (enableSnapToGrid === true) {
const referenceInfo = calcReferenceInfo(mtrl.id, {
calculator,
data: activeStore.data as Data,
groupQueue,
viewScaleInfo,
viewSizeInfo,
});
if (referenceInfo) {
const { offsetX, offsetY, xLines, yLines } = referenceInfo;
if (offsetX === 0 || offsetY === 0) {
drawReferenceLines(overlayContext, {
xLines,
yLines,
styles,
});
}
}
}
}
}
} else {
// in root
if (mtrl && (['select', 'drag', 'resize'] as ActionType[]).includes(actionType)) {
if (actionType === 'drag') {
if (enableSnapToGrid === true) {
const referenceInfo = calcReferenceInfo(mtrl.id, {
calculator,
data: activeStore.data as Data,
groupQueue,
viewScaleInfo,
viewSizeInfo,
});
if (referenceInfo) {
const { offsetX, offsetY, xLines, yLines } = referenceInfo;
if (offsetX === 0 || offsetY === 0) {
drawReferenceLines(overlayContext, {
xLines,
yLines,
styles,
});
}
}
}
}
} else if (actionType === 'area' && areaStart && areaEnd) {
// drawArea(overlayContext, { start: areaStart, end: areaEnd, style });
} else if ((['drag-list', 'drag-list-end'] as ActionType[]).includes(actionType)) {
const listAreaSize = calcSelectedMaterialsArea(sharer.getSharedStorage(keySelectedMaterialList), {
viewScaleInfo: sharer.getActiveViewScaleInfo(),
viewSizeInfo: sharer.getActiveViewSizeInfo(),
calculator,
});
if (listAreaSize) {
// drawListArea(overlayContext, { areaSize: listAreaSize, style });
}
}
}
}

View file

@ -0,0 +1,33 @@
import { scalePathCommands } from '@idraw/util';
import type { StrictMaterial, Material } from '@idraw/types';
export const dragAndResizeMaterial = (
mtrl: Material,
opts: { x: number; y: number; width: number; height: number }
) => {
const { x, y, width, height } = opts;
const prevWidth = mtrl.width;
const prevHeight = mtrl.height;
mtrl.x = x;
mtrl.y = y;
mtrl.width = width;
mtrl.height = height;
if (mtrl.type === 'circle') {
mtrl.cx = x + width / 2;
mtrl.cy = y + height / 2;
mtrl.r = Math.min(width, height) / 2;
} else if (mtrl.type === 'ellipse') {
mtrl.cx = x + width / 2;
mtrl.cy = y + height / 2;
mtrl.rx = width / 2;
mtrl.ry = height / 2;
} else if (mtrl.type === 'path') {
const scaleW = width / prevWidth;
const scaleH = height / prevHeight;
const svgMtrl = mtrl as StrictMaterial<'path'>;
svgMtrl.commands = scalePathCommands(svgMtrl.commands, scaleW, scaleH);
}
};

View file

@ -0,0 +1,129 @@
import type { MiddlewareSelectorStyles, StoreSharer } from '@idraw/types';
import { createId } from '@idraw/util';
import type { DeepSelectorSharedStorage } from './types';
export const key = 'SELECTOR';
export const prefix = `idraw-middleware-selector`;
export const getRootClassName = () => `${prefix}-${createId()}`;
// export const ATTR_MATERIAL_TYPE = 'data-idraw-material-type';
export const ATTR_BOX_TYPE = 'data-idraw-box-type';
export const ATTR_MATERIAL_ID = 'data-idraw-material-id';
export const ATTR_HANDLER_TYPE = 'data-idraw-handler-type';
export const BOX_GROUP = 'box-group';
export const BOX_TARGET = 'box-material';
export const classNameMap = {
// common material box
materialBox: `${prefix}-materialBox`,
groupBox: `${prefix}-groupBox`,
// nestedBox
nestedBox: `${prefix}-nestedBox`,
nestedTargetBox: `${prefix}-nestedTargetBox`,
// hoverBox
hoverBox: `${prefix}-hoverBox`,
hoverTargetBox: `${prefix}-hoverTargetBox`,
// lockedBox
lockedBox: `${prefix}-lockedBox`,
lockedTargetBox: `${prefix}-lockedTargetBox`,
// selected Box
selectedBox: `${prefix}-selectedBox`,
selectedTargetBox: `${prefix}-selectedTargetBox`,
// handlerBox
hideHandler: `${prefix}-hideHandler`,
// edge handler
edgeHandler: `${prefix}-edgeHandler`,
edgeTopHandler: `${prefix}-edgeTopHandler`,
edgeRightHandler: `${prefix}-edgeRightandler`,
edgeBottomHandler: `${prefix}-edgeBottomHandler`,
edgeLeftHandler: `${prefix}-edgeLeftHandler`,
// corner handler
cornerHandler: `${prefix}-cornerHandler`,
cornerTopLeftHandler: `${prefix}-cornerTopLeftHandler`,
cornerTopRightHandler: `${prefix}-cornerTopRightHandler`,
cornerBottomLeftHandler: `${prefix}-cornerBottomLeftHandler`,
cornerBottomRightHandler: `${prefix}-cornerBottomRightHandler`,
// rotate handler
rotateHandler: `${prefix}-rotateHandler`,
// selection area
selectionAreaBox: `${prefix}-selectionAreaBox`,
};
export const keyPrevPoint = Symbol(`${key}_prevPoint`); // Point | null = null;
export const keyPointStartMaterialSizeList = Symbol(`${key}_pointStartMaterialSizeList`); // Array<Partial<MaterialSize> & { id: string }> = [];
export const keyMoveOriginalStartPoint = Symbol(`${key}_moveOriginalStartPoint`); // Point | null = null;
export const keyMoveOriginalStartMaterialSize = Symbol(`${key}_moveOriginalStartMaterialSize`); // MaterialSize | null = null;
export const keyInBusyMode = Symbol(`${key}_inBusyMode`); // 'resize' | 'drag' | 'drag-list' | 'area' | null = null;
export const keyHasChangedData = Symbol(`${key}_hasChangedData`); // boolean | null = null;
export const keyStartResizeGroupRecord = Symbol(`${key}_startResizeGroupRecord`); // ModifyRecord<'resizeMaterials'> | null = null;
export const keyEndResizeGroupRecord = Symbol(`${key}_endResizeGroupRecord`); // ModifyRecord<'resizeMaterials'> | null = null;
export const keyActionType = Symbol(`${key}_actionType`); // 'select' | 'drag-list' | 'drag-list-end' | 'drag' | 'hover' | 'resize' | 'area' | null = null;
export const keyResizeType = Symbol(`${key}_resizeType`); // ResizeType | null;
export const keyAreaStart = Symbol(`${key}_areaStart`); // Point
export const keyAreaEnd = Symbol(`${key}_areaEnd`); // Point
export const keyHoverMaterial = Symbol(`${key}_hoverMaterial`); // Material | []
export const keySelectedMaterialList = Symbol(`${key}_selectedMaterialList`); // Array<Material<MaterialType>> | []
export const keySelectedMaterialListVertexes = Symbol(`${key}_selectedMaterialListVertexes`); // ViewRectVertexes | null
export const keySelectedMaterialPosition = Symbol(`${key}_selectedMaterialPosition`); // MaterialPosition | []
export const keyGroupQueue = Symbol(`${key}_groupQueue`); // Array<Material<'group'>> | []
export const keyIsMoving = Symbol(`${key}_isMoving`); // boolean | null
export const keyEnableSelectInGroup = Symbol(`${key}_enableSelectInGroup`);
export const keyEnableSnapToGrid = Symbol(`${key}_enableSnapToGrid`);
export const selectedBoxBorderWidth = 1.5;
export const selectedNestedBoxBorderWidth = 2;
export const hoverBoxBorderWidth = 1;
export const lockedBoxBorderWidth = 2;
export const cornerHandlerSize = 10;
export const cornerHandlerBorderWidth = 1.5;
export const edgeHandlerSize = 10;
export const selectionAreaBorderWidth = 1;
export const rotateHandlerSize = 20;
export const defaultStyle: MiddlewareSelectorStyles = {
zIndex: 1,
activeColor: '#1973ba',
handlerBorderColor: '#1973ba',
handlerBackground: '#ffffff',
handlerHoverBackground: '#aad4f6',
handlerActiveBackground: '#63b8f8',
selectionAreaBorderColor: '#1973ba',
selectionAreaBackground: '#1973ba3f',
lockedColor: '#5b5959b5',
referenceColor: '#f7276e',
};
export const getSvgRotate = (
currentColor: string
) => `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" fill="${currentColor}" >
<path d="M512 0c282.8 0 512 229.2 512 512s-229.2 512 -512 512S0 794.8 0 512 229.2 0 512 0zm309.8 253.8c0 -10.5 -6.5 -19.8 -15.7 -23.8 -9.7 -4 -21 -2 -28.2 5.6l-52.5 52c-56.9 -53.7 -133.9 -85.5 -213.4 -85.5 -170.7 0 -309.8 139.2 -309.8 309.8 0 170.6 139.2 309.8 309.8 309.8 92.4 0 179.5 -40.8 238.4 -111.8 4 -5.2 4 -12.9 -0.8 -17.3L694.3 637c-2.8 -2.4 -6.5 -3.6 -10.1 -3.6 -3.6 0.4 -7.3 2 -9.3 4.8 -39.5 51.2 -98.8 80.3 -163 80.3 -113.8 0 -206.5 -92.8 -206.5 -206.5 0 -113.8 92.8 -206.5 206.5 -206.5 52.8 0 102.9 20.2 140.8 55.3L597 416.5c-7.7 7.3 -9.7 18.6 -5.6 27.9 4 9.7 13.3 16.1 23.8 16.1H796c14.1 0 25.8 -11.7 25.8 -25.8V253.8z" />
</svg>`;
export const clearStorage = (sharer: StoreSharer<DeepSelectorSharedStorage>) => {
sharer.setSharedStorage(keyStartResizeGroupRecord, null);
sharer.setSharedStorage(keyEndResizeGroupRecord, null);
sharer.setSharedStorage(keyActionType, null);
sharer.setSharedStorage(keyResizeType, null);
sharer.setSharedStorage(keyAreaStart, null);
sharer.setSharedStorage(keyAreaEnd, null);
sharer.setSharedStorage(keyGroupQueue, []);
sharer.setSharedStorage(keyHoverMaterial, null);
sharer.setSharedStorage(keySelectedMaterialList, []);
sharer.setSharedStorage(keySelectedMaterialListVertexes, null);
sharer.setSharedStorage(keySelectedMaterialPosition, []);
sharer.setSharedStorage(keyIsMoving, null);
};

View file

@ -0,0 +1,186 @@
import type { MiddlewareSelectorStyles, MiddlewareSelectorConfig, StylesProps } from '@idraw/types';
import { injectStyles, removeStyles, getMiddlewareValidStyles } from '@idraw/util';
import {
classNameMap,
getSvgRotate,
selectedBoxBorderWidth,
selectedNestedBoxBorderWidth,
hoverBoxBorderWidth,
lockedBoxBorderWidth,
edgeHandlerSize,
cornerHandlerSize,
cornerHandlerBorderWidth,
selectionAreaBorderWidth,
rotateHandlerSize,
} from './static';
export function initStyles(rootClassName: string, styles: MiddlewareSelectorStyles) {
const cls = (str: string) => `.${str}`;
const stylesProps: StylesProps = {
display: 'flex',
position: 'absolute',
zIndex: styles.zIndex,
top: 0,
left: 0,
right: 0,
bottom: 0,
overflow: 'hidden',
// hover
[cls(classNameMap.hoverTargetBox)]: {
position: 'absolute',
outline: `${hoverBoxBorderWidth}px solid ${styles.activeColor}`,
},
// nested box
[cls(classNameMap.nestedBox)]: {
position: 'absolute',
[`&${cls(classNameMap.groupBox)}`]: {
outline: `${selectedNestedBoxBorderWidth}px dashed ${styles.activeColor}`,
},
[cls(classNameMap.groupBox)]: {
outline: `${selectedNestedBoxBorderWidth}px dashed ${styles.activeColor}`,
},
},
// locked box
[cls(classNameMap.lockedTargetBox)]: {
position: 'absolute',
outline: `${lockedBoxBorderWidth}px solid ${styles.lockedColor}`,
},
// selected box
[cls(classNameMap.selectedBox)]: {
position: 'absolute',
[`&${cls(classNameMap.hideHandler)}`]: {
[cls(classNameMap.cornerHandler)]: {
display: 'none',
},
[cls(classNameMap.edgeHandler)]: {
display: 'none',
},
[cls(classNameMap.rotateHandler)]: {
display: 'none',
},
},
},
[cls(classNameMap.selectedTargetBox)]: {
position: 'absolute',
outline: `${selectedBoxBorderWidth}px solid ${styles.handlerBorderColor}`,
[cls(classNameMap.cornerHandler)]: {
position: 'absolute',
outline: `${cornerHandlerBorderWidth}px solid ${styles.handlerBorderColor}`,
background: styles.handlerBackground,
width: `${cornerHandlerSize}px`,
height: `${cornerHandlerSize}px`,
['&:hover']: {
background: styles.handlerHoverBackground,
},
['&:active']: {
background: styles.handlerActiveBackground,
},
[`&${cls(classNameMap.cornerTopLeftHandler)}`]: {
top: `${-cornerHandlerSize / 2}px`,
left: `${-cornerHandlerSize / 2}px`,
},
[`&${cls(classNameMap.cornerTopRightHandler)}`]: {
top: `${-cornerHandlerSize / 2}px`,
right: `${-cornerHandlerSize / 2}px`,
},
[`&${cls(classNameMap.cornerBottomLeftHandler)}`]: {
bottom: `${-cornerHandlerSize / 2}px`,
left: `${-cornerHandlerSize / 2}px`,
},
[`&${cls(classNameMap.cornerBottomRightHandler)}`]: {
bottom: `${-cornerHandlerSize / 2}px`,
right: `${-cornerHandlerSize / 2}px`,
},
},
[cls(classNameMap.rotateHandler)]: {
position: 'absolute',
top: -40,
left: `50%`,
transform: `translateX(-50%)`,
width: rotateHandlerSize,
height: rotateHandlerSize,
background: '#FFFFFF',
borderRadius: `${rotateHandlerSize / 2}px`,
['&::after']: {
display: 'inline-block',
content: '""',
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
backgroundImage: `url(data:image/svg+xml,${encodeURIComponent(getSvgRotate(styles.activeColor))})`,
backgroundPosition: 'center',
backgroundSize: `${rotateHandlerSize}px`,
},
},
[cls(classNameMap.edgeHandler)]: {
position: 'absolute',
background: 'transparent',
[`&${cls(classNameMap.edgeLeftHandler)}`]: {
width: `${edgeHandlerSize}px`,
top: `${edgeHandlerSize / 2}px`,
left: `${-edgeHandlerSize / 2}px`,
bottom: `${edgeHandlerSize / 2}px`,
},
[`&${cls(classNameMap.edgeRightHandler)}`]: {
width: `${edgeHandlerSize}px`,
top: `${edgeHandlerSize / 2}px`,
right: `${-edgeHandlerSize / 2}px`,
bottom: `${edgeHandlerSize / 2}px`,
},
[`&${cls(classNameMap.edgeTopHandler)}`]: {
height: `${edgeHandlerSize}px`,
top: `${-edgeHandlerSize / 2}px`,
left: `${edgeHandlerSize / 2}px`,
right: `${edgeHandlerSize / 2}px`,
},
[`&${cls(classNameMap.edgeBottomHandler)}`]: {
height: `${edgeHandlerSize}px`,
bottom: `${-edgeHandlerSize / 2}px`,
left: `${edgeHandlerSize / 2}px`,
right: `${edgeHandlerSize / 2}px`,
},
},
},
// selection area box
[cls(classNameMap.selectionAreaBox)]: {
position: 'absolute',
outline: `${selectionAreaBorderWidth}px solid ${styles.selectionAreaBorderColor}`,
background: styles.selectionAreaBackground,
},
};
injectStyles({ styles: stylesProps, rootClassName, type: 'element' });
}
export function destroyStyles(rootClassName: string) {
removeStyles({ rootClassName, type: 'element' });
}
export function getMiddlewareSelectorStyles<C = MiddlewareSelectorConfig, S = MiddlewareSelectorStyles>(config: C): S {
const styles: S = getMiddlewareValidStyles<C, S>(config, [
'zIndex',
'activeColor',
'handlerBorderColor',
'handlerBackground',
'handlerHoverBackground',
'handlerActiveBackground',
'selectionAreaBackground',
'selectionAreaBorderColor',
'lockedColor',
'referenceColor',
]);
return styles;
}

View file

@ -1,69 +1,67 @@
import {
Data,
ElementSize,
ElementType,
Element,
MaterialSize,
MaterialType,
StrictMaterial,
Material,
ViewContext2D,
Point,
PointSize,
ViewScaleInfo,
ViewSizeInfo,
ViewCalculator,
PointWatcherEvent,
Middleware,
ViewRectVertexes,
ElementSizeController,
ElementPosition
MaterialPosition,
ModifyRecord,
} from '@idraw/types';
import {
keyPrevPoint,
keyPointStartMaterialSizeList,
keyMoveOriginalStartPoint,
keyMoveOriginalStartMaterialSize,
keyInBusyMode,
keyHasChangedData,
keyStartResizeGroupRecord,
keyEndResizeGroupRecord,
// legacy
keyActionType,
keyResizeType,
keyAreaStart,
keyAreaEnd,
keyGroupQueue,
keyGroupQueueVertexesList,
keyHoverElement,
keyHoverElementVertexes,
keySelectedElementList,
keySelectedElementListVertexes,
keySelectedElementController,
keySelectedElementPosition,
keyHoverMaterial,
keySelectedMaterialList,
keySelectedMaterialListVertexes,
keySelectedMaterialPosition,
keyIsMoving,
keyEnableSelectInGroup,
keyEnableSnapToGrid,
// debug keys
keyDebugElemCenter,
keyDebugEnd0,
keyDebugEndHorizontal,
keyDebugEndVertical,
keyDebugStartHorizontal,
keyDebugStartVertical
} from './config';
} from './static';
import { keyLayoutIsSelected, keyLayoutIsBusyMoving } from '../layout-selector';
export {
Data,
ElementType,
Element,
ElementSize,
MaterialType,
Material,
MaterialSize,
ViewContext2D,
Point,
PointSize,
ViewScaleInfo,
ViewSizeInfo,
ViewCalculator,
PointWatcherEvent,
Middleware
Middleware,
};
export type ControllerStyle = ElementSize & {
borderWidth: number;
borderColor: string;
export type ControllerStyle = MaterialSize & {
strokeWidth: number;
stroke: string;
background: string;
};
export type SelectedElementSizeController = Record<string, ControllerStyle>;
export type SelectedMaterialSizeController = Record<string, ControllerStyle>;
export type ResizeType =
| 'resize-left'
@ -76,44 +74,44 @@ export type ResizeType =
| 'resize-bottom-right'
| 'resize-rotate';
export type PointTargetType = null | ResizeType | 'list-area' | 'over-element';
export type PointTargetType = null | ResizeType | 'list-area' | 'over-material';
export interface PointTarget {
type: PointTargetType;
elements: Element<ElementType>[];
groupQueue: Element<'group'>[];
elementVertexesList: ViewRectVertexes[];
groupQueueVertexesList: ViewRectVertexes[];
materials: StrictMaterial<MaterialType>[];
groupQueue: StrictMaterial<'group'>[];
materialVertexesList: ViewRectVertexes[];
// groupQueueVertexesList: ViewRectVertexes[];
}
export type AreaSize = ElementSize;
export type AreaSize = MaterialSize;
export type ActionType = 'select' | 'drag-list' | 'drag-list-end' | 'drag' | 'hover' | 'resize' | 'area' | null;
export type DeepSelectorSharedStorage = {
[keyPrevPoint]: Point | null; // null;
[keyPointStartMaterialSizeList]: Array<Partial<MaterialSize> & { id: string }>; // [];
[keyMoveOriginalStartPoint]: Point | null; // null;
[keyMoveOriginalStartMaterialSize]: MaterialSize | null; // null;
[keyInBusyMode]: 'resize' | 'drag' | 'drag-list' | 'area' | null; // null;
[keyHasChangedData]: boolean | null; // null;
[keyStartResizeGroupRecord]: ModifyRecord<'resizeMaterials'> | null; // null;
[keyEndResizeGroupRecord]: ModifyRecord<'resizeMaterials'> | null; // null;
// legacy
[keyActionType]: ActionType | null;
[keyResizeType]: ResizeType | null;
[keyAreaStart]: Point | null;
[keyAreaEnd]: Point | null;
[keyGroupQueue]: Element<'group'>[];
[keyGroupQueueVertexesList]: ViewRectVertexes[];
[keyHoverElement]: Element<ElementType> | null;
[keyHoverElementVertexes]: ViewRectVertexes | null;
[keySelectedElementList]: Array<Element<ElementType>>;
[keySelectedElementListVertexes]: ViewRectVertexes | null;
[keySelectedElementController]: ElementSizeController | null;
[keySelectedElementPosition]: ElementPosition;
[keyGroupQueue]: StrictMaterial<'group'>[];
[keyHoverMaterial]: StrictMaterial<MaterialType> | null;
[keySelectedMaterialList]: Array<StrictMaterial<MaterialType>>;
[keySelectedMaterialListVertexes]: ViewRectVertexes | null;
[keySelectedMaterialPosition]: MaterialPosition;
[keyIsMoving]: boolean | null;
[keyEnableSelectInGroup]: boolean | null;
[keyEnableSnapToGrid]: boolean | null;
[keyDebugElemCenter]: PointSize | null;
[keyDebugEnd0]: PointSize | null;
[keyDebugEndHorizontal]: PointSize | null;
[keyDebugEndVertical]: PointSize | null;
[keyDebugStartHorizontal]: PointSize | null;
[keyDebugStartVertical]: PointSize | null;
// layout-selector
[keyLayoutIsSelected]: boolean | null;
[keyLayoutIsBusyMoving]: boolean | null;

View file

@ -1,39 +1,40 @@
import {
calcElementCenter,
rotateElementVertexes,
calcElementVertexesInGroup,
calcElementQueueVertexesQueueInGroup,
calcViewPointSize,
calcViewElementSize,
calcMaterialCenter,
rotateMaterialVertexes,
calcMaterialVertexesInGroup,
// calcMaterialQueueVertexesQueueInGroup,
calcViewPoint,
calcViewMaterialSize,
rotatePoint,
parseAngleToRadian,
parseRadianToAngle,
limitAngle,
calcRadian
calcRadian,
} from '@idraw/util';
import type {
ViewRectVertexes,
ElementSizeController,
// MaterialSizeController,
StoreSharer,
ViewScaleInfo,
ViewSizeInfo,
ElementOperations
ViewCalculator,
MaterialOperations,
StrictMaterial,
} from '@idraw/types';
import type {
Data,
Element,
ViewContext2D,
Point,
PointSize,
PointTarget,
PointTargetType,
ViewCalculator,
ElementType,
ElementSize,
MaterialType,
MaterialSize,
ResizeType,
AreaSize
AreaSize,
} from './types';
// import { keyDebugElemCenter, keyDebugEnd0, keyDebugEndHorizontal, keyDebugEndVertical, keyDebugStartHorizontal, keyDebugStartVertical } from './config';
import { ATTR_HANDLER_TYPE } from './static';
// import { keyDebugMtrlCenter, keyDebugEnd0, keyDebugEndHorizontal, keyDebugEndVertical, keyDebugStartHorizontal, keyDebugStartVertical } from './config';
function parseRadian(angle: number) {
return (angle * Math.PI) / 180;
@ -47,15 +48,15 @@ function changeMoveDistDirect(moveDist: number, moveDirect: number) {
return moveDirect > 0 ? Math.abs(moveDist) : 0 - Math.abs(moveDist);
}
export function isPointInViewActiveVertexes(
p: PointSize,
function isPointInViewActiveVertexes(
p: Point,
opts: { ctx: ViewContext2D; vertexes: ViewRectVertexes; viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }
): boolean {
const { ctx, viewScaleInfo, vertexes } = opts;
const v0 = calcViewPointSize(vertexes[0], { viewScaleInfo });
const v1 = calcViewPointSize(vertexes[1], { viewScaleInfo });
const v2 = calcViewPointSize(vertexes[2], { viewScaleInfo });
const v3 = calcViewPointSize(vertexes[3], { viewScaleInfo });
const v0 = calcViewPoint(vertexes[0], { viewScaleInfo });
const v1 = calcViewPoint(vertexes[1], { viewScaleInfo });
const v2 = calcViewPoint(vertexes[2], { viewScaleInfo });
const v3 = calcViewPoint(vertexes[3], { viewScaleInfo });
ctx.beginPath();
ctx.moveTo(v0.x, v0.y);
ctx.lineTo(v1.x, v1.y);
@ -70,77 +71,45 @@ export function isPointInViewActiveVertexes(
return false;
}
export function isPointInViewActiveGroup(
p: PointSize,
opts: {
ctx: ViewContext2D;
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
groupQueue: Element<'group'>[] | null;
}
): boolean {
const { ctx, viewScaleInfo, viewSizeInfo, groupQueue } = opts;
if (!groupQueue || !(groupQueue?.length > 0)) {
return false;
}
const vesQueue = calcElementQueueVertexesQueueInGroup(groupQueue);
const vertexes = vesQueue[vesQueue.length - 1];
if (!vertexes) {
return false;
}
return isPointInViewActiveVertexes(p, { ctx, vertexes, viewScaleInfo, viewSizeInfo });
}
export function getPointTarget(
p: PointSize,
p: Point,
opts: {
ctx: ViewContext2D;
data?: Data | null;
selectedElements?: Element<ElementType>[];
selectedMaterials?: StrictMaterial<MaterialType>[];
areaSize?: AreaSize | null;
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
calculator: ViewCalculator;
groupQueue: Element<'group'>[] | null;
selectedElementController: ElementSizeController | null;
groupQueue: StrictMaterial<'group'>[] | null;
nativeEvent: Event;
}
): PointTarget {
const target: PointTarget = {
type: null,
elements: [],
elementVertexesList: [],
materials: [],
materialVertexesList: [],
groupQueue: [],
groupQueueVertexesList: []
};
const {
ctx,
data,
calculator,
selectedElements,
viewScaleInfo,
viewSizeInfo,
areaSize,
groupQueue,
selectedElementController
} = opts;
const { ctx, data, calculator, selectedMaterials, viewScaleInfo, viewSizeInfo, areaSize, groupQueue, nativeEvent } =
opts;
const $targetElement = nativeEvent.target as HTMLElement | null;
// resize
if (selectedElementController) {
const { left, right, top, bottom, topLeft, topRight, bottomLeft, bottomRight, rotate } = selectedElementController;
const ctrls = [left, right, top, bottom, topLeft, topRight, bottomLeft, bottomRight];
if (selectedElements?.length === 1 && selectedElements?.[0]?.operations?.rotatable !== false) {
ctrls.push(rotate);
}
for (let i = 0; i < ctrls.length; i++) {
const ctrl = ctrls[i];
if (isPointInViewActiveVertexes(p, { ctx, vertexes: ctrl.vertexes, viewSizeInfo, viewScaleInfo })) {
target.type = `resize-${ctrl.type}` as PointTargetType;
if (selectedElements && selectedElements?.length > 0) {
target.groupQueue = groupQueue || [];
target.elements = [selectedElements[0]];
return target;
}
break;
if (selectedMaterials && selectedMaterials?.length === 1 && $targetElement) {
const $elem = $targetElement;
if ($elem?.hasAttribute(ATTR_HANDLER_TYPE)) {
const handlerType = $elem.getAttribute(ATTR_HANDLER_TYPE);
if (
typeof handlerType === 'string'
// TODO
// !(selectedMaterials?.[0]?.operations?.rotatable === false && handlerType === 'rotate')
) {
target.type = `resize-${handlerType}` as PointTargetType;
target.groupQueue = groupQueue || [];
target.materials = [selectedMaterials[0]];
return target;
}
}
}
@ -149,19 +118,19 @@ export function getPointTarget(
if (groupQueue && Array.isArray(groupQueue) && groupQueue.length > 0) {
// return target;
const lastGroup = groupQueue[groupQueue.length - 1];
if (lastGroup?.detail?.children && Array.isArray(lastGroup?.detail?.children)) {
for (let i = lastGroup.detail.children.length - 1; i >= 0; i--) {
const child = lastGroup.detail.children[i];
if (lastGroup?.children && Array.isArray(lastGroup?.children)) {
for (let i = lastGroup.children.length - 1; i >= 0; i--) {
const child = lastGroup.children[i];
// if (child?.operations?.invisible === true) {
// continue;
// }
const vertexes = calcElementVertexesInGroup(child, { groupQueue });
const vertexes = calcMaterialVertexesInGroup(child, { groupQueue });
if (vertexes && isPointInViewActiveVertexes(p, { ctx, vertexes, viewScaleInfo, viewSizeInfo })) {
if (!target.type) {
target.type = 'over-element';
target.type = 'over-material';
}
target.groupQueue = groupQueue;
target.elements = [child];
target.materials = [child];
return target;
}
}
@ -174,21 +143,21 @@ export function getPointTarget(
}
// list area
if (areaSize && Array.isArray(selectedElements) && selectedElements?.length > 1) {
const { x, y, w, h } = areaSize;
if (p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h) {
if (areaSize && Array.isArray(selectedMaterials) && selectedMaterials?.length > 1) {
const { x, y, width, height } = areaSize;
if (p.x >= x && p.x <= x + width && p.y >= y && p.y <= y + height) {
target.type = 'list-area';
target.elements = selectedElements;
target.materials = selectedMaterials;
return target;
}
}
// over-element
// over-material
if (data) {
const { index, element } = calculator.getPointElement(p as Point, { data, viewScaleInfo, viewSizeInfo });
if (index >= 0 && element && element?.operations?.invisible !== true) {
target.elements = [element];
target.type = 'over-element';
const { index, material } = calculator.getPointMaterial(p as Point, { data, viewScaleInfo, viewSizeInfo });
if (index >= 0 && material && material?.operations?.invisible !== true) {
target.materials = [material];
target.type = 'over-material';
return target;
}
}
@ -196,74 +165,73 @@ export function getPointTarget(
return target;
}
export function resizeElement(
elem: ElementSize & { operations?: ElementOperations },
export function resizeMaterial(
mtrl: MaterialSize & { operations?: MaterialOperations },
opts: {
start: PointSize;
end: PointSize;
start: Point;
end: Point;
resizeType: ResizeType;
scale: number;
sharer: StoreSharer; // TODO
sharer: StoreSharer;
calculator: ViewCalculator;
}
): ElementSize {
let { x, y, w, h, angle = 0 } = elem;
const elemCenter = calcElementCenter({ x, y, w, h, angle });
// const centerX = elemCenter.x;
// const centerY = elemCenter.y;
): MaterialSize {
let { x, y, width, height, angle = 0 } = mtrl;
const mtrlCenter = calcMaterialCenter({ x, y, width, height, angle });
angle = limitAngle(angle);
const radian = parseAngleToRadian(angle);
const limitRatio = !!elem?.operations?.limitRatio;
const { start, end, resizeType, scale } = opts;
const limitRatio = !!mtrl?.operations?.limitRatio;
const { start, end, resizeType, scale, calculator } = opts;
let start0: PointSize = { ...start };
let end0: PointSize = { ...end };
let startHorizontal0 = { x: start0.x, y: elemCenter.y };
let endHorizontal0 = { x: end0.x, y: elemCenter.y };
let start0: Point = { ...start };
let end0: Point = { ...end };
let startHorizontal0 = { x: start0.x, y: mtrlCenter.y };
let endHorizontal0 = { x: end0.x, y: mtrlCenter.y };
let startHorizontal = { ...startHorizontal0 };
let endHorizontal = { ...endHorizontal0 };
let startVertical0 = { x: elemCenter.x, y: start0.y };
let endVertical0 = { x: elemCenter.x, y: end0.y };
let startVertical0 = { x: mtrlCenter.x, y: start0.y };
let endVertical0 = { x: mtrlCenter.x, y: end0.y };
let startVertical = { ...startVertical0 };
let endVertical = { ...endVertical0 };
let moveHorizontalX = (endHorizontal.x - startHorizontal.x) / scale;
let moveHorizontalY = (endHorizontal.y - startHorizontal.y) / scale;
let moveHorizontalDist = calcMoveDist(moveHorizontalX, moveHorizontalY);
let centerMoveHorizontalDist = 0;
// let centerMoveHorizontalDist = 0;
let moveVerticalX = (endVertical.x - startVertical.x) / scale;
let moveVerticalY = (endVertical.y - startVertical.y) / scale;
let moveVerticalDist = calcMoveDist(moveVerticalX, moveVerticalY);
let centerMoveVerticalDist = 0;
// let centerMoveVerticalDist = 0;
if (angle > 0 || angle < 0) {
start0 = rotatePoint(elemCenter, start, 0 - radian);
end0 = rotatePoint(elemCenter, end, 0 - radian);
start0 = rotatePoint(mtrlCenter, start, 0 - radian);
end0 = rotatePoint(mtrlCenter, end, 0 - radian);
startHorizontal0 = { x: start0.x, y: elemCenter.y };
endHorizontal0 = { x: end0.x, y: elemCenter.y };
startHorizontal = rotatePoint(elemCenter, startHorizontal0, radian);
endHorizontal = rotatePoint(elemCenter, endHorizontal0, radian);
startHorizontal0 = { x: start0.x, y: mtrlCenter.y };
endHorizontal0 = { x: end0.x, y: mtrlCenter.y };
startHorizontal = rotatePoint(mtrlCenter, startHorizontal0, radian);
endHorizontal = rotatePoint(mtrlCenter, endHorizontal0, radian);
startVertical0 = { x: elemCenter.x, y: start0.y };
endVertical0 = { x: elemCenter.x, y: end0.y };
startVertical = rotatePoint(elemCenter, startVertical0, radian);
endVertical = rotatePoint(elemCenter, endVertical0, radian);
startVertical0 = { x: mtrlCenter.x, y: start0.y };
endVertical0 = { x: mtrlCenter.x, y: end0.y };
startVertical = rotatePoint(mtrlCenter, startVertical0, radian);
endVertical = rotatePoint(mtrlCenter, endVertical0, radian);
moveHorizontalX = (endHorizontal.x - startHorizontal.x) / scale;
moveHorizontalY = (endHorizontal.y - startHorizontal.y) / scale;
moveHorizontalDist = calcMoveDist(moveHorizontalX, moveHorizontalY);
moveHorizontalDist = changeMoveDistDirect(moveHorizontalDist, moveHorizontalY);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
centerMoveHorizontalDist = moveHorizontalDist / 2;
// // eslint-disable-next-line @typescript-eslint/no-unused-vars
// centerMoveHorizontalDist = moveHorizontalDist / 2;
moveVerticalX = (endVertical.x - startVertical.x) / scale;
moveVerticalY = (endVertical.y - startVertical.y) / scale;
moveVerticalDist = calcMoveDist(moveVerticalX, moveVerticalY);
moveVerticalDist = changeMoveDistDirect(moveVerticalDist, moveVerticalY);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
centerMoveVerticalDist = moveVerticalDist / 2;
// // eslint-disable-next-line @typescript-eslint/no-unused-vars
// centerMoveVerticalDist = moveVerticalDist / 2;
}
let moveX = (end.x - start.x) / scale;
@ -274,15 +242,15 @@ export function resizeElement(
if (['resize-top', 'resize-bottom', 'resize-left', 'resize-right'].includes(resizeType)) {
const maxDist = Math.max(Math.abs(moveX), Math.abs(moveY));
moveX = (moveX >= 0 ? 1 : -1) * maxDist;
moveY = (((moveY >= 0 ? 1 : -1) * maxDist) / elem.w) * elem.h;
moveY = (((moveY >= 0 ? 1 : -1) * maxDist) / mtrl.width) * mtrl.height;
const maxVerticalDist = Math.max(Math.abs(moveVerticalX), Math.abs(moveVerticalY));
moveVerticalX = (moveVerticalX >= 0 ? 1 : -1) * maxVerticalDist;
moveVerticalY = (((moveVerticalY >= 0 ? 1 : -1) * maxVerticalDist) / elem.w) * elem.h;
moveVerticalY = (((moveVerticalY >= 0 ? 1 : -1) * maxVerticalDist) / mtrl.width) * mtrl.height;
const maxHorizontalDist = Math.max(Math.abs(moveHorizontalX), Math.abs(moveHorizontalY));
moveHorizontalX = (moveHorizontalX >= 0 ? 1 : -1) * maxHorizontalDist;
moveHorizontalY = (((moveHorizontalY >= 0 ? 1 : -1) * maxHorizontalDist) / elem.w) * elem.h;
moveHorizontalY = (((moveHorizontalY >= 0 ? 1 : -1) * maxHorizontalDist) / mtrl.width) * mtrl.height;
} else if (
['resize-top-left', 'resize-top-right', 'resize-bottom-left', 'resize-bottom-right'].includes(resizeType)
) {
@ -290,7 +258,7 @@ export function resizeElement(
// const maxDist = Math.max(Math.abs(moveX), Math.abs(moveY));
const maxDist = Math.abs(moveX);
moveX = (moveX >= 0 ? 1 : -1) * maxDist;
const moveYLeng = (maxDist / elem.w) * elem.h;
const moveYLeng = (maxDist / mtrl.width) * mtrl.height;
if (resizeType === 'resize-top-left' || resizeType === 'resize-bottom-right') {
moveY = moveX > 0 ? moveYLeng : -moveYLeng;
} else if (resizeType === 'resize-top-right' || resizeType === 'resize-bottom-left') {
@ -300,66 +268,25 @@ export function resizeElement(
{
moveHorizontalDist = Math.abs(moveHorizontalDist);
moveVerticalDist = (moveHorizontalDist / elem.w) * elem.h;
// // const maxDist = Math.max(Math.abs(moveHorizontalDist), Math.abs(moveVerticalDist));
// const maxDist = Math.abs(moveHorizontalDist);
// moveHorizontalDist = (moveHorizontalX >= 0 ? 1 : -1) * maxDist;
// const moveVerticalDistLeng = (maxDist / elem.w) * elem.h;
// if (resizeType === 'resize-top-left') {
// moveVerticalDist = moveHorizontalDist > 0 ? moveVerticalDistLeng : -moveVerticalDistLeng;
// }
// console.log('moveHorizontalDist, moveHorizontalDist ====== ', moveHorizontalDist, moveHorizontalDist);
// if (resizeType === 'resize-top-left' || resizeType === 'resize-bottom-right') {
// moveVerticalDist = moveHorizontalDist > 0 ? moveVerticalDistLeng : -moveVerticalDistLeng;
// } else if (resizeType === 'resize-top-right' || resizeType === 'resize-bottom-left') {
// moveVerticalDist = moveHorizontalDist > 0 ? -moveVerticalDistLeng : moveVerticalDistLeng;
// }
moveVerticalDist = (moveHorizontalDist / mtrl.width) * mtrl.height;
}
// const maxVerticalDist = Math.max(Math.abs(moveVerticalX), Math.abs(moveVerticalY));
// moveVerticalX = (moveVerticalX >= 0 ? 1 : -1) * maxVerticalDist;
// const moveVerticalYDist = (maxVerticalDist / elem.w) * elem.h;
// if (resizeType === 'resize-top-left' || resizeType === 'resize-bottom-right') {
// moveVerticalY = moveVerticalX > 0 ? moveVerticalYDist : -moveVerticalYDist;
// } else if (resizeType === 'resize-top-right' || resizeType === 'resize-bottom-left') {
// moveVerticalY = moveVerticalX > 0 ? -moveVerticalYDist : moveVerticalYDist;
// }
// const maxHorizontalDist = Math.max(Math.abs(moveHorizontalX), Math.abs(moveHorizontalY));
// moveHorizontalX = (moveHorizontalX >= 0 ? 1 : -1) * maxHorizontalDist;
// const moveHorizontalYDist = (maxHorizontalDist / elem.w) * elem.h;
// if (resizeType === 'resize-top-left' || resizeType === 'resize-bottom-right') {
// moveHorizontalY = moveHorizontalX > 0 ? moveHorizontalYDist : -moveHorizontalYDist;
// } else if (resizeType === 'resize-top-right' || resizeType === 'resize-bottom-left') {
// moveHorizontalY = moveHorizontalX > 0 ? -moveHorizontalYDist : moveHorizontalYDist;
// }
// const maxVerticalDist = Math.abs(moveVerticalX);
// moveVerticalX = (moveVerticalX >= 0 ? 1 : -1) * maxVerticalDist;
// moveVerticalY = (((moveVerticalY >= 0 ? 1 : -1) * maxVerticalDist) / elem.w) * elem.h;
// const maxHorizontalDist = Math.abs(moveHorizontalX);
// moveHorizontalX = (moveHorizontalX >= 0 ? 1 : -1) * maxHorizontalDist;
// moveHorizontalY = (((moveHorizontalY >= 0 ? 1 : -1) * maxHorizontalDist) / elem.w) * elem.h;
}
}
switch (resizeType) {
case 'resize-top': {
if (angle === 0) {
if (h - moveY > 0) {
if (height - moveY > 0) {
y += moveY;
h -= moveY;
if (elem.operations?.limitRatio === true) {
x += ((moveY / elem.h) * elem.w) / 2;
w -= (moveY / elem.h) * elem.w;
height -= moveY;
if (mtrl.operations?.limitRatio === true) {
x += ((moveY / mtrl.height) * mtrl.width) / 2;
width -= (moveY / mtrl.height) * mtrl.width;
}
}
} else if (angle > 0 || angle < 0) {
let centerX = elemCenter.x;
let centerY = elemCenter.y;
let centerX = mtrlCenter.x;
let centerY = mtrlCenter.y;
if (angle < 90) {
moveVerticalDist = 0 - changeMoveDistDirect(moveVerticalDist, moveVerticalY);
const radian = parseRadian(angle);
@ -385,29 +312,29 @@ export function resizeElement(
centerX = centerX - centerMoveVerticalDist * Math.cos(radian);
centerY = centerY - centerMoveVerticalDist * Math.sin(radian);
}
if (h + moveVerticalDist > 0) {
if (elem.operations?.limitRatio === true) {
w = w + (moveVerticalDist / elem.h) * elem.w;
if (height + moveVerticalDist > 0) {
if (mtrl.operations?.limitRatio === true) {
width = width + (moveVerticalDist / mtrl.height) * mtrl.width;
}
h = h + moveVerticalDist;
x = centerX - w / 2;
y = centerY - h / 2;
height = height + moveVerticalDist;
x = centerX - width / 2;
y = centerY - height / 2;
}
}
break;
}
case 'resize-bottom': {
if (angle === 0) {
if (elem.h + moveY > 0) {
h += moveY;
if (elem.operations?.limitRatio === true) {
x -= ((moveY / elem.h) * elem.w) / 2;
w += (moveY / elem.h) * elem.w;
if (mtrl.height + moveY > 0) {
height += moveY;
if (mtrl.operations?.limitRatio === true) {
x -= ((moveY / mtrl.height) * mtrl.width) / 2;
width += (moveY / mtrl.height) * mtrl.width;
}
}
} else if (angle > 0 || angle < 0) {
let centerX = elemCenter.x;
let centerY = elemCenter.y;
let centerX = mtrlCenter.x;
let centerY = mtrlCenter.y;
if (angle < 90) {
moveVerticalDist = changeMoveDistDirect(moveVerticalDist, moveVerticalY);
const radian = parseRadian(angle);
@ -433,30 +360,30 @@ export function resizeElement(
centerX = centerX + centerMoveDist * Math.cos(radian);
centerY = centerY + centerMoveDist * Math.sin(radian);
}
if (h + moveVerticalDist > 0) {
if (elem.operations?.limitRatio === true) {
w = w + (moveVerticalDist / elem.h) * elem.w;
if (height + moveVerticalDist > 0) {
if (mtrl.operations?.limitRatio === true) {
width = width + (moveVerticalDist / mtrl.height) * mtrl.width;
}
h = h + moveVerticalDist;
x = centerX - w / 2;
y = centerY - h / 2;
height = height + moveVerticalDist;
x = centerX - width / 2;
y = centerY - height / 2;
}
}
break;
}
case 'resize-left': {
if (angle === 0) {
if (elem.w - moveX > 0) {
if (mtrl.width - moveX > 0) {
x += moveX;
w -= moveX;
if (elem.operations?.limitRatio === true) {
h -= (moveX / elem.w) * elem.h;
y += ((moveX / elem.w) * elem.h) / 2;
width -= moveX;
if (mtrl.operations?.limitRatio === true) {
height -= (moveX / mtrl.width) * mtrl.height;
y += ((moveX / mtrl.width) * mtrl.height) / 2;
}
}
} else if (angle > 0 || angle < 0) {
let centerX = elemCenter.x;
let centerY = elemCenter.y;
let centerX = mtrlCenter.x;
let centerY = mtrlCenter.y;
if (angle < 90) {
moveHorizontalDist = 0 - changeMoveDistDirect(moveHorizontalDist, moveHorizontalX);
const radian = parseRadian(angle);
@ -482,29 +409,29 @@ export function resizeElement(
centerX = centerX - centerMoveHorizontalDist * Math.sin(radian);
centerY = centerY + centerMoveHorizontalDist * Math.cos(radian);
}
if (w + moveHorizontalDist > 0) {
if (elem.operations?.limitRatio === true) {
h = h + (moveHorizontalDist / elem.w) * elem.h;
if (width + moveHorizontalDist > 0) {
if (mtrl.operations?.limitRatio === true) {
height = height + (moveHorizontalDist / mtrl.width) * mtrl.height;
}
w = w + moveHorizontalDist;
x = centerX - w / 2;
y = centerY - h / 2;
width = width + moveHorizontalDist;
x = centerX - width / 2;
y = centerY - height / 2;
}
}
break;
}
case 'resize-right': {
if (angle === 0) {
if (elem.w + moveX > 0) {
w += moveX;
if (elem.operations?.limitRatio === true) {
y -= (moveX * elem.h) / elem.w / 2;
h += (moveX * elem.h) / elem.w;
if (mtrl.width + moveX > 0) {
width += moveX;
if (mtrl.operations?.limitRatio === true) {
y -= (moveX * mtrl.height) / mtrl.width / 2;
height += (moveX * mtrl.height) / mtrl.width;
}
}
} else if (angle > 0 || angle < 0) {
let centerX = elemCenter.x;
let centerY = elemCenter.y;
let centerX = mtrlCenter.x;
let centerY = mtrlCenter.y;
if (angle < 90) {
moveHorizontalDist = changeMoveDistDirect(moveHorizontalDist, moveHorizontalY);
const radian = parseRadian(angle);
@ -531,30 +458,30 @@ export function resizeElement(
centerX = centerX + centerMoveHorizontalDist * Math.sin(radian);
centerY = centerY - centerMoveHorizontalDist * Math.cos(radian);
}
if (w + moveHorizontalDist > 0) {
if (elem.operations?.limitRatio === true) {
h = h + (moveHorizontalDist / elem.w) * elem.h;
if (width + moveHorizontalDist > 0) {
if (mtrl.operations?.limitRatio === true) {
height = height + (moveHorizontalDist / mtrl.width) * mtrl.height;
}
w = w + moveHorizontalDist;
x = centerX - w / 2;
y = centerY - h / 2;
width = width + moveHorizontalDist;
x = centerX - width / 2;
y = centerY - height / 2;
}
}
break;
}
case 'resize-top-left': {
if (angle === 0) {
if (w - moveX > 0) {
if (width - moveX > 0) {
x += moveX;
w -= moveX;
width -= moveX;
}
if (h - moveY > 0) {
if (height - moveY > 0) {
y += moveY;
h -= moveY;
height -= moveY;
}
} else if (angle > 0 || angle < 0) {
let centerX = elemCenter.x;
let centerY = elemCenter.y;
let centerX = mtrlCenter.x;
let centerY = mtrlCenter.y;
if (angle < 90) {
moveVerticalDist = 0 - changeMoveDistDirect(moveVerticalDist, moveVerticalY);
@ -614,29 +541,29 @@ export function resizeElement(
centerX = centerX - centerMoveHorizontalDist * Math.sin(radian);
centerY = centerY + centerMoveHorizontalDist * Math.cos(radian);
}
if (h + moveVerticalDist > 0) {
h = h + moveVerticalDist;
if (height + moveVerticalDist > 0) {
height = height + moveVerticalDist;
}
if (w + moveHorizontalDist > 0) {
w = w + moveHorizontalDist;
if (width + moveHorizontalDist > 0) {
width = width + moveHorizontalDist;
}
x = centerX - w / 2;
y = centerY - h / 2;
x = centerX - width / 2;
y = centerY - height / 2;
}
break;
}
case 'resize-top-right': {
if (angle === 0) {
if (w + moveX > 0) {
w += moveX;
if (width + moveX > 0) {
width += moveX;
}
if (h - moveY > 0) {
if (height - moveY > 0) {
y += moveY;
h -= moveY;
height -= moveY;
}
} else if (angle > 0 || angle < 0) {
let centerX = elemCenter.x;
let centerY = elemCenter.y;
let centerX = mtrlCenter.x;
let centerY = mtrlCenter.y;
if (angle < 90) {
moveVerticalDist = 0 - changeMoveDistDirect(moveVerticalDist, moveVerticalY);
moveHorizontalDist = changeMoveDistDirect(
@ -697,29 +624,29 @@ export function resizeElement(
centerX = centerX + centerMoveHorizontalDist * Math.sin(radian);
centerY = centerY - centerMoveHorizontalDist * Math.cos(radian);
}
if (h + moveVerticalDist > 0) {
h = h + moveVerticalDist;
if (height + moveVerticalDist > 0) {
height = height + moveVerticalDist;
}
if (w + moveHorizontalDist > 0) {
w = w + moveHorizontalDist;
if (width + moveHorizontalDist > 0) {
width = width + moveHorizontalDist;
}
x = centerX - w / 2;
y = centerY - h / 2;
x = centerX - width / 2;
y = centerY - height / 2;
}
break;
}
case 'resize-bottom-left': {
if (angle === 0) {
if (elem.h + moveY > 0) {
h += moveY;
if (mtrl.height + moveY > 0) {
height += moveY;
}
if (elem.w - moveX > 0) {
if (mtrl.width - moveX > 0) {
x += moveX;
w -= moveX;
width -= moveX;
}
} else if (angle > 0 || angle < 0) {
let centerX = elemCenter.x;
let centerY = elemCenter.y;
let centerX = mtrlCenter.x;
let centerY = mtrlCenter.y;
if (angle < 90) {
moveVerticalDist = changeMoveDistDirect(moveVerticalDist, moveVerticalY);
moveHorizontalDist =
@ -778,28 +705,28 @@ export function resizeElement(
centerX = centerX - centerMoveHorizontalDist * Math.sin(radian);
centerY = centerY + centerMoveHorizontalDist * Math.cos(radian);
}
if (h + moveVerticalDist > 0) {
h = h + moveVerticalDist;
if (height + moveVerticalDist > 0) {
height = height + moveVerticalDist;
}
if (w + moveHorizontalDist > 0) {
w = w + moveHorizontalDist;
if (width + moveHorizontalDist > 0) {
width = width + moveHorizontalDist;
}
x = centerX - w / 2;
y = centerY - h / 2;
x = centerX - width / 2;
y = centerY - height / 2;
}
break;
}
case 'resize-bottom-right': {
if (angle === 0) {
if (elem.h + moveY > 0) {
h += moveY;
if (mtrl.height + moveY > 0) {
height += moveY;
}
if (elem.w + moveX > 0) {
w += moveX;
if (mtrl.width + moveX > 0) {
width += moveX;
}
} else if (angle > 0 || angle < 0) {
let centerX = elemCenter.x;
let centerY = elemCenter.y;
let centerX = mtrlCenter.x;
let centerY = mtrlCenter.y;
if (angle < 90) {
moveVerticalDist = changeMoveDistDirect(moveVerticalDist, moveVerticalY);
moveHorizontalDist = changeMoveDistDirect(
@ -858,15 +785,15 @@ export function resizeElement(
centerX = centerX + centerMoveHorizontalDist * Math.sin(radian);
centerY = centerY - centerMoveHorizontalDist * Math.cos(radian);
}
if (h + moveVerticalDist > 0) {
h = h + moveVerticalDist;
if (height + moveVerticalDist > 0) {
height = height + moveVerticalDist;
}
if (w + moveHorizontalDist > 0) {
w = w + moveHorizontalDist;
if (width + moveHorizontalDist > 0) {
width = width + moveHorizontalDist;
}
x = centerX - w / 2;
y = centerY - h / 2;
x = centerX - width / 2;
y = centerY - height / 2;
}
break;
}
@ -875,46 +802,43 @@ export function resizeElement(
}
}
// // TODO mock data
// const sharer = opts.sharer;
// sharer.setSharedStorage(keyDebugElemCenter, elemCenter);
// sharer.setSharedStorage(keyDebugStartVertical, startVertical);
// sharer.setSharedStorage(keyDebugEndVertical, endVertical);
// sharer.setSharedStorage(keyDebugStartHorizontal, startHorizontal);
// sharer.setSharedStorage(keyDebugEndHorizontal, endHorizontal);
// sharer.setSharedStorage(keyDebugStartHorizontal, startHorizontal);
// sharer.setSharedStorage(keyDebugEnd0, end);
return { x, y, w, h, angle: elem.angle };
return {
x: calculator.toGridNum(x),
y: calculator.toGridNum(y),
width: calculator.toGridNum(width),
height: calculator.toGridNum(height),
angle: calculator.toGridNum(mtrl.angle || 0),
};
}
export function rotateElement(
elem: ElementSize,
export function rotateMaterial(
mtrl: MaterialSize,
opts: {
center: PointSize;
start: PointSize;
end: PointSize;
center: Point;
start: Point;
end: Point;
resizeType: ResizeType;
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
sharer: StoreSharer; // TODO
sharer: StoreSharer;
calculator: ViewCalculator;
}
): ElementSize {
const { x, y, w, h, angle = 0 } = elem;
const { center, start, end, viewScaleInfo } = opts;
const elemCenter = calcViewPointSize(center, {
viewScaleInfo
): MaterialSize {
const { x, y, width, height, angle = 0 } = mtrl;
const { center, start, end, viewScaleInfo, calculator } = opts;
const mtrlCenter = calcViewPoint(center, {
viewScaleInfo,
});
const startAngle = limitAngle(angle);
const changedRadian = calcRadian(elemCenter, start, end);
const changedRadian = calcRadian(mtrlCenter, start, end);
const endAngle = limitAngle(startAngle + parseRadianToAngle(changedRadian));
return {
x,
y,
w,
h,
angle: endAngle
x: calculator.toGridNum(x),
y: calculator.toGridNum(y),
width: calculator.toGridNum(width),
height: calculator.toGridNum(height),
angle: calculator.toGridNum(endAngle),
};
}
@ -927,109 +851,109 @@ export function getSelectedListArea(
viewSizeInfo: ViewSizeInfo;
calculator: ViewCalculator;
}
): { indexes: number[]; uuids: string[]; elements: Element<ElementType>[] } {
): { indexes: number[]; ids: string[]; materials: StrictMaterial<MaterialType>[] } {
const indexes: number[] = [];
const uuids: string[] = [];
const elements: Element<ElementType>[] = [];
const ids: string[] = [];
const materials: StrictMaterial<MaterialType>[] = [];
const { viewScaleInfo, start, end } = opts;
if (!(Array.isArray(data.elements) && start && end)) {
return { indexes, uuids, elements };
if (!(Array.isArray(data.materials) && start && end)) {
return { indexes, ids, materials };
}
const startX = Math.min(start.x, end.x);
const endX = Math.max(start.x, end.x);
const startY = Math.min(start.y, end.y);
const endY = Math.max(start.y, end.y);
for (let idx = 0; idx < data.elements.length; idx++) {
const elem = data.elements[idx];
if (elem?.operations?.locked === true) {
for (let idx = 0; idx < data.materials.length; idx++) {
const mtrl = data.materials[idx];
if (mtrl?.operations?.locked === true) {
continue;
}
const elemSize = calcViewElementSize(elem, { viewScaleInfo });
const mtrlSize = calcViewMaterialSize(mtrl, { viewScaleInfo });
const center = calcElementCenter(elemSize);
const center = calcMaterialCenter(mtrlSize);
if (center.x >= startX && center.x <= endX && center.y >= startY && center.y <= endY) {
indexes.push(idx);
uuids.push(elem.uuid);
elements.push(elem);
if (elemSize.angle && (elemSize.angle > 0 || elemSize.angle < 0)) {
const ves = rotateElementVertexes(elemSize);
ids.push(mtrl.id);
materials.push(mtrl);
if (mtrlSize.angle && (mtrlSize.angle > 0 || mtrlSize.angle < 0)) {
const ves = rotateMaterialVertexes(mtrlSize);
if (ves.length === 4) {
const xList = [ves[0].x, ves[1].x, ves[2].x, ves[3].x];
const yList = [ves[0].y, ves[1].y, ves[2].y, ves[3].y];
elemSize.x = Math.min(...xList);
elemSize.y = Math.min(...yList);
elemSize.w = Math.abs(Math.max(...xList) - Math.min(...xList));
elemSize.h = Math.abs(Math.max(...yList) - Math.min(...yList));
mtrlSize.x = Math.min(...xList);
mtrlSize.y = Math.min(...yList);
mtrlSize.width = Math.abs(Math.max(...xList) - Math.min(...xList));
mtrlSize.height = Math.abs(Math.max(...yList) - Math.min(...yList));
}
}
}
}
return { indexes, uuids, elements };
return { indexes, ids, materials };
}
export function calcSelectedElementsArea(
elements: Element<ElementType>[],
export function calcSelectedMaterialsArea(
materials: StrictMaterial<MaterialType>[],
opts: {
viewScaleInfo: ViewScaleInfo;
viewSizeInfo: ViewSizeInfo;
calculator: ViewCalculator;
}
): AreaSize | null {
if (!Array.isArray(elements)) {
if (!Array.isArray(materials)) {
return null;
}
const area: AreaSize = { x: 0, y: 0, w: 0, h: 0 };
const area: AreaSize = { x: 0, y: 0, width: 0, height: 0 };
const { viewScaleInfo } = opts;
let prevElemSize: ElementSize | null = null;
let prevMtrlSize: MaterialSize | null = null;
for (let i = 0; i < elements.length; i++) {
const elem = elements[i];
if (elem?.operations?.invisible) {
for (let i = 0; i < materials.length; i++) {
const mtrl = materials[i];
if (mtrl?.operations?.invisible) {
continue;
}
const elemSize = calcViewElementSize(elem, { viewScaleInfo });
const mtrlSize = calcViewMaterialSize(mtrl, { viewScaleInfo });
if (elemSize.angle && (elemSize.angle > 0 || elemSize.angle < 0)) {
const ves = rotateElementVertexes(elemSize);
if (mtrlSize.angle && (mtrlSize.angle > 0 || mtrlSize.angle < 0)) {
const ves = rotateMaterialVertexes(mtrlSize);
if (ves.length === 4) {
const xList = [ves[0].x, ves[1].x, ves[2].x, ves[3].x];
const yList = [ves[0].y, ves[1].y, ves[2].y, ves[3].y];
elemSize.x = Math.min(...xList);
elemSize.y = Math.min(...yList);
elemSize.w = Math.abs(Math.max(...xList) - Math.min(...xList));
elemSize.h = Math.abs(Math.max(...yList) - Math.min(...yList));
mtrlSize.x = Math.min(...xList);
mtrlSize.y = Math.min(...yList);
mtrlSize.width = Math.abs(Math.max(...xList) - Math.min(...xList));
mtrlSize.height = Math.abs(Math.max(...yList) - Math.min(...yList));
}
}
if (prevElemSize) {
const areaStartX = Math.min(elemSize.x, area.x);
const areaStartY = Math.min(elemSize.y, area.y);
if (prevMtrlSize) {
const areaStartX = Math.min(mtrlSize.x, area.x);
const areaStartY = Math.min(mtrlSize.y, area.y);
const areaEndX = Math.max(elemSize.x + elemSize.w, area.x + area.w);
const areaEndY = Math.max(elemSize.y + elemSize.h, area.y + area.h);
const areaEndX = Math.max(mtrlSize.x + mtrlSize.width, area.x + area.width);
const areaEndY = Math.max(mtrlSize.y + mtrlSize.height, area.y + area.height);
area.x = areaStartX;
area.y = areaStartY;
area.w = Math.abs(areaEndX - areaStartX);
area.h = Math.abs(areaEndY - areaStartY);
area.width = Math.abs(areaEndX - areaStartX);
area.height = Math.abs(areaEndY - areaStartY);
} else {
area.x = elemSize.x;
area.y = elemSize.y;
area.w = elemSize.w;
area.h = elemSize.h;
area.x = mtrlSize.x;
area.y = mtrlSize.y;
area.width = mtrlSize.width;
area.height = mtrlSize.height;
}
prevElemSize = elemSize;
prevMtrlSize = mtrlSize;
}
return area;
}
export function isElementInGroup(elem: Element<ElementType>, group: Element<'group'>): boolean {
if (group?.type === 'group' && Array.isArray(group?.detail?.children)) {
for (let i = 0; i < group.detail.children.length; i++) {
const child = group.detail.children[i];
if (elem.uuid === child.uuid) {
export function isMaterialInGroup(mtrl: StrictMaterial<MaterialType>, group: StrictMaterial<'group'>): boolean {
if (group?.type === 'group' && Array.isArray(group?.children)) {
for (let i = 0; i < group.children.length; i++) {
const child = group.children[i];
if (mtrl.id === child.id) {
return true;
}
}

View file

@ -0,0 +1,184 @@
import type { HTMLCSSProps, MiddlewareTextEditorStyles, MaterialSize } from '@idraw/types';
import {
injectStyles,
removeStyles,
setHTMLCSSProps,
limitAngle,
getDefaultMaterialAttributes,
enhanceFontFamliy,
} from '@idraw/util';
import { classNameMap } from './static';
import type { InnerOptions } from './types';
const defaultMaterialAttributes = getDefaultMaterialAttributes();
export function initStyles(rootClassName: string, styles: MiddlewareTextEditorStyles) {
injectStyles({
type: 'element',
rootClassName,
styles: {
position: 'fixed',
top: '0',
bottom: '0',
left: '0',
right: '0',
display: 'block',
zIndex: styles.zIndex,
[`&.${classNameMap.hide}`]: {
display: 'none',
},
[`.${classNameMap.textarea}`]: {
display: 'inline-flex',
flexDirection: 'column',
position: 'absolute',
boxSizing: 'border-box',
overflow: 'hidden',
wordBreak: 'break-all',
padding: '0',
margin: '0',
outline: 'none',
border: `1px solid ${styles.boxBorderColor}`,
background: `transparent`,
},
[`.${classNameMap.canvasWrapper}`]: {
position: 'absolute',
},
},
});
}
export function destroyStyles(rootClassName: string) {
removeStyles({ rootClassName, type: 'element' });
}
const createBox = (opts: { size: MaterialSize; parent: HTMLDivElement }) => {
const { size, parent } = opts;
const div = document.createElement('div');
const { x, y, width, height } = size;
const angle = limitAngle(size.angle || 0);
setHTMLCSSProps(div, {
position: 'absolute',
left: `${x}px`,
top: `${y}px`,
width: `${width}px`,
height: `${height}px`,
transform: `rotate(${angle}deg)`,
});
parent.appendChild(div);
return div;
};
export const resetTextArea = (
textarea: HTMLDivElement | null,
canvasWrapper: HTMLDivElement | null,
opts: InnerOptions
) => {
if (!textarea || !canvasWrapper) {
return;
}
const { viewScaleInfo, material, groupQueue } = opts;
const { scale, offsetTop, offsetLeft } = viewScaleInfo;
if (canvasWrapper?.children) {
Array.from(canvasWrapper.children).forEach((child) => {
child.remove();
});
}
let parent = canvasWrapper;
for (let i = 0; i < groupQueue.length; i++) {
const group = groupQueue[i];
const { x, y, width, height } = group;
const angle = limitAngle(group.angle || 0);
const size: MaterialSize = {
x: x * scale,
y: y * scale,
width: width * scale,
height: height * scale,
angle,
};
if (i === 0) {
size.x += offsetLeft;
size.y += offsetTop;
}
parent = createBox({ size, parent });
}
const attributes = {
...defaultMaterialAttributes,
...material,
};
let mtrlX = material.x * scale + offsetLeft;
let mtrlY = material.y * scale + offsetTop;
let mtrlW = material.width * scale;
let mtrlH = material.height * scale;
if (groupQueue.length > 0) {
mtrlX = material.x * scale;
mtrlY = material.y * scale;
mtrlW = material.width * scale;
mtrlH = material.height * scale;
}
let justifyContent: ElementCSSInlineStyle['style']['justifyContent'] = 'center';
let alignItems = 'center';
if (attributes.textAlign === 'left') {
justifyContent = 'start';
} else if (attributes.textAlign === 'right') {
justifyContent = 'end';
}
if (attributes.verticalAlign === 'top') {
alignItems = 'start';
} else if (attributes.verticalAlign === 'bottom') {
alignItems = 'end';
}
setHTMLCSSProps(textarea, {
justifyContent: justifyContent as HTMLCSSProps['justifyContent'],
alignItems: alignItems as HTMLCSSProps['alignItems'],
transform: `rotate(${limitAngle(material.angle || 0)}deg)`,
left: `${mtrlX - 1}px`,
top: `${mtrlY - 1}px`,
width: `${mtrlW + 2}px`,
height: `${mtrlH + 2}px`,
cornerRadius: `${(typeof attributes.cornerRadius === 'number' ? attributes.cornerRadius : 0) * scale}px`,
color: `${attributes.fill || '#000000'}`,
textStroke: `${
typeof attributes.strokeWidth === 'number' && attributes.strokeWidth > 0
? `${attributes.strokeWidth}px ${attributes.stroke}`
: ''
}`,
'-webkit-text-stroke': `${
typeof attributes.strokeWidth === 'number' && attributes.strokeWidth > 0
? `${attributes.strokeWidth}px ${attributes.stroke}`
: ''
}`,
fontSize: `${attributes.fontSize * scale}px`,
lineHeight: `${(attributes.lineHeight || attributes.fontSize) * scale}px`,
fontFamily: enhanceFontFamliy(attributes.fontFamily),
fontWeight: `${attributes.fontWeight}`,
opacity: attributes.opacity || 1,
// display: 'inline-flex',
// flexDirection: 'column',
// position: 'absolute',
// boxSizing: 'border-box',
// overflow: 'hidden',
// wordBreak: 'break-all',
// padding: '0',
// margin: '0',
// outline: 'none',
// border: `1px solid ${styles.boxBorderColor}`,
// background: `transparent`,
});
// textarea.value = attributes.text || '';
textarea.innerText = attributes.text || '';
parent.appendChild(textarea);
};

View file

@ -1,85 +1,122 @@
import type { Middleware, CoreEventMap, Element, ElementSize, ViewScaleInfo, ElementPosition } from '@idraw/types';
import { limitAngle, getDefaultElementDetailConfig, enhanceFontFamliy, updateElementInList } from '@idraw/util';
import { coreEventKeys } from '../../config';
import type {
Middleware,
CoreEventMap,
StrictMaterial,
MaterialPosition,
MiddlewareTextEditorStyles,
MiddlewareTextEditorConfig,
} from '@idraw/types';
import {
updateMaterialInList,
getGroupQueueByMaterialPosition,
getMaterialAndGroupQueueFromList,
addClassName,
removeClassName,
createHTMLElement,
setHTMLCSSProps,
} from '@idraw/util';
import { coreEventKeys } from '../../static';
import { initStyles, destroyStyles, resetTextArea } from './dom';
import { classNameMap, getRootClassName, defaultStyles, getMiddlewareTextEditorStyles } from './static';
import type { TextEditEvent, InnerOptions, ExtendEventMap } from './types';
import { triggerChangeEvent } from '../common';
type TextEditEvent = {
element: Element<'text'>;
position: ElementPosition;
groupQueue: Element<'group'>[];
viewScaleInfo: ViewScaleInfo;
};
export { getMiddlewareTextEditorStyles };
type TextChangeEvent = {
element: {
uuid: string;
detail: {
text: string;
};
};
position: ElementPosition;
};
type ExtendEventMap = Record<typeof coreEventKeys.TEXT_EDIT, TextEditEvent> &
Record<typeof coreEventKeys.TEXT_CHANGE, TextChangeEvent>;
const defaultElementDetail = getDefaultElementDetailConfig();
export const MiddlewareTextEditor: Middleware<ExtendEventMap, CoreEventMap & ExtendEventMap> = (opts) => {
const { eventHub, boardContent, viewer, sharer, calculator } = opts;
export const MiddlewareTextEditor: Middleware<
ExtendEventMap,
CoreEventMap & ExtendEventMap,
MiddlewareTextEditorConfig
> = (options, config) => {
const { eventHub, boardContent, viewer, sharer, calculator } = options;
const canvas = boardContent.boardContext.canvas;
const container = opts.container || document.body;
let textarea = document.createElement('div');
textarea.setAttribute('contenteditable', 'true');
let canvasWrapper = document.createElement('div');
let mask = document.createElement('div');
let activeElem: Element<'text'> | null = null;
let activePosition: ElementPosition = [];
const container = options.container || document.body;
const innerConfig = { ...defaultStyles, ...config };
const styles: MiddlewareTextEditorStyles = getMiddlewareTextEditorStyles(innerConfig);
let activeMtrl: StrictMaterial<'text'> | null = null;
let activePosition: MaterialPosition = [];
let originText: string = '';
let isShow: boolean | null = false;
const id = `idraw-middleware-text-editor-${Math.random().toString(26).substring(2)}`;
mask.setAttribute('id', id);
canvasWrapper.appendChild(textarea);
const rootClassName = getRootClassName();
canvasWrapper.style.position = 'absolute';
mask.appendChild(canvasWrapper);
let textarea: HTMLDivElement | null = null;
let canvasWrapper: HTMLDivElement | null = null;
let root: HTMLDivElement | null = null;
mask.style.position = 'fixed';
mask.style.top = '0';
mask.style.bottom = '0';
mask.style.left = '0';
mask.style.right = '0';
mask.style.display = 'none';
container.appendChild(mask);
const initDOM = () => {
if (isShow === true) {
return;
}
textarea = createHTMLElement('div', {
className: classNameMap.textarea,
contenteditable: 'true',
});
canvasWrapper = createHTMLElement(
'div',
{
className: classNameMap.canvasWrapper,
},
[textarea]
);
root = createHTMLElement(
'div',
{
id,
className: rootClassName,
},
[canvasWrapper]
);
container.appendChild(root);
};
const showTextArea = (e: TextEditEvent) => {
const destroyDOM = () => {
root?.remove();
};
const showTextArea = (e: InnerOptions) => {
if (!root || !textarea) {
return;
}
resetCanvasWrapper();
resetTextArea(e);
mask.style.display = 'block';
resetTextArea(textarea, canvasWrapper, e);
removeClassName(root, [classNameMap.hide]);
originText = '';
if (activeElem?.uuid) {
sharer.setActiveOverrideElemenentMap({
[activeElem.uuid]: {
operations: { invisible: true }
}
isShow = true;
// moveCursorToEnd(textarea);
textarea.focus();
if (activeMtrl?.id) {
sharer.setActiveOverrideMaterialMap({
[activeMtrl.id]: {
operations: { invisible: true },
},
});
originText = activeElem.detail.text || '';
originText = activeMtrl.text || '';
viewer.drawFrame();
}
};
const hideTextArea = () => {
if (activeElem?.uuid) {
const map = sharer.getActiveOverrideElemenentMap();
if (activeMtrl?.id) {
const map = sharer.getActiveOverrideMaterialMap();
if (map) {
delete map[activeElem.uuid];
delete map[activeMtrl.id];
}
sharer.setActiveOverrideElemenentMap(map);
sharer.setActiveOverrideMaterialMap(map);
viewer.drawFrame();
}
if (root) {
addClassName(root, [classNameMap.hide]);
}
mask.style.display = 'none';
activeElem = null;
activeMtrl = null;
activePosition = [];
isShow = false;
destroyDOM();
};
const getCanvasRect = () => {
@ -88,215 +125,120 @@ export const MiddlewareTextEditor: Middleware<ExtendEventMap, CoreEventMap & Ext
return { left, top, width, height };
};
const createBox = (opts: { size: ElementSize; parent: HTMLDivElement }) => {
const { size, parent } = opts;
const div = document.createElement('div');
const { x, y, w, h } = size;
const angle = limitAngle(size.angle || 0);
div.style.position = 'absolute';
div.style.left = `${x}px`;
div.style.top = `${y}px`;
div.style.width = `${w}px`;
div.style.height = `${h}px`;
div.style.transform = `rotate(${angle}deg)`;
parent.appendChild(div);
return div;
};
const resetTextArea = (e: TextEditEvent) => {
const { viewScaleInfo, element, groupQueue } = e;
const { scale, offsetTop, offsetLeft } = viewScaleInfo;
if (canvasWrapper.children) {
Array.from(canvasWrapper.children).forEach((child) => {
child.remove();
});
}
let parent = canvasWrapper;
for (let i = 0; i < groupQueue.length; i++) {
const group = groupQueue[i];
const { x, y, w, h } = group;
const angle = limitAngle(group.angle || 0);
const size = {
x: x * scale,
y: y * scale,
w: w * scale,
h: h * scale,
angle
};
if (i === 0) {
size.x += offsetLeft;
size.y += offsetTop;
}
parent = createBox({ size, parent });
}
const detail = {
...defaultElementDetail,
...element.detail
};
let elemX = element.x * scale + offsetLeft;
let elemY = element.y * scale + offsetTop;
let elemW = element.w * scale;
let elemH = element.h * scale;
if (groupQueue.length > 0) {
elemX = element.x * scale;
elemY = element.y * scale;
elemW = element.w * scale;
elemH = element.h * scale;
}
let justifyContent: ElementCSSInlineStyle['style']['justifyContent'] = 'center';
let alignItems = 'center';
if (detail.textAlign === 'left') {
justifyContent = 'start';
} else if (detail.textAlign === 'right') {
justifyContent = 'end';
}
if (detail.verticalAlign === 'top') {
alignItems = 'start';
} else if (detail.verticalAlign === 'bottom') {
alignItems = 'end';
}
textarea.style.display = 'inline-flex';
textarea.style.flexDirection = 'column';
textarea.style.justifyContent = justifyContent;
textarea.style.alignItems = alignItems;
textarea.style.position = 'absolute';
textarea.style.left = `${elemX - 1}px`;
textarea.style.top = `${elemY - 1}px`;
textarea.style.width = `${elemW + 2}px`;
textarea.style.height = `${elemH + 2}px`;
textarea.style.transform = `rotate(${limitAngle(element.angle || 0)}deg)`;
// textarea.style.border = 'none';
textarea.style.boxSizing = 'border-box';
textarea.style.border = '1px solid #1973ba';
textarea.style.resize = 'none';
textarea.style.overflow = 'hidden';
textarea.style.wordBreak = 'break-all';
textarea.style.borderRadius = `${(typeof detail.borderRadius === 'number' ? detail.borderRadius : 0) * scale}px`;
textarea.style.background = `${detail.background || 'transparent'}`;
textarea.style.color = `${detail.color || '#333333'}`;
textarea.style.fontSize = `${detail.fontSize * scale}px`;
textarea.style.lineHeight = `${(detail.lineHeight || detail.fontSize) * scale}px`;
textarea.style.fontFamily = enhanceFontFamliy(detail.fontFamily);
textarea.style.fontWeight = `${detail.fontWeight}`;
textarea.style.padding = '0';
textarea.style.margin = '0';
textarea.style.outline = 'none';
// textarea.value = detail.text || '';
textarea.innerText = detail.text || '';
parent.appendChild(textarea);
};
const resetCanvasWrapper = () => {
if (!canvasWrapper) {
return;
}
const { left, top, width, height } = getCanvasRect();
canvasWrapper.style.position = 'absolute';
canvasWrapper.style.overflow = 'hidden';
canvasWrapper.style.top = `${top}px`;
canvasWrapper.style.left = `${left}px`;
canvasWrapper.style.width = `${width}px`;
canvasWrapper.style.height = `${height}px`;
// canvasWrapper.style.background = '#000000';
setHTMLCSSProps(canvasWrapper, {
position: 'absolute',
overflow: 'hidden',
top: `${top}px`,
left: `${left}px`,
width: `${width}px`,
height: `${height}px`,
});
};
const maskClickEvent = () => {
hideTextArea();
};
const textareaClickEvent = (e: MouseEvent) => {
const textareaDoubleClickEvent = (e: MouseEvent) => {
e.stopPropagation();
e.preventDefault();
window?.getSelection()?.removeAllRanges();
};
const textareaSelectStartEvent = (e: any) => {
if (e.attributes === 2) {
// attributes=2 double click
e.preventDefault();
}
};
const textareaInputEvent = () => {
if (activeElem && activePosition) {
// activeElem.detail.text = (e.target as any).value || '';
activeElem.detail.text = textarea.innerText || '';
if (!textarea) {
return;
}
if (activeMtrl && activePosition) {
// activeMtrl.text = (e.target as any).value || '';
activeMtrl.text = textarea.innerText || '';
eventHub.trigger(coreEventKeys.TEXT_CHANGE, {
element: {
uuid: activeElem.uuid,
detail: {
text: activeElem.detail.text
}
material: {
id: activeMtrl.id,
attributes: {
text: activeMtrl.text,
},
},
position: [...(activePosition || [])]
position: [...(activePosition || [])],
});
const virtualItem = calculator.getVirtualItem(activeMtrl.id);
const data = sharer.getActiveStorage('data') || { materials: [] };
calculator.modifyVirtualAttributes(activeMtrl, {
viewScaleInfo: sharer.getActiveViewScaleInfo(),
viewSizeInfo: sharer.getActiveViewSizeInfo(),
groupQueue: getGroupQueueByMaterialPosition(data.materials, virtualItem?.position || []) || [],
});
calculator.modifyText(activeElem);
viewer.drawFrame();
}
};
const textareaBlurEvent = () => {
if (activeElem && activePosition) {
activeElem.detail.text = textarea.innerText || '';
if (activeMtrl && activePosition) {
activeMtrl.text = textarea?.innerText || '';
eventHub.trigger(coreEventKeys.TEXT_CHANGE, {
element: {
uuid: activeElem.uuid,
detail: {
text: activeElem.detail.text
}
material: {
id: activeMtrl.id,
attributes: {
text: activeMtrl.text,
},
},
position: [...activePosition]
position: [...activePosition],
});
const data = sharer.getActiveStorage('data') || { elements: [] };
const data = sharer.getActiveStorage('data') || { materials: [] };
const updateContent = {
detail: {
text: activeElem.detail.text
}
text: activeMtrl.text,
};
updateElementInList(activeElem.uuid, updateContent, data.elements);
updateMaterialInList(activeMtrl.id, updateContent, data.materials);
eventHub.trigger(coreEventKeys.CHANGE, {
selectedElements: [
triggerChangeEvent(eventHub, {
selectedMaterials: [
{
...activeElem,
detail: {
...activeElem.detail,
...updateContent.detail
}
}
...activeMtrl,
...activeMtrl,
...updateContent,
},
],
data,
type: 'modifyElement',
type: 'modifyMaterial',
modifyRecord: {
type: 'modifyElement',
type: 'modifyMaterial',
time: Date.now(),
content: {
method: 'modifyElement',
uuid: activeElem.uuid as string,
method: 'modifyMaterial',
id: activeMtrl.id as string,
before: {
'detail.text': originText
'attributes.text': originText,
},
after: {
'detail.text': activeElem.detail.text
}
}
}
'attributes.text': activeMtrl.text,
},
},
},
});
const virtualItem = calculator.getVirtualItem(activeMtrl.id);
calculator.modifyVirtualAttributes(activeMtrl, {
viewScaleInfo: sharer.getActiveViewScaleInfo(),
viewSizeInfo: sharer.getActiveViewSizeInfo(),
groupQueue: getGroupQueueByMaterialPosition(data.materials, virtualItem?.position || []) || [],
});
calculator.modifyText(activeElem);
viewer.drawFrame();
}
hideTextArea();
};
const textareaKeyDownEvent = (e: KeyboardEvent) => {
e.stopPropagation();
};
const textareaKeyPressEvent = (e: KeyboardEvent) => {
e.stopPropagation();
};
const textareaKeyUpEvent = (e: KeyboardEvent) => {
const preventDefaultEvent = (e: KeyboardEvent | MouseEvent) => {
e.stopPropagation();
};
@ -305,51 +247,102 @@ export const MiddlewareTextEditor: Middleware<ExtendEventMap, CoreEventMap & Ext
e.preventDefault();
};
mask.addEventListener('click', maskClickEvent);
textarea.addEventListener('click', textareaClickEvent);
textarea.addEventListener('input', textareaInputEvent);
textarea.addEventListener('blur', textareaBlurEvent);
textarea.addEventListener('keydown', textareaKeyDownEvent);
textarea.addEventListener('keypress', textareaKeyPressEvent);
textarea.addEventListener('keyup', textareaKeyUpEvent);
textarea.addEventListener('wheel', textareaWheelEvent);
const onEvents = () => {
root?.addEventListener('click', maskClickEvent);
textarea?.addEventListener('mousedown', preventDefaultEvent);
textarea?.addEventListener('mouseover', preventDefaultEvent);
textarea?.addEventListener('mouseenter', preventDefaultEvent);
textarea?.addEventListener('mouseleave', preventDefaultEvent);
textarea?.addEventListener('dblclick', textareaDoubleClickEvent);
textarea?.addEventListener('selectstart', textareaSelectStartEvent);
textarea?.addEventListener('click', preventDefaultEvent);
textarea?.addEventListener('input', textareaInputEvent);
textarea?.addEventListener('blur', textareaBlurEvent);
textarea?.addEventListener('keydown', preventDefaultEvent);
textarea?.addEventListener('keypress', preventDefaultEvent);
textarea?.addEventListener('keyup', preventDefaultEvent);
textarea?.addEventListener('wheel', textareaWheelEvent);
};
const offEvents = () => {
root?.removeEventListener('click', maskClickEvent);
textarea?.removeEventListener('mousedown', preventDefaultEvent);
textarea?.removeEventListener('mouseover', preventDefaultEvent);
textarea?.removeEventListener('mouseenter', preventDefaultEvent);
textarea?.removeEventListener('mouseleave', preventDefaultEvent);
textarea?.removeEventListener('dblclick', textareaDoubleClickEvent);
textarea?.removeEventListener('selectstart', textareaSelectStartEvent);
textarea?.removeEventListener('click', preventDefaultEvent);
textarea?.removeEventListener('input', textareaInputEvent);
textarea?.removeEventListener('blur', textareaBlurEvent);
textarea?.removeEventListener('keydown', preventDefaultEvent);
textarea?.removeEventListener('keypress', preventDefaultEvent);
textarea?.removeEventListener('keyup', preventDefaultEvent);
textarea?.removeEventListener('wheel', textareaWheelEvent);
};
const textEditCallback = (e: TextEditEvent) => {
if (e?.position && e?.element && e?.element?.type === 'text') {
activeElem = e.element;
activePosition = e.position;
const { id } = e;
if (!(typeof id === 'string' && id)) {
return;
}
initDOM();
onEvents();
const data = sharer.getActiveStorage('data');
const { material, groupQueue, position } = getMaterialAndGroupQueueFromList(id, data.materials);
if (material?.type === 'text') {
activeMtrl = material as StrictMaterial<'text'>;
activePosition = position;
showTextArea({
material: activeMtrl,
groupQueue,
viewScaleInfo: sharer.getActiveViewScaleInfo(),
styles,
});
}
};
const preventAction = () => {
if (isShow === true) {
return false;
}
showTextArea(e);
};
return {
name: '@middleware/text-editor',
use() {
initStyles(rootClassName, styles);
eventHub.on(coreEventKeys.TEXT_EDIT, textEditCallback);
},
disuse() {
destroyStyles(rootClassName);
eventHub.off(coreEventKeys.TEXT_EDIT, textEditCallback);
mask.removeEventListener('click', maskClickEvent);
textarea.removeEventListener('click', textareaClickEvent);
textarea.removeEventListener('input', textareaInputEvent);
textarea.removeEventListener('blur', textareaBlurEvent);
textarea.removeEventListener('keydown', textareaKeyDownEvent);
textarea.removeEventListener('keypress', textareaKeyPressEvent);
textarea.removeEventListener('keyup', textareaKeyUpEvent);
textarea.removeEventListener('wheel', textareaWheelEvent);
canvasWrapper.removeChild(textarea);
mask.removeChild(canvasWrapper);
container.removeChild(mask);
offEvents();
destroyDOM();
textarea.remove();
canvasWrapper.remove();
mask = null as any;
textarea = null as any;
canvasWrapper = null as any;
mask = null as any;
activeElem = null;
root = null as any;
activeMtrl = null;
activePosition = null as any;
originText = null as any;
}
},
hover: preventAction,
pointStart: preventAction,
pointMove: preventAction,
pointEnd: preventAction,
pointLeave: preventAction,
doubleClick: preventAction,
contextMenu: preventAction,
wheel: preventAction,
wheelScale: preventAction,
scrollX: preventAction,
scrollY: preventAction,
resize: preventAction,
};
};

View file

@ -0,0 +1,26 @@
import { createId, getMiddlewareValidStyles } from '@idraw/util';
import type { MiddlewareTextEditorStyles, MiddlewareTextEditorConfig } from '@idraw/types';
export const key = 'TEXT-EDITOR';
const prefix = `idraw-middleware-text-editor`;
export const getRootClassName = () => `${prefix}-${createId()}`;
export const defaultStyles: MiddlewareTextEditorStyles = {
zIndex: 1,
boxBorderColor: '#0c8ce9',
};
export const classNameMap = {
textarea: `${prefix}-textarea`,
hide: `${prefix}-hide`,
canvasWrapper: `${prefix}-canvas-wrapper`,
};
export function getMiddlewareTextEditorStyles<C = MiddlewareTextEditorConfig, S = MiddlewareTextEditorStyles>(
config: C
): S {
const styles: S = getMiddlewareValidStyles<C, S>(config, ['zIndex', 'boxBorderColor']);
return styles;
}

View file

@ -0,0 +1,26 @@
import type { StrictMaterial, ViewScaleInfo, MaterialPosition, MiddlewareTextEditorStyles } from '@idraw/types';
import { coreEventKeys } from '../../static';
export type TextEditEvent = {
id: string;
};
export type InnerOptions = {
material: StrictMaterial<'text'>;
groupQueue: StrictMaterial<'group'>[];
viewScaleInfo: ViewScaleInfo;
styles: MiddlewareTextEditorStyles;
};
export type TextChangeEvent = {
material: {
id: string;
attributes: {
text: string;
};
};
position: MaterialPosition;
};
export type ExtendEventMap = Record<typeof coreEventKeys.TEXT_EDIT, TextEditEvent> &
Record<typeof coreEventKeys.TEXT_CHANGE, TextChangeEvent>;

View file

@ -1,33 +1,33 @@
import type { RecursivePartial, FlattenElement, Element, ModifyRecord } from '@idraw/types';
import { toFlattenElement, get } from '@idraw/util';
import type { RecursivePartial, FlattenMaterial, Material, ModifyRecord } from '@idraw/types';
import { toFlattenMaterial, get } from '@idraw/util';
export function getModifyElementRecord(opts: {
modifiedElement: RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>;
beforeElement: Element;
}): ModifyRecord<'modifyElement'> {
const { modifiedElement, beforeElement } = opts;
const { uuid, ...restElement } = modifiedElement;
const after = toFlattenElement(restElement);
let before: FlattenElement = {};
export function getModifyMaterialRecord(opts: {
modifiedMaterial: RecursivePartial<Omit<Material, 'id'>> & Pick<Material, 'id'>;
beforeMaterial: Material;
}): ModifyRecord<'modifyMaterial'> {
const { modifiedMaterial, beforeMaterial } = opts;
const { id, ...restMaterial } = modifiedMaterial;
const after = toFlattenMaterial(restMaterial);
let before: FlattenMaterial = {};
Object.keys(after).forEach((key: string) => {
let val = get(beforeElement, key);
if (val === undefined && /(borderRadius|borderWidth)\[[0-9]{1,}\]$/.test(key)) {
let val = get(beforeMaterial, key);
if (val === undefined && /(cornerRadius|strokeWidth)\[[0-9]{1,}\]$/.test(key)) {
key = key.replace(/\[[0-9]{1,}\]$/, '');
val = get(beforeElement, key);
val = get(beforeMaterial, key);
}
before[key] = val;
});
before = toFlattenElement(before);
before = toFlattenMaterial(before);
const record: ModifyRecord<'modifyElement'> = {
type: 'modifyElement',
const record: ModifyRecord<'modifyMaterial'> = {
type: 'modifyMaterial',
time: Date.now(),
content: {
method: 'modifyElement',
uuid,
method: 'modifyMaterial',
id,
before,
after
}
after,
},
};
return record;

View file

@ -1,7 +1,10 @@
export const EVENT_KEY_CHANGE = 'change';
export const EVENT_KEY_CHANGING = 'changing';
export const EVENT_KEY_CURSOR = 'cursor';
export const EVENT_KEY_RULER = 'ruler';
export const EVENT_KEY_SCALE = 'scale';
export const EVENT_KEY_CREATE = 'create';
export const EVENT_KEY_CLEAR_CREATE = 'clearCreate';
export const EVENT_KEY_SELECT = 'select';
export const EVENT_KEY_SELECT_LAYOUT = 'selectLayout';
export const EVENT_KEY_CLEAR_SELECT = 'clearSelect';
@ -10,27 +13,43 @@ export const EVENT_KEY_TEXT_CHANGE = 'textChange';
export const EVENT_KEY_CONTEXT_MENU = 'contextMenu';
export const EVENT_KEY_SELECT_IN_GROUP = 'selectInGroup';
export const EVENT_KEY_SNAP_TO_GRID = 'snapToGrid';
export const EVENT_KEY_PATH_EDIT = 'pathEdit';
export const EVENT_CLEAR_PATH_EDIT = 'clearPathEdit';
export const EVENT_KEY_PATH_CREATE = 'pathCreate';
export const EVENT_CLEAR_PATH_CREATE = 'clearPathCreate';
export const EVENT_KEY_MODE_CHANGE = 'modeChange';
export type CoreEventKeys = {
CURSOR: typeof EVENT_KEY_CURSOR;
CHANGE: typeof EVENT_KEY_CHANGE;
CHANGING: typeof EVENT_KEY_CHANGING;
RULER: typeof EVENT_KEY_RULER;
SCALE: typeof EVENT_KEY_SCALE;
SELECT: typeof EVENT_KEY_SELECT;
SELECT_LAYOUT: typeof EVENT_KEY_SELECT_LAYOUT;
CLEAR_SELECT: typeof EVENT_KEY_CLEAR_SELECT;
CREATE: typeof EVENT_KEY_CREATE;
CLEAR_CREATE: typeof EVENT_KEY_CLEAR_CREATE;
TEXT_EDIT: typeof EVENT_KEY_TEXT_EDIT;
TEXT_CHANGE: typeof EVENT_KEY_TEXT_CHANGE;
CONTEXT_MENU: typeof EVENT_KEY_CONTEXT_MENU;
SELECT_IN_GROUP: typeof EVENT_KEY_SELECT_IN_GROUP;
SNAP_TO_GRID: typeof EVENT_KEY_SELECT_IN_GROUP;
PATH_EDIT: typeof EVENT_KEY_PATH_EDIT;
CLEAR_PATH_EDIT: typeof EVENT_CLEAR_PATH_EDIT;
PATH_CREATE: typeof EVENT_KEY_PATH_CREATE;
CLEAR_PATH_CREATE: typeof EVENT_CLEAR_PATH_CREATE;
MODE_CHANGE: typeof EVENT_KEY_MODE_CHANGE;
};
const innerEventKeys: CoreEventKeys = {
CURSOR: EVENT_KEY_CURSOR,
CHANGE: EVENT_KEY_CHANGE,
CHANGING: EVENT_KEY_CHANGING,
RULER: EVENT_KEY_RULER,
SCALE: EVENT_KEY_SCALE,
CREATE: EVENT_KEY_CREATE,
CLEAR_CREATE: EVENT_KEY_CLEAR_CREATE,
SELECT_LAYOUT: EVENT_KEY_SELECT_LAYOUT,
SELECT: EVENT_KEY_SELECT,
CLEAR_SELECT: EVENT_KEY_CLEAR_SELECT,
@ -38,14 +57,19 @@ const innerEventKeys: CoreEventKeys = {
TEXT_CHANGE: EVENT_KEY_TEXT_CHANGE,
CONTEXT_MENU: EVENT_KEY_CONTEXT_MENU,
SELECT_IN_GROUP: EVENT_KEY_SELECT_IN_GROUP,
SNAP_TO_GRID: EVENT_KEY_SELECT_IN_GROUP
SNAP_TO_GRID: EVENT_KEY_SELECT_IN_GROUP,
PATH_EDIT: EVENT_KEY_PATH_EDIT,
CLEAR_PATH_EDIT: EVENT_CLEAR_PATH_EDIT,
PATH_CREATE: EVENT_KEY_PATH_CREATE,
CLEAR_PATH_CREATE: EVENT_CLEAR_PATH_CREATE,
MODE_CHANGE: EVENT_KEY_MODE_CHANGE,
};
const coreEventKeys = {} as CoreEventKeys;
Object.keys(innerEventKeys).forEach((keyName: string) => {
Object.defineProperty(coreEventKeys, keyName, {
value: innerEventKeys[keyName as keyof CoreEventKeys],
writable: false
writable: false,
});
});

View file

@ -1,6 +1,6 @@
{
"name": "@idraw/figma",
"version": "0.4.0",
"version": "1.0.0",
"description": "",
"main": "dist/esm/index.js",
"module": "dist/esm/index.js",
@ -11,8 +11,8 @@
"dist/**/*.js"
],
"dependencies": {
"@idraw/types": "workspace:^0.4",
"@idraw/util": "workspace:^0.4",
"@idraw/types": "workspace:*",
"@idraw/util": "workspace:*",
"kiwi-schema": "^0.5.0",
"matrix-inverse": "^2.0.0",
"pako": "^2.1.0",
@ -20,7 +20,7 @@
},
"devDependencies": {
"@types/pako": "^2.0.3",
"@idraw/types": "workspace:^0.4"
"@idraw/types": "workspace:*"
},
"repository": {
"type": "git",

File diff suppressed because one or more lines are too long

View file

@ -1,171 +0,0 @@
import { iDraw, useHistory, deepClone, createElement, findElementFromListByPosition } from 'idraw';
const createData = () => ({
elements: [
createElement('rect', {
uuid: 'test-001',
x: 0,
y: 0,
w: 100,
h: 100,
detail: {
background: '#DDDDDD'
}
}),
createElement('group', {
uuid: 'test-005',
detail: {
children: [
createElement('image', { uuid: 'test-004', detail: { src: 'https://example.com/001.png' } }),
createElement('circle', { uuid: 'test-007' }),
createElement('text', {
uuid: 'test-008',
detail: {
text: 'Text in Group'
}
}),
createElement('image', { uuid: 'test-009', detail: { src: 'https://example.com/002.png' } })
]
}
})
]
});
describe('idraw: useHistory ', () => {
beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
});
test('updateElement', () => {
const data = createData();
const div = document.createElement('div') as HTMLDivElement;
const idraw = new iDraw(div, {
height: 200,
width: 200
});
const { MiddlewareHistory, historyHandler } = useHistory({ core: idraw.getCore() });
const { undo, redo, __getDoRecords, __getUndoRecords } = historyHandler;
idraw.use(MiddlewareHistory);
idraw.setData(data);
// modify 1: do
const newElement1 = idraw.createElement('rect', {
x: 22,
y: 33,
h: 300,
w: 400,
name: 'new element 001',
detail: {
background: '#666666'
}
});
const position = [1, 2];
idraw.addElement(newElement1, {
position
});
const record1 = {
type: 'addElement',
time: new Date().getTime(),
content: {
method: 'addElement',
uuid: newElement1.uuid,
position: [...position],
element: deepClone(newElement1)
}
};
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(newElement1);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 2: do
const newElement2 = idraw.createElement('text', {
x: 22,
y: 33,
h: 300,
w: 400,
name: 'new element 002',
detail: {
text: 'Hello Element'
}
});
idraw.addElement(newElement2, { position });
const record2 = {
type: 'addElement',
time: new Date().getTime(),
content: {
method: 'addElement',
uuid: newElement2.uuid,
position: [...position],
element: deepClone(newElement2)
}
};
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(newElement2);
expect(__getDoRecords()).toStrictEqual([record1, record2]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 3: undo
undo();
const record3 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'deleteElement',
uuid: record2.content.uuid,
position: deepClone(record2.content.position),
element: deepClone(record2.content.element)
}
};
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(newElement1);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([record3]);
// modify 4: undo
undo();
const record4 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'deleteElement',
uuid: record1.content.uuid,
position: deepClone(record1.content.position),
element: deepClone(record1.content.element)
}
};
expect(idraw.getData()).toStrictEqual(createData());
expect(__getDoRecords()).toStrictEqual([]);
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
// modify 5: redo
redo();
const record5 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'addElement',
uuid: record4.content.uuid,
position: record4.content.position,
element: deepClone(record4.content.element)
}
};
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(newElement1);
expect(__getDoRecords()).toStrictEqual([record5]);
expect(__getUndoRecords()).toStrictEqual([record3]);
// modify 5: redo
redo();
const record6 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'addElement',
uuid: record3.content.uuid,
position: record3.content.position,
element: deepClone(record3.content.element)
}
};
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(newElement2);
expect(__getDoRecords()).toStrictEqual([record5, record6]);
expect(__getUndoRecords()).toStrictEqual([]);
});
});

View file

@ -0,0 +1,161 @@
import { iDraw, useHistory, deepClone, createMaterial, findMaterialFromListByPosition } from 'idraw';
const createData = () => ({
materials: [
createMaterial('rect', {
id: 'test-001',
x: 0,
y: 0,
width: 100,
height: 100,
fill: '#DDDDDD',
}),
createMaterial('group', {
id: 'test-005',
children: [
createMaterial('image', { id: 'test-004', src: 'https://example.com/001.png' }),
createMaterial('circle', { id: 'test-007' }),
createMaterial('text', {
id: 'test-008',
text: 'Text in Group',
}),
createMaterial('image', { id: 'test-009', src: 'https://example.com/002.png' }),
],
}),
],
});
describe('idraw: useHistory ', () => {
beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
});
test('updateMaterial', () => {
const data = createData();
const div = document.createElement('div') as HTMLDivElement;
const idraw = new iDraw(div, {
height: 200,
width: 200,
});
const { MiddlewareHistory, historyHandler } = useHistory({ core: idraw.getCore() });
const { undo, redo, __getDoRecords, __getUndoRecords } = historyHandler;
idraw.use(MiddlewareHistory);
idraw.setData(data);
// modify 1: do
const newMaterial1 = idraw.createMaterial('rect', {
x: 22,
y: 33,
height: 300,
width: 400,
name: 'new material 001',
fill: '#666666',
});
const position = [1, 2];
idraw.addMaterial(newMaterial1, {
position,
});
const record1 = {
type: 'addMaterial',
time: new Date().getTime(),
content: {
method: 'addMaterial',
id: newMaterial1.id,
position: [...position],
material: deepClone(newMaterial1),
},
};
expect(findMaterialFromListByPosition(position, idraw.getData()?.materials || [])).toStrictEqual(newMaterial1);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 2: do
const newMaterial2 = idraw.createMaterial('text', {
x: 22,
y: 33,
height: 300,
width: 400,
name: 'new material 002',
text: 'Hello Material',
});
idraw.addMaterial(newMaterial2, { position });
const record2 = {
type: 'addMaterial',
time: new Date().getTime(),
content: {
method: 'addMaterial',
id: newMaterial2.id,
position: [...position],
material: deepClone(newMaterial2),
},
};
expect(findMaterialFromListByPosition(position, idraw.getData()?.materials || [])).toStrictEqual(newMaterial2);
expect(__getDoRecords()).toStrictEqual([record1, record2]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 3: undo
undo();
const record3 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'deleteMaterial',
id: record2.content.id,
position: deepClone(record2.content.position),
material: deepClone(record2.content.material),
},
};
expect(findMaterialFromListByPosition(position, idraw.getData()?.materials || [])).toStrictEqual(newMaterial1);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([record3]);
// modify 4: undo
undo();
const record4 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'deleteMaterial',
id: record1.content.id,
position: deepClone(record1.content.position),
material: deepClone(record1.content.material),
},
};
expect(idraw.getData()).toStrictEqual(createData());
expect(__getDoRecords()).toStrictEqual([]);
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
// modify 5: redo
redo();
const record5 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'addMaterial',
id: record4.content.id,
position: record4.content.position,
material: deepClone(record4.content.material),
},
};
expect(findMaterialFromListByPosition(position, idraw.getData()?.materials || [])).toStrictEqual(newMaterial1);
expect(__getDoRecords()).toStrictEqual([record5]);
expect(__getUndoRecords()).toStrictEqual([record3]);
// modify 5: redo
redo();
const record6 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'addMaterial',
id: record3.content.id,
position: record3.content.position,
material: deepClone(record3.content.material),
},
};
expect(findMaterialFromListByPosition(position, idraw.getData()?.materials || [])).toStrictEqual(newMaterial2);
expect(__getDoRecords()).toStrictEqual([record5, record6]);
expect(__getUndoRecords()).toStrictEqual([]);
});
});

View file

@ -1,158 +0,0 @@
import { iDraw, useHistory, deepClone, createElement, findElementFromListByPosition } from 'idraw';
import type { Element } from 'idraw';
const createData = () => ({
elements: [
createElement('rect', {
uuid: 'test-000',
x: 0,
y: 0,
w: 100,
h: 100,
detail: {
background: '#DDDDDD'
}
}),
createElement('group', {
uuid: 'test-001',
detail: {
children: [
createElement('image', { uuid: 'test-001-000', detail: { src: 'https://example.com/001.png' } }),
createElement('circle', { uuid: 'test-001-001' }),
createElement('text', {
uuid: 'test-001-002',
detail: {
text: 'Text in Group'
}
}),
createElement('image', { uuid: 'test-001-003', detail: { src: 'https://example.com/002.png' } }),
createElement('rect', { uuid: 'test-001-004' }),
createElement('circle', { uuid: 'test-001-005' })
]
}
})
]
});
describe('idraw: useHistory ', () => {
beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
});
test('updateElement', () => {
const data = createData();
const div = document.createElement('div') as HTMLDivElement;
const idraw = new iDraw(div, {
height: 200,
width: 200
});
const { MiddlewareHistory, historyHandler } = useHistory({ core: idraw.getCore() });
const { undo, redo, __getDoRecords, __getUndoRecords } = historyHandler;
idraw.use(MiddlewareHistory);
idraw.setData(data);
const position = [1, 2];
const nextPosition = [1, 3];
// modify 1: do
const deletedElem1 = deepClone(findElementFromListByPosition(position, data.elements) as Element);
const expectedElem1 = deepClone(findElementFromListByPosition(nextPosition, data.elements) as Element);
idraw.deleteElement(deletedElem1?.uuid);
const record1 = {
type: 'deleteElement',
time: new Date().getTime(),
content: {
method: 'deleteElement',
uuid: deletedElem1.uuid,
position: [...position],
element: deepClone(deletedElem1)
}
};
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(expectedElem1);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 2: do
const deletedElem2 = deepClone(findElementFromListByPosition(position, data.elements) as Element);
const expectedElem2 = deepClone(findElementFromListByPosition(nextPosition, data.elements) as Element);
idraw.deleteElement(deletedElem2?.uuid);
const record2 = {
type: 'deleteElement',
time: new Date().getTime(),
content: {
method: 'deleteElement',
uuid: deletedElem2.uuid,
position: [...position],
element: deepClone(deletedElem2)
}
};
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(expectedElem2);
expect(__getDoRecords()).toStrictEqual([record1, record2]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 3: undo
undo();
const record3 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'addElement',
uuid: record2.content.uuid,
position: deepClone(record2.content.position),
element: deepClone(record2.content.element)
}
};
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(deletedElem2);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([record3]);
// modify 4: undo
undo();
const record4 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'addElement',
uuid: record1.content.uuid,
position: deepClone(record1.content.position),
element: deepClone(record1.content.element)
}
};
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(deletedElem1);
expect(__getDoRecords()).toStrictEqual([]);
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
// modify 5: redo
redo();
const record5 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'deleteElement',
uuid: record4.content.uuid,
position: record4.content.position,
element: deepClone(record4.content.element)
}
};
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(expectedElem1);
expect(__getDoRecords()).toStrictEqual([record5]);
expect(__getUndoRecords()).toStrictEqual([record3]);
// modify 5: redo
redo();
const record6 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'deleteElement',
uuid: record3.content.uuid,
position: record3.content.position,
element: deepClone(record3.content.element)
}
};
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(expectedElem2);
expect(__getDoRecords()).toStrictEqual([record5, record6]);
expect(__getUndoRecords()).toStrictEqual([]);
});
});

View file

@ -0,0 +1,152 @@
import { iDraw, useHistory, deepClone, createMaterial, findMaterialFromListByPosition } from 'idraw';
import type { Material } from 'idraw';
const createData = () => ({
materials: [
createMaterial('rect', {
id: 'test-000',
x: 0,
y: 0,
width: 100,
height: 100,
fill: '#DDDDDD',
}),
createMaterial('group', {
id: 'test-001',
children: [
createMaterial('image', { id: 'test-001-000', src: 'https://example.com/001.png' }),
createMaterial('circle', { id: 'test-001-001' }),
createMaterial('text', {
id: 'test-001-002',
text: 'Text in Group',
}),
createMaterial('image', { id: 'test-001-003', src: 'https://example.com/002.png' }),
createMaterial('rect', { id: 'test-001-004' }),
createMaterial('circle', { id: 'test-001-005' }),
],
}),
],
});
describe('idraw: useHistory ', () => {
beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
});
test('updateMaterial', () => {
const data = createData();
const div = document.createElement('div') as HTMLDivElement;
const idraw = new iDraw(div, {
height: 200,
width: 200,
});
const { MiddlewareHistory, historyHandler } = useHistory({ core: idraw.getCore() });
const { undo, redo, __getDoRecords, __getUndoRecords } = historyHandler;
idraw.use(MiddlewareHistory);
idraw.setData(data);
const position = [1, 2];
const nextPosition = [1, 3];
// modify 1: do
const deletedElem1 = deepClone(findMaterialFromListByPosition(position, data.materials) as Material);
const expectedElem1 = deepClone(findMaterialFromListByPosition(nextPosition, data.materials) as Material);
idraw.deleteMaterial(deletedElem1?.id);
const record1 = {
type: 'deleteMaterial',
time: new Date().getTime(),
content: {
method: 'deleteMaterial',
id: deletedElem1.id,
position: [...position],
material: deepClone(deletedElem1),
},
};
expect(findMaterialFromListByPosition(position, idraw.getData()?.materials || [])).toStrictEqual(expectedElem1);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 2: do
const deletedElem2 = deepClone(findMaterialFromListByPosition(position, data.materials) as Material);
const expectedElem2 = deepClone(findMaterialFromListByPosition(nextPosition, data.materials) as Material);
idraw.deleteMaterial(deletedElem2?.id);
const record2 = {
type: 'deleteMaterial',
time: new Date().getTime(),
content: {
method: 'deleteMaterial',
id: deletedElem2.id,
position: [...position],
material: deepClone(deletedElem2),
},
};
expect(findMaterialFromListByPosition(position, idraw.getData()?.materials || [])).toStrictEqual(expectedElem2);
expect(__getDoRecords()).toStrictEqual([record1, record2]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 3: undo
undo();
const record3 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'addMaterial',
id: record2.content.id,
position: deepClone(record2.content.position),
material: deepClone(record2.content.material),
},
};
expect(findMaterialFromListByPosition(position, idraw.getData()?.materials || [])).toStrictEqual(deletedElem2);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([record3]);
// modify 4: undo
undo();
const record4 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'addMaterial',
id: record1.content.id,
position: deepClone(record1.content.position),
material: deepClone(record1.content.material),
},
};
expect(findMaterialFromListByPosition(position, idraw.getData()?.materials || [])).toStrictEqual(deletedElem1);
expect(__getDoRecords()).toStrictEqual([]);
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
// modify 5: redo
redo();
const record5 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'deleteMaterial',
id: record4.content.id,
position: record4.content.position,
material: deepClone(record4.content.material),
},
};
expect(findMaterialFromListByPosition(position, idraw.getData()?.materials || [])).toStrictEqual(expectedElem1);
expect(__getDoRecords()).toStrictEqual([record5]);
expect(__getUndoRecords()).toStrictEqual([record3]);
// modify 5: redo
redo();
const record6 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'deleteMaterial',
id: record3.content.id,
position: record3.content.position,
material: deepClone(record3.content.material),
},
};
expect(findMaterialFromListByPosition(position, idraw.getData()?.materials || [])).toStrictEqual(expectedElem2);
expect(__getDoRecords()).toStrictEqual([record5, record6]);
expect(__getUndoRecords()).toStrictEqual([]);
});
});

View file

@ -1,44 +1,36 @@
import { iDraw, useHistory, deepClone, createElement, set, get, toFlattenGlobal } from 'idraw';
import { iDraw, useHistory, deepClone, createMaterial, set, get, toFlattenGlobal } from 'idraw';
import type { Data, DataGlobal, RecursivePartial } from 'idraw';
const createData = () =>
({
elements: [
createElement('rect', {
uuid: 'test-001',
materials: [
createMaterial('rect', {
id: 'test-001',
x: 0,
y: 0,
w: 100,
h: 100,
detail: {
background: '#DDDDDD'
}
width: 100,
height: 100,
fill: '#DDDDDD',
}),
createElement('circle', { uuid: 'test-002' }),
createElement('text', {
uuid: 'test-003',
detail: {
text: 'Hello World'
}
createMaterial('circle', { id: 'test-002' }),
createMaterial('text', {
id: 'test-003',
text: 'Hello World',
}),
createElement('image', { uuid: 'test-004', detail: { src: 'https://example.com/001.png' } }),
createElement('group', {
uuid: 'test-005',
detail: {
children: [
createElement('rect', { uuid: 'test-006' }),
createElement('circle', { uuid: 'test-007' }),
createElement('text', {
uuid: 'test-008',
detail: {
text: 'Text in Group'
}
}),
createElement('image', { uuid: 'test-009', detail: { src: 'https://example.com/002.png' } })
]
}
})
]
createMaterial('image', { id: 'test-004', src: 'https://example.com/001.png' }),
createMaterial('group', {
id: 'test-005',
children: [
createMaterial('rect', { id: 'test-006' }),
createMaterial('circle', { id: 'test-007' }),
createMaterial('text', {
id: 'test-008',
text: 'Text in Group',
}),
createMaterial('image', { id: 'test-009', src: 'https://example.com/002.png' }),
],
}),
],
} as Data);
describe('idraw: useHistory ', () => {
@ -52,7 +44,7 @@ describe('idraw: useHistory ', () => {
const idraw = new iDraw(div, {
height: 200,
width: 200
width: 200,
});
const { MiddlewareHistory, historyHandler } = useHistory({ core: idraw.getCore() });
const { undo, redo, __getDoRecords, __getUndoRecords } = historyHandler;
@ -61,14 +53,14 @@ describe('idraw: useHistory ', () => {
// modify 1: do
const modifiedInfo1 = {
background: '#123456'
fill: '#123456',
};
idraw.modifyGlobal({
...deepClone(modifiedInfo1)
...deepClone(modifiedInfo1),
});
const expectedData1 = createData();
const flattenModifiedInfo1 = toFlattenGlobal(modifiedInfo1);
const beforeInfo1: Record<string, any> | null = null;
const beforeInfo1: Record<string, unknown> | null = null;
const afterInfo1 = { ...flattenModifiedInfo1 };
Object.keys(flattenModifiedInfo1).forEach((k) => {
@ -81,8 +73,8 @@ describe('idraw: useHistory ', () => {
content: {
method: 'modifyGlobal',
before: beforeInfo1,
after: afterInfo1
}
after: afterInfo1,
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record1]);
@ -90,14 +82,14 @@ describe('idraw: useHistory ', () => {
// modify 2: do
const modifiedInfo2 = {
background: '#AAAAAA'
fill: '#AAAAAA',
} as unknown as RecursivePartial<DataGlobal>;
idraw.modifyGlobal({ ...modifiedInfo2 });
const expectedData2 = deepClone(expectedData1);
const flattenModifiedInfo2 = toFlattenGlobal(modifiedInfo2);
const beforeInfo2: Record<string, any> = {};
const beforeInfo2: Record<string, unknown> = {};
const afterInfo2 = { ...flattenModifiedInfo2 };
Object.keys(flattenModifiedInfo2).forEach((key) => {
@ -110,8 +102,8 @@ describe('idraw: useHistory ', () => {
content: {
method: 'modifyGlobal',
before: beforeInfo2,
after: afterInfo2
}
after: afterInfo2,
},
};
expect(idraw.getData()).toStrictEqual(expectedData2);
expect(__getDoRecords()).toStrictEqual([record1, record2]);
@ -125,8 +117,8 @@ describe('idraw: useHistory ', () => {
content: {
method: 'modifyGlobal',
before: deepClone(record2.content.after),
after: deepClone(record2.content.before)
}
after: deepClone(record2.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record1]);
@ -140,8 +132,8 @@ describe('idraw: useHistory ', () => {
content: {
method: 'modifyGlobal',
before: deepClone(record1.content.after),
after: deepClone(record1.content.before)
}
after: deepClone(record1.content.before),
},
};
expect(idraw.getData()).toStrictEqual(createData());
expect(__getDoRecords()).toStrictEqual([]);
@ -155,8 +147,8 @@ describe('idraw: useHistory ', () => {
content: {
method: 'modifyGlobal',
before: deepClone(record4.content.after),
after: deepClone(record4.content.before)
}
after: deepClone(record4.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record5]);
@ -170,8 +162,8 @@ describe('idraw: useHistory ', () => {
content: {
method: 'modifyGlobal',
before: deepClone(record3.content.after),
after: deepClone(record3.content.before)
}
after: deepClone(record3.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData2);
expect(__getDoRecords()).toStrictEqual([record5, record6]);

View file

@ -1,44 +1,36 @@
import { iDraw, useHistory, deepClone, createElement, set, get, toFlattenLayout } from 'idraw';
import { iDraw, useHistory, deepClone, createMaterial, set, get, toFlattenLayout } from 'idraw';
import type { Data, DataLayout, RecursivePartial } from 'idraw';
const createData = () =>
({
elements: [
createElement('rect', {
uuid: 'test-001',
materials: [
createMaterial('rect', {
id: 'test-001',
x: 0,
y: 0,
w: 100,
h: 100,
detail: {
background: '#DDDDDD'
}
width: 100,
height: 100,
fill: '#DDDDDD',
}),
createElement('circle', { uuid: 'test-002' }),
createElement('text', {
uuid: 'test-003',
detail: {
text: 'Hello World'
}
createMaterial('circle', { id: 'test-002' }),
createMaterial('text', {
id: 'test-003',
text: 'Hello World',
}),
createElement('image', { uuid: 'test-004', detail: { src: 'https://example.com/001.png' } }),
createElement('group', {
uuid: 'test-005',
detail: {
children: [
createElement('rect', { uuid: 'test-006' }),
createElement('circle', { uuid: 'test-007' }),
createElement('text', {
uuid: 'test-008',
detail: {
text: 'Text in Group'
}
}),
createElement('image', { uuid: 'test-009', detail: { src: 'https://example.com/002.png' } })
]
}
})
]
createMaterial('image', { id: 'test-004', src: 'https://example.com/001.png' }),
createMaterial('group', {
id: 'test-005',
children: [
createMaterial('rect', { id: 'test-006' }),
createMaterial('circle', { id: 'test-007' }),
createMaterial('text', {
id: 'test-008',
text: 'Text in Group',
}),
createMaterial('image', { id: 'test-009', src: 'https://example.com/002.png' }),
],
}),
],
} as Data);
describe('idraw: useHistory ', () => {
@ -52,7 +44,7 @@ describe('idraw: useHistory ', () => {
const idraw = new iDraw(div, {
height: 200,
width: 200
width: 200,
});
const { MiddlewareHistory, historyHandler } = useHistory({ core: idraw.getCore() });
const { undo, redo, __getDoRecords, __getUndoRecords } = historyHandler;
@ -63,19 +55,17 @@ describe('idraw: useHistory ', () => {
const modifiedInfo1 = {
x: 1,
y: 2,
w: 100,
h: 200,
detail: {
background: '#123456',
borderRadius: 3
}
width: 100,
height: 200,
fill: '#123456',
cornerRadius: 3,
};
idraw.modifyLayout({
...deepClone(modifiedInfo1)
...deepClone(modifiedInfo1),
});
const expectedData1 = createData();
const flattenModifiedInfo1 = toFlattenLayout(modifiedInfo1);
const beforeInfo1: Record<string, any> | null = null;
const beforeInfo1: Record<string, unknown> | null = null;
const afterInfo1 = { ...flattenModifiedInfo1 };
Object.keys(flattenModifiedInfo1).forEach((k) => {
@ -88,8 +78,8 @@ describe('idraw: useHistory ', () => {
content: {
method: 'modifyLayout',
before: beforeInfo1,
after: afterInfo1
}
after: afterInfo1,
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record1]);
@ -99,22 +89,20 @@ describe('idraw: useHistory ', () => {
const modifiedInfo2 = {
x: modifiedInfo1.x + 3,
y: modifiedInfo1.y + 4,
detail: {
borderRadius: [2, 4, 6, 8]
}
cornerRadius: [2, 4, 6, 8],
} as unknown as RecursivePartial<DataLayout>;
idraw.modifyLayout({ ...modifiedInfo2 });
const expectedData2 = deepClone(expectedData1);
const flattenModifiedInfo2 = toFlattenLayout(modifiedInfo2);
const beforeInfo2: Record<string, any> = {};
const beforeInfo2: Record<string, unknown> = {};
const afterInfo2 = { ...flattenModifiedInfo2 };
Object.keys(flattenModifiedInfo2).forEach((key) => {
let beforeVal = get(expectedData1.layout, key);
let beforeKey = key;
if (beforeVal === undefined && /(borderRadius|borderWidth)\[[0-9]{1,}\]$/.test(beforeKey)) {
if (beforeVal === undefined && /(cornerRadius|strokeWidth)\[[0-9]{1,}\]$/.test(beforeKey)) {
beforeKey = beforeKey.replace(/\[[0-9]{1,}\]$/, '');
beforeVal = get(expectedData1.layout, beforeKey);
}
@ -127,8 +115,8 @@ describe('idraw: useHistory ', () => {
content: {
method: 'modifyLayout',
before: beforeInfo2,
after: afterInfo2
}
after: afterInfo2,
},
};
expect(idraw.getData()).toStrictEqual(expectedData2);
expect(__getDoRecords()).toStrictEqual([record1, record2]);
@ -142,8 +130,8 @@ describe('idraw: useHistory ', () => {
content: {
method: 'modifyLayout',
before: deepClone(record2.content.after),
after: deepClone(record2.content.before)
}
after: deepClone(record2.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record1]);
@ -157,8 +145,8 @@ describe('idraw: useHistory ', () => {
content: {
method: 'modifyLayout',
before: deepClone(record1.content.after),
after: deepClone(record1.content.before)
}
after: deepClone(record1.content.before),
},
};
expect(idraw.getData()).toStrictEqual(createData());
expect(__getDoRecords()).toStrictEqual([]);
@ -172,8 +160,8 @@ describe('idraw: useHistory ', () => {
content: {
method: 'modifyLayout',
before: deepClone(record4.content.after),
after: deepClone(record4.content.before)
}
after: deepClone(record4.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record5]);
@ -187,8 +175,8 @@ describe('idraw: useHistory ', () => {
content: {
method: 'modifyLayout',
before: deepClone(record3.content.after),
after: deepClone(record3.content.before)
}
after: deepClone(record3.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData2);
expect(__getDoRecords()).toStrictEqual([record5, record6]);

View file

@ -1,43 +1,35 @@
import { iDraw, useHistory, deepClone, createElement, set, get, toFlattenElement } from 'idraw';
import type { RecursivePartial, Element } from 'idraw';
import { iDraw, useHistory, deepClone, createMaterial, set, get, toFlattenMaterial } from 'idraw';
import type { RecursivePartial, Material } from 'idraw';
const createData = () => ({
elements: [
createElement('rect', {
uuid: 'test-001',
materials: [
createMaterial('rect', {
id: 'test-001',
x: 0,
y: 0,
w: 100,
h: 100,
detail: {
background: '#DDDDDD'
}
width: 100,
height: 100,
fill: '#DDDDDD',
}),
createElement('circle', { uuid: 'test-002' }),
createElement('text', {
uuid: 'test-003',
detail: {
text: 'Hello World'
}
createMaterial('circle', { id: 'test-002' }),
createMaterial('text', {
id: 'test-003',
text: 'Hello World',
}),
createElement('image', { uuid: 'test-004', detail: { src: 'https://example.com/001.png' } }),
createElement('group', {
uuid: 'test-005',
detail: {
children: [
createElement('rect', { uuid: 'test-006' }),
createElement('circle', { uuid: 'test-007' }),
createElement('text', {
uuid: 'test-008',
detail: {
text: 'Text in Group'
}
}),
createElement('image', { uuid: 'test-009', detail: { src: 'https://example.com/002.png' } })
]
}
})
]
createMaterial('image', { id: 'test-004', src: 'https://example.com/001.png' }),
createMaterial('group', {
id: 'test-005',
children: [
createMaterial('rect', { id: 'test-006' }),
createMaterial('circle', { id: 'test-007' }),
createMaterial('text', {
id: 'test-008',
text: 'Text in Group',
}),
createMaterial('image', { id: 'test-009', src: 'https://example.com/002.png' }),
],
}),
],
});
describe('idraw: useHistory ', () => {
@ -45,50 +37,48 @@ describe('idraw: useHistory ', () => {
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
});
test('modifyElement', () => {
test('modifyMaterial', () => {
const data = createData();
const div = document.createElement('div') as HTMLDivElement;
const idraw = new iDraw(div, {
height: 200,
width: 200
width: 200,
});
const { MiddlewareHistory, historyHandler } = useHistory({ core: idraw.getCore() });
const { undo, redo, __getDoRecords, __getUndoRecords } = historyHandler;
idraw.use(MiddlewareHistory);
idraw.setData(data);
const targetElement = deepClone(data.elements[0]);
const targetMaterial = deepClone(data.materials[0]);
// modify 1: do
const modifiedInfo1 = {
x: targetElement.x + 1,
y: targetElement.y + 2,
detail: {
background: '#123456',
borderRadius: 3
}
x: targetMaterial.x + 1,
y: targetMaterial.y + 2,
fill: '#123456',
cornerRadius: 3,
};
idraw.modifyElement({
uuid: targetElement.uuid,
...deepClone(modifiedInfo1)
idraw.modifyMaterial({
id: targetMaterial.id,
...deepClone(modifiedInfo1),
});
const expectedData1 = createData();
const flattenModifiedInfo1 = toFlattenElement(modifiedInfo1);
const beforeInfo1: Record<string, any> = {};
const flattenModifiedInfo1 = toFlattenMaterial(modifiedInfo1);
const beforeInfo1: Record<string, unknown> = {};
const afterInfo1 = { ...flattenModifiedInfo1 };
Object.keys(flattenModifiedInfo1).forEach((key) => {
beforeInfo1[key] = get(expectedData1.elements[0], key);
set(expectedData1.elements[0], key, flattenModifiedInfo1[key]);
beforeInfo1[key] = get(expectedData1.materials[0], key);
set(expectedData1.materials[0], key, flattenModifiedInfo1[key]);
});
const record1 = {
type: 'modifyElement',
type: 'modifyMaterial',
time: new Date().getTime(),
content: {
method: 'modifyElement',
uuid: targetElement.uuid,
method: 'modifyMaterial',
id: targetMaterial.id,
before: beforeInfo1,
after: afterInfo1
}
after: afterInfo1,
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record1]);
@ -98,40 +88,38 @@ describe('idraw: useHistory ', () => {
const modifiedInfo2 = {
x: modifiedInfo1.x + 3,
y: modifiedInfo1.y + 4,
detail: {
borderRadius: [2, 4, 6, 8]
}
} as unknown as RecursivePartial<Omit<Element, 'uuid'>>;
cornerRadius: [2, 4, 6, 8],
} as unknown as RecursivePartial<Omit<Material, 'id'>>;
idraw.modifyElement({
uuid: targetElement.uuid,
...deepClone(modifiedInfo2)
} as RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>);
idraw.modifyMaterial({
id: targetMaterial.id,
...deepClone(modifiedInfo2),
} as RecursivePartial<Omit<Material, 'id'>> & Pick<Material, 'id'>);
const expectedData2 = deepClone(expectedData1);
const flattenModifiedInfo2 = toFlattenElement(modifiedInfo2);
const beforeInfo2: Record<string, any> = {};
const flattenModifiedInfo2 = toFlattenMaterial(modifiedInfo2);
const beforeInfo2: Record<string, unknown> = {};
const afterInfo2 = { ...flattenModifiedInfo2 };
Object.keys(flattenModifiedInfo2).forEach((key) => {
let beforeVal = get(expectedData1.elements[0], key);
let beforeVal = get(expectedData1.materials[0], key);
let beforeKey = key;
if (beforeVal === undefined && /(borderRadius|borderWidth)\[[0-9]{1,}\]$/.test(beforeKey)) {
if (beforeVal === undefined && /(cornerRadius|strokeWidth)\[[0-9]{1,}\]$/.test(beforeKey)) {
beforeKey = beforeKey.replace(/\[[0-9]{1,}\]$/, '');
beforeVal = get(expectedData1.elements[0], beforeKey);
beforeVal = get(expectedData1.materials[0], beforeKey);
}
beforeInfo2[beforeKey] = beforeVal;
set(expectedData2.elements[0], key, flattenModifiedInfo2[key]);
set(expectedData2.materials[0], key, flattenModifiedInfo2[key]);
});
const record2 = {
type: 'modifyElement',
type: 'modifyMaterial',
time: new Date().getTime(),
content: {
method: 'modifyElement',
uuid: targetElement.uuid,
method: 'modifyMaterial',
id: targetMaterial.id,
before: beforeInfo2,
after: afterInfo2
}
after: afterInfo2,
},
};
expect(idraw.getData()).toStrictEqual(expectedData2);
expect(__getDoRecords()).toStrictEqual([record1, record2]);
@ -143,11 +131,11 @@ describe('idraw: useHistory ', () => {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'modifyElement',
uuid: targetElement.uuid,
method: 'modifyMaterial',
id: targetMaterial.id,
before: deepClone(record2.content.after),
after: deepClone(record2.content.before)
}
after: deepClone(record2.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record1]);
@ -159,11 +147,11 @@ describe('idraw: useHistory ', () => {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'modifyElement',
uuid: targetElement.uuid,
method: 'modifyMaterial',
id: targetMaterial.id,
before: deepClone(record1.content.after),
after: deepClone(record1.content.before)
}
after: deepClone(record1.content.before),
},
};
expect(idraw.getData()).toStrictEqual(createData());
expect(__getDoRecords()).toStrictEqual([]);
@ -175,11 +163,11 @@ describe('idraw: useHistory ', () => {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'modifyElement',
uuid: targetElement.uuid,
method: 'modifyMaterial',
id: targetMaterial.id,
before: deepClone(record4.content.after),
after: deepClone(record4.content.before)
}
after: deepClone(record4.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record5]);
@ -191,11 +179,11 @@ describe('idraw: useHistory ', () => {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'modifyElement',
uuid: targetElement.uuid,
method: 'modifyMaterial',
id: targetMaterial.id,
before: deepClone(record3.content.after),
after: deepClone(record3.content.before)
}
after: deepClone(record3.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData2);
expect(__getDoRecords()).toStrictEqual([record5, record6]);

View file

@ -0,0 +1,222 @@
import { iDraw, useHistory, deepClone, createMaterial, set, get, toFlattenMaterial } from 'idraw';
import type { RecursivePartial, Material } from 'idraw';
const createData = () => ({
materials: [
createMaterial('group', {
id: 'test-001',
x: 0,
y: 0,
width: 2000,
height: 2000,
children: [
createMaterial('rect', { id: 'test-002', x: 20, y: 20, width: 20, height: 20 }),
createMaterial('circle', { id: 'test-003', x: 40, y: 40, width: 40, height: 40 }),
createMaterial('text', {
id: 'test-004',
x: 60,
y: 60,
width: 60,
height: 60,
fontSize: 16,
text: 'Text in Group',
}),
createMaterial('image', {
id: 'test-005',
x: 80,
y: 80,
width: 80,
height: 80,
src: 'https://example.com/002.png',
}),
createMaterial('group', {
id: 'test-101',
x: 500,
y: 500,
width: 1000,
height: 1000,
children: [
createMaterial('rect', { id: 'test-102', x: 20, y: 20, width: 20, height: 20 }),
createMaterial('circle', { id: 'test-103', x: 40, y: 40, width: 40, height: 40 }),
createMaterial('text', {
id: 'test-104',
x: 60,
y: 60,
width: 60,
height: 60,
fontSize: 16,
text: 'Text in Group',
}),
createMaterial('image', {
id: 'test-105',
x: 80,
y: 80,
width: 80,
height: 80,
src: 'https://example.com/002.png',
}),
],
}),
],
}),
],
});
describe('idraw: useHistory ', () => {
beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
});
test('modifyMaterial', () => {
const data = createData();
const div = document.createElement('div') as HTMLDivElement;
const idraw = new iDraw(div, {
height: 200,
width: 200,
});
const { MiddlewareHistory, historyHandler } = useHistory({ core: idraw.getCore() });
const { undo, redo, __getDoRecords, __getUndoRecords } = historyHandler;
idraw.use(MiddlewareHistory);
idraw.setData(data);
const targetMaterial = deepClone(data.materials[0]);
// modify 1: do
const modifiedInfo1 = {
x: targetMaterial.x + 1,
y: targetMaterial.y + 2,
fill: '#123456',
cornerRadius: 3,
};
idraw.modifyMaterial({
id: targetMaterial.id,
...deepClone(modifiedInfo1),
});
const expectedData1 = createData();
const flattenModifiedInfo1 = toFlattenMaterial(modifiedInfo1);
const beforeInfo1: Record<string, unknown> = {};
const afterInfo1 = { ...flattenModifiedInfo1 };
Object.keys(flattenModifiedInfo1).forEach((key) => {
beforeInfo1[key] = get(expectedData1.materials[0], key);
set(expectedData1.materials[0], key, flattenModifiedInfo1[key]);
});
const record1 = {
type: 'modifyMaterial',
time: new Date().getTime(),
content: {
method: 'modifyMaterial',
id: targetMaterial.id,
before: beforeInfo1,
after: afterInfo1,
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 2: do
const modifiedInfo2 = {
x: modifiedInfo1.x + 3,
y: modifiedInfo1.y + 4,
cornerRadius: [2, 4, 6, 8],
} as unknown as RecursivePartial<Omit<Material, 'id'>>;
idraw.modifyMaterial({
id: targetMaterial.id,
...deepClone(modifiedInfo2),
} as RecursivePartial<Omit<Material, 'id'>> & Pick<Material, 'id'>);
const expectedData2 = deepClone(expectedData1);
const flattenModifiedInfo2 = toFlattenMaterial(modifiedInfo2);
const beforeInfo2: Record<string, unknown> = {};
const afterInfo2 = { ...flattenModifiedInfo2 };
Object.keys(flattenModifiedInfo2).forEach((key) => {
let beforeVal = get(expectedData1.materials[0], key);
let beforeKey = key;
if (beforeVal === undefined && /(cornerRadius|strokeWidth)\[[0-9]{1,}\]$/.test(beforeKey)) {
beforeKey = beforeKey.replace(/\[[0-9]{1,}\]$/, '');
beforeVal = get(expectedData1.materials[0], beforeKey);
}
beforeInfo2[beforeKey] = beforeVal;
set(expectedData2.materials[0], key, flattenModifiedInfo2[key]);
});
const record2 = {
type: 'modifyMaterial',
time: new Date().getTime(),
content: {
method: 'modifyMaterial',
id: targetMaterial.id,
before: beforeInfo2,
after: afterInfo2,
},
};
expect(idraw.getData()).toStrictEqual(expectedData2);
expect(__getDoRecords()).toStrictEqual([record1, record2]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 3: undo
undo();
const record3 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'modifyMaterial',
id: targetMaterial.id,
before: deepClone(record2.content.after),
after: deepClone(record2.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([record3]);
// modify 4: undo
undo();
const record4 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'modifyMaterial',
id: targetMaterial.id,
before: deepClone(record1.content.after),
after: deepClone(record1.content.before),
},
};
expect(idraw.getData()).toStrictEqual(createData());
expect(__getDoRecords()).toStrictEqual([]);
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
// modify 5: redo
redo();
const record5 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'modifyMaterial',
id: targetMaterial.id,
before: deepClone(record4.content.after),
after: deepClone(record4.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record5]);
expect(__getUndoRecords()).toStrictEqual([record3]);
// modify 5: redo
redo();
const record6 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'modifyMaterial',
id: targetMaterial.id,
before: deepClone(record3.content.after),
after: deepClone(record3.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData2);
expect(__getDoRecords()).toStrictEqual([record5, record6]);
expect(__getUndoRecords()).toStrictEqual([]);
});
});

View file

@ -1,42 +1,39 @@
import { iDraw, useHistory, findElementFromListByPosition, calcResultMovePosition } from 'idraw';
import type { Elements } from 'idraw';
import { iDraw, useHistory, findMaterialFromListByPosition, calcResultMovePosition } from 'idraw';
import type { StrictMaterial } from 'idraw';
const getElemBase = () => {
return {
x: 0,
y: 0,
w: 1,
h: 1
width: 1,
height: 1,
};
};
function generateElements(list: any[]): Elements {
const elements: Elements = list.map((item) => {
function generateMaterials(list: any[]): StrictMaterial[] {
const materials: StrictMaterial[] = list.map((item) => {
if (Array.isArray(item)) {
const groupIds = item[0].split('-');
groupIds.pop();
return {
...getElemBase(),
uuid: groupIds.join('-'),
id: groupIds.join('-'),
type: 'group',
detail: {
children: generateElements(item)
}
children: generateMaterials(item),
};
} else {
return {
...getElemBase(),
uuid: item,
id: item,
type: 'rect',
detail: {}
};
}
}) as Elements;
return elements;
}) as StrictMaterial[];
return materials;
}
const createData = (list: any[]) => ({
elements: generateElements(list)
materials: generateMaterials(list),
});
describe('idraw: useHistory ', () => {
@ -44,14 +41,14 @@ describe('idraw: useHistory ', () => {
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
});
test('moveElement', () => {
test('moveMaterial', () => {
const getList1 = () => ['0', '1', '2', ['3-0', '3-1', ['3-2-0', '3-2-1', '3-2-2', '3-2-3'], '3-3'], '4', '5'];
const data = createData(getList1());
const div = document.createElement('div') as HTMLDivElement;
const idraw = new iDraw(div, {
height: 200,
width: 200
width: 200,
});
const { MiddlewareHistory, historyHandler } = useHistory({ core: idraw.getCore() });
const { undo, redo, __getDoRecords, __getUndoRecords } = historyHandler;
@ -62,69 +59,69 @@ describe('idraw: useHistory ', () => {
const from1 = [3, 2, 1];
const to1 = [2];
// result from: [ 4, 2, 1 ], to: [ 2 ]
const uuid1 = findElementFromListByPosition(from1, data.elements)?.uuid as string;
idraw.moveElement(uuid1, to1);
const id1 = findMaterialFromListByPosition(from1, data.materials)?.id as string;
idraw.moveMaterial(id1, to1);
const record1 = {
type: 'moveElement',
type: 'moveMaterial',
time: new Date().getTime(),
content: {
method: 'moveElement',
uuid: uuid1,
method: 'moveMaterial',
id: id1,
from: [...from1],
to: [...to1]
}
to: [...to1],
},
};
// ['0', '1', '3-2-1', '2', ['3-0', '3-1', ['3-2-0', '3-2-2', '3-2-3'], '3-3'], '4', '5'];
const expectedElements1 = generateElements([
const expectedMaterials1 = generateMaterials([
'0',
'1',
'3-2-1',
'2',
['3-0', '3-1', ['3-2-0', '3-2-2', '3-2-3'], '3-3'],
'4',
'5'
'5',
]);
// const expectedElements1 = moveElementPosition(generateElements(getList1()), {
// const expectedMaterials1 = moveMaterialPosition(generateMaterials(getList1()), {
// from: [...from1],
// to: [...to1]
// }).elements;
// }).materials;
expect(idraw.getData()?.elements).toStrictEqual(expectedElements1);
expect(idraw.getData()?.materials).toStrictEqual(expectedMaterials1);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 2: do
const from2 = [2];
const to2 = [4];
const uuid2 = findElementFromListByPosition(from2, data.elements)?.uuid as string;
// console.log('uuid2 ----- ', uuid2, findElementFromListByPosition(to2, data.elements)?.uuid);
idraw.moveElement(uuid1, to2);
const id2 = findMaterialFromListByPosition(from2, data.materials)?.id as string;
// console.log('id2 ----- ', id2, findMaterialFromListByPosition(to2, data.materials)?.id);
idraw.moveMaterial(id1, to2);
const record2 = {
type: 'moveElement',
type: 'moveMaterial',
time: new Date().getTime(),
content: {
method: 'moveElement',
uuid: uuid2,
method: 'moveMaterial',
id: id2,
from: [...from2],
to: [...to2]
}
to: [...to2],
},
};
const expectedElements2 = generateElements([
const expectedMaterials2 = generateMaterials([
'0',
'1',
'2',
'3-2-1',
['3-0', '3-1', ['3-2-0', '3-2-2', '3-2-3'], '3-3'],
'4',
'5'
'5',
]);
// const expectedElements2 = moveElementPosition(expectedElements1, {
// const expectedMaterials2 = moveMaterialPosition(expectedMaterials1, {
// from: [...from2],
// to: [...to2]
// }).elements;
expect(idraw.getData()?.elements).toStrictEqual(expectedElements2);
// }).materials;
expect(idraw.getData()?.materials).toStrictEqual(expectedMaterials2);
expect(__getDoRecords()).toStrictEqual([record1, record2]);
expect(__getUndoRecords()).toStrictEqual([]);
@ -132,32 +129,32 @@ describe('idraw: useHistory ', () => {
undo();
const moveResult2 = calcResultMovePosition({
from: [...from2],
to: [...to2]
to: [...to2],
}) as { from: number[]; to: number[] };
const record3 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'moveElement',
uuid: record2.content.uuid,
method: 'moveMaterial',
id: record2.content.id,
from: [...moveResult2.to],
to: [...moveResult2.from]
}
to: [...moveResult2.from],
},
};
const expectedElements3 = generateElements([
const expectedMaterials3 = generateMaterials([
'0',
'1',
'3-2-1',
'2',
['3-0', '3-1', ['3-2-0', '3-2-2', '3-2-3'], '3-3'],
'4',
'5'
'5',
]);
// const expectedElements3 = moveElementPosition(expectedElements1, {
// const expectedMaterials3 = moveMaterialPosition(expectedMaterials1, {
// from: [...moveResult2.to],
// to: [...moveResult2.from]
// }).elements;
expect(idraw.getData()?.elements).toStrictEqual(expectedElements3);
// }).materials;
expect(idraw.getData()?.materials).toStrictEqual(expectedMaterials3);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([record3]);
@ -165,31 +162,31 @@ describe('idraw: useHistory ', () => {
undo();
const moveResult3 = calcResultMovePosition({
from: [...from1],
to: [...to1]
to: [...to1],
}) as { from: number[]; to: number[] };
const record4 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'moveElement',
uuid: record1.content.uuid,
method: 'moveMaterial',
id: record1.content.id,
from: [...moveResult3.to],
to: [...moveResult3.from]
}
to: [...moveResult3.from],
},
};
const expectedElements4 = generateElements([
const expectedMaterials4 = generateMaterials([
'0',
'1',
'2',
['3-0', '3-1', ['3-2-0', '3-2-1', '3-2-2', '3-2-3'], '3-3'],
'4',
'5'
'5',
]);
// const expectedElements4 = moveElementPosition(expectedElements3, {
// const expectedMaterials4 = moveMaterialPosition(expectedMaterials3, {
// from: [...moveResult3.to],
// to: [...moveResult3.from]
// }).elements;
expect(idraw.getData()?.elements).toStrictEqual(expectedElements4);
// }).materials;
expect(idraw.getData()?.materials).toStrictEqual(expectedMaterials4);
expect(__getDoRecords()).toStrictEqual([]);
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
@ -197,32 +194,32 @@ describe('idraw: useHistory ', () => {
redo();
const moveResult4 = calcResultMovePosition({
from: [...record4.content.from],
to: [...record4.content.to]
to: [...record4.content.to],
}) as { from: number[]; to: number[] };
const record5 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'moveElement',
uuid: record4.content.uuid,
method: 'moveMaterial',
id: record4.content.id,
from: [...moveResult4.to],
to: [...moveResult4.from]
}
to: [...moveResult4.from],
},
};
const expectedElements5 = generateElements([
const expectedMaterials5 = generateMaterials([
'0',
'1',
'3-2-1',
'2',
['3-0', '3-1', ['3-2-0', '3-2-2', '3-2-3'], '3-3'],
'4',
'5'
'5',
]);
// const expectedElements5 = moveElementPosition(expectedElements3, {
// const expectedMaterials5 = moveMaterialPosition(expectedMaterials3, {
// from: [...moveResult4.from],
// to: [...moveResult4.to]
// }).elements;
expect(idraw.getData()?.elements).toStrictEqual(expectedElements5);
// }).materials;
expect(idraw.getData()?.materials).toStrictEqual(expectedMaterials5);
expect(__getDoRecords()).toStrictEqual([record5]);
expect(__getUndoRecords()).toStrictEqual([record3]);
@ -230,28 +227,28 @@ describe('idraw: useHistory ', () => {
redo();
const moveResult5 = calcResultMovePosition({
from: [...record3.content.from],
to: [...record3.content.to]
to: [...record3.content.to],
}) as { from: number[]; to: number[] };
const record6 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'moveElement',
uuid: record4.content.uuid,
method: 'moveMaterial',
id: record4.content.id,
from: [...moveResult5.to],
to: [...moveResult5.from]
}
to: [...moveResult5.from],
},
};
const expectedElements6 = generateElements([
const expectedMaterials6 = generateMaterials([
'0',
'1',
'2',
'3-2-1',
['3-0', '3-1', ['3-2-0', '3-2-2', '3-2-3'], '3-3'],
'4',
'5'
'5',
]);
expect(idraw.getData()?.elements).toStrictEqual(expectedElements6);
expect(idraw.getData()?.materials).toStrictEqual(expectedMaterials6);
expect(__getDoRecords()).toStrictEqual([record5, record6]);
expect(__getUndoRecords()).toStrictEqual([]);
@ -259,28 +256,28 @@ describe('idraw: useHistory ', () => {
undo();
const moveResult6 = calcResultMovePosition({
from: [...record6.content.from],
to: [...record6.content.to]
to: [...record6.content.to],
}) as { from: number[]; to: number[] };
const record7 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'moveElement',
uuid: record6.content.uuid,
method: 'moveMaterial',
id: record6.content.id,
from: [...moveResult6.to],
to: [...moveResult6.from]
}
to: [...moveResult6.from],
},
};
const expectedElements7 = generateElements([
const expectedMaterials7 = generateMaterials([
'0',
'1',
'3-2-1',
'2',
['3-0', '3-1', ['3-2-0', '3-2-2', '3-2-3'], '3-3'],
'4',
'5'
'5',
]);
expect(idraw.getData()?.elements).toStrictEqual(expectedElements7);
expect(idraw.getData()?.materials).toStrictEqual(expectedMaterials7);
expect(__getDoRecords()).toStrictEqual([record5]);
expect(__getUndoRecords()).toStrictEqual([record7]);
@ -288,27 +285,27 @@ describe('idraw: useHistory ', () => {
undo();
const moveResult7 = calcResultMovePosition({
from: [...record5.content.from],
to: [...record5.content.to]
to: [...record5.content.to],
}) as { from: number[]; to: number[] };
const record8 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'moveElement',
uuid: record5.content.uuid,
method: 'moveMaterial',
id: record5.content.id,
from: [...moveResult7.to],
to: [...moveResult7.from]
}
to: [...moveResult7.from],
},
};
const expectedElements8 = generateElements([
const expectedMaterials8 = generateMaterials([
'0',
'1',
'2',
['3-0', '3-1', ['3-2-0', '3-2-1', '3-2-2', '3-2-3'], '3-3'],
'4',
'5'
'5',
]);
expect(idraw.getData()?.elements).toStrictEqual(expectedElements8);
expect(idraw.getData()?.materials).toStrictEqual(expectedMaterials8);
expect(__getDoRecords()).toStrictEqual([]);
expect(__getUndoRecords()).toStrictEqual([record7, record8]);
});

View file

@ -1,177 +0,0 @@
import { iDraw, useHistory, deepClone, createElement, toFlattenElement, mergeElement } from 'idraw';
const createData = () => ({
elements: [
createElement('rect', {
uuid: 'test-001',
x: 0,
y: 0,
w: 100,
h: 100,
detail: {
background: '#DDDDDD'
}
}),
createElement('circle', { uuid: 'test-002' }),
createElement('text', {
uuid: 'test-003',
detail: {
text: 'Hello World'
}
}),
createElement('image', { uuid: 'test-004', detail: { src: 'https://example.com/001.png' } }),
createElement('group', {
uuid: 'test-005',
detail: {
children: [
createElement('rect', { uuid: 'test-006' }),
createElement('circle', { uuid: 'test-007' }),
createElement('text', {
uuid: 'test-008',
detail: {
text: 'Text in Group'
}
}),
createElement('image', { uuid: 'test-009', detail: { src: 'https://example.com/002.png' } })
]
}
})
]
});
describe('idraw: useHistory ', () => {
beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
});
test('updateElement', () => {
const data = createData();
const div = document.createElement('div') as HTMLDivElement;
const idraw = new iDraw(div, {
height: 200,
width: 200
});
const { MiddlewareHistory, historyHandler } = useHistory({ core: idraw.getCore() });
const { undo, redo, __getDoRecords, __getUndoRecords } = historyHandler;
idraw.use(MiddlewareHistory);
idraw.setData(data);
const targetElement = deepClone(data.elements[0]);
// modify 1: do
const updatedElement1 = deepClone(targetElement);
updatedElement1.x += 1;
updatedElement1.y += 2;
updatedElement1.detail.background = '#123456';
updatedElement1.detail.borderRadius = 3;
idraw.updateElement(updatedElement1);
const beforeInfo1: Record<string, any> = toFlattenElement(targetElement);
const afterInfo1: Record<string, any> = toFlattenElement(updatedElement1);
const expectedData1 = createData();
mergeElement(expectedData1.elements[0], updatedElement1);
const record1 = {
type: 'updateElement',
time: new Date().getTime(),
content: {
method: 'updateElement',
uuid: targetElement.uuid,
before: beforeInfo1,
after: afterInfo1
}
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 2: do
const updatedElement2 = deepClone(updatedElement1);
updatedElement2.x += 3;
updatedElement2.y += 4;
updatedElement2.detail.borderRadius = [2, 4, 6, 8];
idraw.updateElement(updatedElement2);
const beforeInfo2: Record<string, any> = toFlattenElement(updatedElement1);
const afterInfo2: Record<string, any> = toFlattenElement(updatedElement2);
const expectedData2 = createData();
mergeElement(expectedData2.elements[0], updatedElement2);
const record2 = {
type: 'updateElement',
time: new Date().getTime(),
content: {
method: 'updateElement',
uuid: targetElement.uuid,
before: beforeInfo2,
after: afterInfo2
}
};
expect(idraw.getData()).toStrictEqual(expectedData2);
expect(__getDoRecords()).toStrictEqual([record1, record2]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 3: undo
undo();
const record3 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'updateElement',
uuid: targetElement.uuid,
before: deepClone(record2.content.after),
after: deepClone(record2.content.before)
}
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([record3]);
// modify 4: undo
undo();
const record4 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'updateElement',
uuid: targetElement.uuid,
before: deepClone(record1.content.after),
after: deepClone(record1.content.before)
}
};
expect(idraw.getData()).toStrictEqual(createData());
expect(__getDoRecords()).toStrictEqual([]);
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
// modify 5: redo
redo();
const record5 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'updateElement',
uuid: targetElement.uuid,
before: deepClone(record4.content.after),
after: deepClone(record4.content.before)
}
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record5]);
expect(__getUndoRecords()).toStrictEqual([record3]);
// modify 5: redo
redo();
const record6 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'updateElement',
uuid: targetElement.uuid,
before: deepClone(record3.content.after),
after: deepClone(record3.content.before)
}
};
expect(idraw.getData()).toStrictEqual(expectedData2);
expect(__getDoRecords()).toStrictEqual([record5, record6]);
expect(__getUndoRecords()).toStrictEqual([]);
});
});

View file

@ -0,0 +1,169 @@
import { iDraw, useHistory, deepClone, createMaterial, toFlattenMaterial, mergeMaterial } from 'idraw';
const createData = () => ({
materials: [
createMaterial('rect', {
id: 'test-001',
x: 0,
y: 0,
width: 100,
height: 100,
fill: '#DDDDDD',
}),
createMaterial('circle', { id: 'test-002' }),
createMaterial('text', {
id: 'test-003',
text: 'Hello World',
}),
createMaterial('image', { id: 'test-004', src: 'https://example.com/001.png' }),
createMaterial('group', {
id: 'test-005',
children: [
createMaterial('rect', { id: 'test-006' }),
createMaterial('circle', { id: 'test-007' }),
createMaterial('text', {
id: 'test-008',
text: 'Text in Group',
}),
createMaterial('image', { id: 'test-009', src: 'https://example.com/002.png' }),
],
}),
],
});
describe('idraw: useHistory ', () => {
beforeEach(() => {
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
});
test('updateMaterial', () => {
const data = createData();
const div = document.createElement('div') as HTMLDivElement;
const idraw = new iDraw(div, {
height: 200,
width: 200,
});
const { MiddlewareHistory, historyHandler } = useHistory({ core: idraw.getCore() });
const { undo, redo, __getDoRecords, __getUndoRecords } = historyHandler;
idraw.use(MiddlewareHistory);
idraw.setData(data);
const targetMaterial = deepClone(data.materials[0]);
// modify 1: do
const updatedMaterial1 = deepClone(targetMaterial);
updatedMaterial1.x += 1;
updatedMaterial1.y += 2;
updatedMaterial1.fill = '#123456';
updatedMaterial1.cornerRadius = 3;
idraw.updateMaterial(updatedMaterial1);
const beforeInfo1: Record<string, any> = toFlattenMaterial(targetMaterial);
const afterInfo1: Record<string, any> = toFlattenMaterial(updatedMaterial1);
const expectedData1 = createData();
mergeMaterial(expectedData1.materials[0], updatedMaterial1);
const record1 = {
type: 'updateMaterial',
time: new Date().getTime(),
content: {
method: 'updateMaterial',
id: targetMaterial.id,
before: beforeInfo1,
after: afterInfo1,
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 2: do
const updatedMaterial2 = deepClone(updatedMaterial1);
updatedMaterial2.x += 3;
updatedMaterial2.y += 4;
updatedMaterial2.cornerRadius = [2, 4, 6, 8];
idraw.updateMaterial(updatedMaterial2);
const beforeInfo2: Record<string, any> = toFlattenMaterial(updatedMaterial1);
const afterInfo2: Record<string, any> = toFlattenMaterial(updatedMaterial2);
const expectedData2 = createData();
mergeMaterial(expectedData2.materials[0], updatedMaterial2);
const record2 = {
type: 'updateMaterial',
time: new Date().getTime(),
content: {
method: 'updateMaterial',
id: targetMaterial.id,
before: beforeInfo2,
after: afterInfo2,
},
};
expect(idraw.getData()).toStrictEqual(expectedData2);
expect(__getDoRecords()).toStrictEqual([record1, record2]);
expect(__getUndoRecords()).toStrictEqual([]);
// modify 3: undo
undo();
const record3 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'updateMaterial',
id: targetMaterial.id,
before: deepClone(record2.content.after),
after: deepClone(record2.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record1]);
expect(__getUndoRecords()).toStrictEqual([record3]);
// modify 4: undo
undo();
const record4 = {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'updateMaterial',
id: targetMaterial.id,
before: deepClone(record1.content.after),
after: deepClone(record1.content.before),
},
};
expect(idraw.getData()).toStrictEqual(createData());
expect(__getDoRecords()).toStrictEqual([]);
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
// modify 5: redo
redo();
const record5 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'updateMaterial',
id: targetMaterial.id,
before: deepClone(record4.content.after),
after: deepClone(record4.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record5]);
expect(__getUndoRecords()).toStrictEqual([record3]);
// modify 5: redo
redo();
const record6 = {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'updateMaterial',
id: targetMaterial.id,
before: deepClone(record3.content.after),
after: deepClone(record3.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData2);
expect(__getDoRecords()).toStrictEqual([record5, record6]);
expect(__getUndoRecords()).toStrictEqual([]);
});
});

View file

@ -1,43 +1,35 @@
import { iDraw, useHistory, deepClone, createElement, set, get, toFlattenElement } from 'idraw';
import type { RecursivePartial, Element } from 'idraw';
import { iDraw, useHistory, deepClone, createMaterial, set, get, toFlattenMaterial } from 'idraw';
import type { RecursivePartial, Material } from 'idraw';
const createData = () => ({
elements: [
createElement('rect', {
uuid: 'test-001',
materials: [
createMaterial('rect', {
id: 'test-001',
x: 0,
y: 0,
w: 100,
h: 100,
detail: {
background: '#DDDDDD'
}
width: 100,
height: 100,
fill: '#DDDDDD',
}),
createElement('circle', { uuid: 'test-002' }),
createElement('text', {
uuid: 'test-003',
detail: {
text: 'Hello World'
}
createMaterial('circle', { id: 'test-002' }),
createMaterial('text', {
id: 'test-003',
text: 'Hello World',
}),
createElement('image', { uuid: 'test-004', detail: { src: 'https://example.com/001.png' } }),
createElement('group', {
uuid: 'test-005',
detail: {
children: [
createElement('rect', { uuid: 'test-006' }),
createElement('circle', { uuid: 'test-007' }),
createElement('text', {
uuid: 'test-008',
detail: {
text: 'Text in Group'
}
}),
createElement('image', { uuid: 'test-009', detail: { src: 'https://example.com/002.png' } })
]
}
})
]
createMaterial('image', { id: 'test-004', src: 'https://example.com/001.png' }),
createMaterial('group', {
id: 'test-005',
children: [
createMaterial('rect', { id: 'test-006' }),
createMaterial('circle', { id: 'test-007' }),
createMaterial('text', {
id: 'test-008',
text: 'Text in Group',
}),
createMaterial('image', { id: 'test-009', src: 'https://example.com/002.png' }),
],
}),
],
});
describe('idraw: useHistory ', () => {
@ -45,50 +37,48 @@ describe('idraw: useHistory ', () => {
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
});
test('modifyElement', () => {
test('modifyMaterial', () => {
const data = createData();
const div = document.createElement('div') as HTMLDivElement;
const idraw = new iDraw(div, {
height: 200,
width: 200
width: 200,
});
const { MiddlewareHistory, historyHandler } = useHistory({ core: idraw.getCore() });
const { undo, redo, __getDoRecords, __getUndoRecords } = historyHandler;
idraw.use(MiddlewareHistory);
idraw.setData(data);
const targetElement = deepClone(data.elements[0]);
const targetMaterial = deepClone(data.materials[0]);
// modify 1: do
const modifiedInfo1 = {
x: targetElement.x + 1,
y: targetElement.y + 2,
detail: {
background: '#123456',
borderRadius: 3
}
x: targetMaterial.x + 1,
y: targetMaterial.y + 2,
fill: '#123456',
cornerRadius: 3,
};
idraw.modifyElement({
uuid: targetElement.uuid,
...deepClone(modifiedInfo1)
idraw.modifyMaterial({
id: targetMaterial.id,
...deepClone(modifiedInfo1),
});
const expectedData1 = createData();
const flattenModifiedInfo1 = toFlattenElement(modifiedInfo1);
const flattenModifiedInfo1 = toFlattenMaterial(modifiedInfo1);
const beforeInfo1: Record<string, any> = {};
const afterInfo1 = { ...flattenModifiedInfo1 };
Object.keys(flattenModifiedInfo1).forEach((key) => {
beforeInfo1[key] = get(expectedData1.elements[0], key);
set(expectedData1.elements[0], key, flattenModifiedInfo1[key]);
beforeInfo1[key] = get(expectedData1.materials[0], key);
set(expectedData1.materials[0], key, flattenModifiedInfo1[key]);
});
const record1 = {
type: 'modifyElement',
type: 'modifyMaterial',
time: new Date().getTime(),
content: {
method: 'modifyElement',
uuid: targetElement.uuid,
method: 'modifyMaterial',
id: targetMaterial.id,
before: beforeInfo1,
after: afterInfo1
}
after: afterInfo1,
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record1]);
@ -98,40 +88,38 @@ describe('idraw: useHistory ', () => {
const modifiedInfo2 = {
x: modifiedInfo1.x + 3,
y: modifiedInfo1.y + 4,
detail: {
borderRadius: [2, 4, 6, 8]
}
} as unknown as RecursivePartial<Omit<Element, 'uuid'>>;
cornerRadius: [2, 4, 6, 8],
} as unknown as RecursivePartial<Omit<Material, 'id'>>;
idraw.modifyElement({
uuid: targetElement.uuid,
...deepClone(modifiedInfo2)
} as RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>);
idraw.modifyMaterial({
id: targetMaterial.id,
...deepClone(modifiedInfo2),
} as RecursivePartial<Omit<Material, 'id'>> & Pick<Material, 'id'>);
const expectedData2 = deepClone(expectedData1);
const flattenModifiedInfo2 = toFlattenElement(modifiedInfo2);
const flattenModifiedInfo2 = toFlattenMaterial(modifiedInfo2);
const beforeInfo2: Record<string, any> = {};
const afterInfo2 = { ...flattenModifiedInfo2 };
Object.keys(flattenModifiedInfo2).forEach((key) => {
let beforeVal = get(expectedData1.elements[0], key);
let beforeVal = get(expectedData1.materials[0], key);
let beforeKey = key;
if (beforeVal === undefined && /(borderRadius|borderWidth)\[[0-9]{1,}\]$/.test(beforeKey)) {
if (beforeVal === undefined && /(cornerRadius|strokeWidth)\[[0-9]{1,}\]$/.test(beforeKey)) {
beforeKey = beforeKey.replace(/\[[0-9]{1,}\]$/, '');
beforeVal = get(expectedData1.elements[0], beforeKey);
beforeVal = get(expectedData1.materials[0], beforeKey);
}
beforeInfo2[beforeKey] = beforeVal;
set(expectedData2.elements[0], key, flattenModifiedInfo2[key]);
set(expectedData2.materials[0], key, flattenModifiedInfo2[key]);
});
const record2 = {
type: 'modifyElement',
type: 'modifyMaterial',
time: new Date().getTime(),
content: {
method: 'modifyElement',
uuid: targetElement.uuid,
method: 'modifyMaterial',
id: targetMaterial.id,
before: beforeInfo2,
after: afterInfo2
}
after: afterInfo2,
},
};
expect(idraw.getData()).toStrictEqual(expectedData2);
expect(__getDoRecords()).toStrictEqual([record1, record2]);
@ -143,11 +131,11 @@ describe('idraw: useHistory ', () => {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'modifyElement',
uuid: targetElement.uuid,
method: 'modifyMaterial',
id: targetMaterial.id,
before: deepClone(record2.content.after),
after: deepClone(record2.content.before)
}
after: deepClone(record2.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record1]);
@ -159,11 +147,11 @@ describe('idraw: useHistory ', () => {
type: 'undo',
time: new Date().getTime(),
content: {
method: 'modifyElement',
uuid: targetElement.uuid,
method: 'modifyMaterial',
id: targetMaterial.id,
before: deepClone(record1.content.after),
after: deepClone(record1.content.before)
}
after: deepClone(record1.content.before),
},
};
expect(idraw.getData()).toStrictEqual(createData());
expect(__getDoRecords()).toStrictEqual([]);
@ -175,11 +163,11 @@ describe('idraw: useHistory ', () => {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'modifyElement',
uuid: targetElement.uuid,
method: 'modifyMaterial',
id: targetMaterial.id,
before: deepClone(record4.content.after),
after: deepClone(record4.content.before)
}
after: deepClone(record4.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData1);
expect(__getDoRecords()).toStrictEqual([record5]);
@ -191,11 +179,11 @@ describe('idraw: useHistory ', () => {
type: 'redo',
time: new Date().getTime(),
content: {
method: 'modifyElement',
uuid: targetElement.uuid,
method: 'modifyMaterial',
id: targetMaterial.id,
before: deepClone(record3.content.after),
after: deepClone(record3.content.before)
}
after: deepClone(record3.content.before),
},
};
expect(idraw.getData()).toStrictEqual(expectedData2);
expect(__getDoRecords()).toStrictEqual([record5, record6]);

View file

@ -1,6 +1,6 @@
{
"name": "idraw",
"version": "0.4.0",
"version": "1.0.0",
"description": "",
"main": "dist/esm/index.js",
"module": "dist/esm/index.js",
@ -22,10 +22,10 @@
"license": "MIT",
"devDependencies": {},
"dependencies": {
"@idraw/core": "workspace:^0.4",
"@idraw/renderer": "workspace:^0.4",
"@idraw/types": "workspace:^0.4",
"@idraw/util": "workspace:^0.4"
"@idraw/core": "workspace:*",
"@idraw/renderer": "workspace:*",
"@idraw/types": "workspace:*",
"@idraw/util": "workspace:*"
},
"publishConfig": {
"access": "public",

View file

@ -1,29 +1,37 @@
import { Core, coreEventKeys } from '@idraw/core';
import type {
PointSize,
Point,
IDrawOptions,
IDrawSettings,
IDrawFeature,
IDrawMode,
IDrawModeEventMap,
Data,
ViewSizeInfo,
ViewScaleInfo,
ElementType,
Element,
MaterialType,
StrictMaterial,
RecursivePartial,
ElementPosition,
MaterialPosition,
IDrawStorage,
DataLayout,
DataGlobal,
Middleware,
HistoryHandler
HistoryHandler,
} from '@idraw/types';
import { filterCompactData, calcViewCenterContent, calcViewCenter, Store } from '@idraw/util';
import { defaultSettings, defaultOptions, getDefaultStorage, defaultMode, parseStyles } from './setting/config';
import type { ExportImageFileBaseOptions, ExportImageFileResult } from './file';
import type { IDrawEvent } from './event';
import { changeMode } from './setting/mode';
import { createElement, updateElement, modifyElement, addElement, deleteElement, moveElement } from './methods/element';
import {
createMaterial,
updateMaterial,
modifyMaterial,
addMaterial,
deleteMaterial,
moveMaterial,
} from './methods/material';
import { modifyLayout } from './methods/layout';
import { modifyGlobal } from './methods/global';
import { reset } from './methods/reset';
@ -35,7 +43,7 @@ export class iDraw {
#core: Core<IDrawEvent>;
#opts: IDrawOptions;
#store: Store<IDrawStorage> = new Store<IDrawStorage>({
defaultStorage: getDefaultStorage()
defaultStorage: getDefaultStorage(),
});
#historyHandler: HistoryHandler | null = null;
@ -59,7 +67,7 @@ export class iDraw {
this.#historyHandler = historyHandler;
core.use(MiddlewareHistory);
}
changeMode('select', core, store);
changeMode('select', undefined, core, store);
}
#setFeature(feat: IDrawFeature, status: boolean) {
@ -79,13 +87,17 @@ export class iDraw {
this.#opts = { ...this.#opts, ...newOpts };
}
setMode(mode: IDrawMode) {
setMode<T extends IDrawMode>(mode: IDrawMode, e?: IDrawModeEventMap[T]) {
const core = this.#core;
const store = this.#store;
changeMode(mode || defaultMode, core, store);
changeMode<T>(mode || defaultMode, e, core, store);
core.refresh();
}
getMode(): IDrawMode | undefined | null {
return this.#store.get('mode');
}
enable(feat: IDrawFeature) {
this.#setFeature(feat, true);
}
@ -104,7 +116,7 @@ export class iDraw {
const data = this.#core.getData();
if (data && opts?.compact === true) {
return filterCompactData(data, {
loadItemMap: this.#core.getLoadItemMap()
loadItemMap: this.#core.getLoadItemMap(),
});
}
return data;
@ -114,7 +126,7 @@ export class iDraw {
return this.#core.getViewInfo();
}
scale(opts: { scale: number; point: PointSize }) {
scale(opts: { scale: number; point: Point }) {
this.#core.scale(opts);
}
@ -127,7 +139,7 @@ export class iDraw {
centerContent(opts?: { data?: Data }) {
const data = opts?.data || this.#core.getData();
const { viewSizeInfo } = this.getViewInfo();
if (data?.layout || (Array.isArray(data?.elements) && data?.elements.length > 0)) {
if (data?.layout || (Array.isArray(data?.materials) && data?.materials.length > 0)) {
const result = calcViewCenterContent(data, { viewSizeInfo });
this.setViewScale(result);
}
@ -149,52 +161,52 @@ export class iDraw {
this.#core.trigger(name, e as IDrawEvent[T]);
}
selectElement(uuid: string, opts?: { type?: string }) {
this.trigger(coreEventKeys.SELECT, { uuids: [uuid], type: opts?.type || 'selectElement' });
selectMaterial(id: string, opts?: { type?: string }) {
this.trigger(coreEventKeys.SELECT, { ids: [id], type: opts?.type || 'selectMaterial' });
}
selectElements(uuids: string[], opts?: { type?: string }) {
this.trigger(coreEventKeys.SELECT, { uuids, type: opts?.type || 'selectElements' });
selectMaterials(ids: string[], opts?: { type?: string }) {
this.trigger(coreEventKeys.SELECT, { ids, type: opts?.type || 'selectMaterials' });
}
selectElementByPosition(position: ElementPosition, opts?: { type?: string }) {
this.trigger(coreEventKeys.SELECT, { positions: [position], type: opts?.type || 'selectElementByPosition' });
selectMaterialByPosition(position: MaterialPosition, opts?: { type?: string }) {
this.trigger(coreEventKeys.SELECT, { positions: [position], type: opts?.type || 'selectMaterialByPosition' });
}
selectElementsByPositions(positions: ElementPosition[], opts?: { type?: string }) {
this.trigger(coreEventKeys.SELECT, { positions, type: opts?.type || 'selectElementsByPositions' });
selectMaterialsByPositions(positions: MaterialPosition[], opts?: { type?: string }) {
this.trigger(coreEventKeys.SELECT, { positions, type: opts?.type || 'selectMaterialsByPositions' });
}
cancelElements() {
this.trigger(coreEventKeys.CLEAR_SELECT, { uuids: [] });
cancelMaterials() {
this.trigger(coreEventKeys.CLEAR_SELECT, { ids: [] });
}
createElement<T extends ElementType>(
createMaterial<T extends MaterialType>(
type: T,
element: RecursivePartial<Omit<Element, 'uuid' | 'type'>>,
material: RecursivePartial<Omit<StrictMaterial<T>, 'id' | 'type'>>,
opts?: { viewCenter?: boolean }
): Element<T> {
return createElement<T>({ core: this.#core }, type, element, opts);
): StrictMaterial<T> {
return createMaterial<T>({ core: this.#core }, type, material as StrictMaterial, opts);
}
updateElement(element: Element) {
return updateElement({ core: this.#core }, element);
updateMaterial(material: StrictMaterial) {
return updateMaterial({ core: this.#core }, material);
}
modifyElement(element: RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>) {
return modifyElement({ core: this.#core }, element);
modifyMaterial(material: RecursivePartial<Omit<StrictMaterial, 'id'>> & Pick<StrictMaterial, 'id'>) {
return modifyMaterial({ core: this.#core }, material);
}
addElement(element: Element, opts?: { position: ElementPosition }): Data {
return addElement({ core: this.#core }, element, opts);
addMaterial(material: StrictMaterial, opts?: { position: MaterialPosition }): Data {
return addMaterial({ core: this.#core }, material, opts);
}
deleteElement(uuid: string) {
return deleteElement({ core: this.#core }, uuid);
deleteMaterial(id: string) {
return deleteMaterial({ core: this.#core }, id);
}
moveElement(uuid: string, to: ElementPosition) {
return moveElement({ core: this.#core }, uuid, to);
moveMaterial(id: string, to: MaterialPosition) {
return moveMaterial({ core: this.#core }, id, to);
}
modifyLayout(layout: RecursivePartial<DataLayout> | null) {
@ -206,7 +218,7 @@ export class iDraw {
}
async getImageBlobURL(opts?: ExportImageFileBaseOptions): Promise<ExportImageFileResult> {
const data = this.getData() || { elements: [] };
const data = this.getData() || { materials: [] };
const { viewSizeInfo } = this.getViewInfo();
return await getImageBlobURL({ data, viewSizeInfo, core: this.#core }, opts);
}
@ -224,9 +236,9 @@ export class iDraw {
this.#historyHandler = null as any;
}
getViewCenter(): PointSize {
getViewCenter(): Point {
const { viewScaleInfo, viewSizeInfo } = this.getViewInfo();
const pointSize: PointSize = calcViewCenter({ viewScaleInfo, viewSizeInfo });
const pointSize: Point = calcViewCenter({ viewScaleInfo, viewSizeInfo });
return pointSize;
}

View file

@ -5,11 +5,18 @@ export {
Core,
Board,
MiddlewareSelector,
MiddlewareCreator,
MiddlewareDragger,
MiddlewareInfo,
MiddlewareLayoutSelector,
MiddlewarePointer,
MiddlewareScroller,
MiddlewareScaler,
MiddlewareRuler,
MiddlewareTextEditor,
coreEventKeys
MiddlewarePathCreator,
MiddlewarePathEditor,
coreEventKeys,
} from '@idraw/core';
export { Renderer } from '@idraw/renderer';
export {
@ -30,6 +37,7 @@ export {
colorToLinearGradientCSS,
mergeHexColorAlpha,
createUUID,
createId,
isAssetId,
createAssetId,
deepClone,
@ -37,8 +45,8 @@ export {
sortDataAsserts,
istype,
loadImage,
loadSVG,
loadHTML,
loadSVGCode,
loadForeignObject,
is,
check,
createBoardContent,
@ -46,61 +54,61 @@ export {
createOffscreenContext2D,
EventEmitter,
calcDistance,
calcSpeed,
equalPoint,
equalTouchPoint,
vaildPoint,
vaildTouchPoint,
// calcSpeed,
// equalPoint,
// equalTouchPoint,
// vaildPoint,
// vaildTouchPoint,
getCenterFromTwoPoints,
Store,
getViewScaleInfoFromSnapshot,
getViewSizeInfoFromSnapshot,
Context2D,
rotateElement,
rotateMaterial,
parseRadianToAngle,
parseAngleToRadian,
rotateElementVertexes,
getElementRotateVertexes,
calcElementCenter,
calcElementCenterFromVertexes,
rotateMaterialVertexes,
getMaterialRotateVertexes,
calcMaterialCenter,
calcMaterialCenterFromVertexes,
rotatePointInGroup,
limitAngle,
getSelectedElementUUIDs,
validateElements,
calcElementsContextSize,
calcElementsViewInfo,
calcElementListSize,
getElemenetsAssetIds,
findElementFromList,
findElementsFromList,
findElementFromListByPosition,
findElementsFromListByPositions,
findElementQueueFromListByPosition,
getElementPositionFromList,
getElementPositionMapFromList,
updateElementInList,
getSelectedMaterialUUIDs,
validateMaterials,
calcMaterialsContextSize,
calcMaterialsViewInfo,
calcMaterialListSize,
getMaterialsAssetIds,
findMaterialFromList,
findMaterialsFromList,
findMaterialFromListByPosition,
findMaterialsFromListByPositions,
findMaterialQueueFromListByPosition,
getMaterialPositionFromList,
getMaterialPositionMapFromList,
updateMaterialInList,
getGroupQueueFromList,
getElementSize,
mergeElementAsset,
filterElementAsset,
isResourceElement,
getMaterialSize,
mergeMaterialAsset,
filterMaterialAsset,
isResourceMaterial,
checkRectIntersect,
viewScale,
viewScroll,
calcViewElementSize,
calcViewPointSize,
calcViewMaterialSize,
calcViewPoint,
calcViewVertexes,
isViewPointInElement,
getViewPointAtElement,
isElementInView,
isViewPointInMaterial,
getViewPointAtMaterial,
isMaterialInView,
rotatePoint,
rotateVertexes,
getElementVertexes,
calcElementVertexesInGroup,
calcElementVertexesQueueInGroup,
calcElementQueueVertexesQueueInGroup,
calcElementSizeController,
generateSVGPath,
getMaterialVertexes,
calcMaterialVertexesInGroup,
calcMaterialVertexesQueueInGroup,
calcMaterialQueueVertexesQueueInGroup,
calcMaterialSizeController,
convertPathCommandsToStr,
parseSVGPath,
generateHTML,
parseHTML,
@ -108,32 +116,34 @@ export {
formatNumber,
matrixToAngle,
matrixToRadian,
getDefaultElementDetailConfig,
calcViewBoxSize,
createElement,
moveElementPosition,
insertElementToListByPosition,
deleteElementInListByPosition,
deleteElementInList,
deepCloneElement,
getDefaultMaterialAttributes,
createMaterial,
moveMaterialPosition,
insertMaterialToListByPosition,
deleteMaterialInListByPosition,
deleteMaterialInList,
deepCloneMaterial,
calcViewCenterContent,
calcViewCenter,
calcElementViewRectInfo,
calcElementOriginRectInfo,
flatElementList,
calcPointMoveElementInGroup,
calcMaterialViewBoundingInfo,
calcMaterialBoundingInfo,
flatMaterialList,
calcPointMoveMaterialInGroup,
merge,
omit,
toFlattenElement,
toFlattenMaterial,
toFlattenGlobal,
toFlattenLayout,
flatObject,
unflatObject,
set,
get,
mergeElement,
mergeMaterial,
calcResultMovePosition,
calcRevertMovePosition
calcRevertMovePosition,
svgToMaterial,
materialToSVG,
dataToSVG,
} from '@idraw/util';
export { iDraw } from './idraw';
export { eventKeys } from './event';

View file

@ -1,84 +0,0 @@
import type { Data, Element, ElementType, ElementPosition, RecursivePartial } from '@idraw/types';
import { Core, coreEventKeys } from '@idraw/core';
import { IDrawEvent } from '../event';
export function createElement<T extends ElementType = ElementType>(
depOptions: {
core: Core<IDrawEvent>;
},
type: T,
element: RecursivePartial<Element<T>>,
opts?: {
viewCenter?: boolean;
}
): Element<T> {
const { core } = depOptions;
return core.createElement<T>(type, element, opts);
}
export function updateElement(
depOptions: {
core: Core<IDrawEvent>;
},
element: Element
) {
const { core } = depOptions;
const modifyRecord = core.updateElement(element);
if (!modifyRecord) {
return;
}
const data = core.getData();
if (!data) {
return;
}
core.trigger(coreEventKeys.CHANGE, { data, type: 'updateElement', modifyRecord });
}
export function modifyElement(
depOptions: {
core: Core<IDrawEvent>;
},
element: RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>
) {
const { core } = depOptions;
const modifyRecord = core.modifyElement(element);
if (!modifyRecord) {
return;
}
const data = core.getData();
if (!data) {
return;
}
core.trigger(coreEventKeys.CHANGE, { data, type: 'modifyElement', modifyRecord });
}
export function addElement(
depOptions: {
core: Core<IDrawEvent>;
},
element: Element,
opts?: {
position: ElementPosition;
}
): Data {
const { core } = depOptions;
const modifyRecord = core.addElement(element, opts);
const data = core.getData() as Data;
core.trigger(coreEventKeys.CHANGE, { data, type: 'addElement', modifyRecord });
return data;
}
export function deleteElement(depOptions: { core: Core<IDrawEvent> }, uuid: string) {
const { core } = depOptions;
const modifyRecord = core.deleteElement(uuid);
const data = core.getData() as Data;
core.trigger(coreEventKeys.CHANGE, { data, type: 'deleteElement', modifyRecord });
}
export function moveElement(depOptions: { core: Core<IDrawEvent> }, uuid: string, to: ElementPosition) {
const { core } = depOptions;
const modifyRecord = core.moveElement(uuid, to);
const data = core.getData() as Data;
core.trigger(coreEventKeys.CHANGE, { data, type: 'moveElement', modifyRecord });
}

View file

@ -15,18 +15,18 @@ export function setFeature(
ruler: 'enableRuler',
scroll: 'enableScroll',
scale: 'enableScale',
info: 'enableInfo'
info: 'enableInfo',
};
store.set(map[feat], !!status);
runMiddlewares(core, store);
runMiddlewares(null, core, store);
core.refresh();
} else if (feat === 'selectInGroup') {
core.trigger(coreEventKeys.SELECT_IN_GROUP, {
enable: !!status
enable: !!status,
});
} else if (feat === 'snapToGrid') {
core.trigger(coreEventKeys.SNAP_TO_GRID, {
enable: !!status
enable: !!status,
});
}
}

Some files were not shown because too many files have changed in this diff Show more