mirror of
https://github.com/idrawjs/idraw
synced 2026-05-23 09:38:22 +00:00
commit
5094ed2e92
255 changed files with 17739 additions and 12252 deletions
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"private": false,
|
||||
"version": "0.4.3",
|
||||
"version": "1.0.0-alpha.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
481
packages/core/src/core.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
15
packages/core/src/middlewares/common.ts
Normal file
15
packages/core/src/middlewares/common.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
79
packages/core/src/middlewares/creator/dom.ts
Normal file
79
packages/core/src/middlewares/creator/dom.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
178
packages/core/src/middlewares/creator/index.ts
Normal file
178
packages/core/src/middlewares/creator/index.ts
Normal 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);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
28
packages/core/src/middlewares/creator/static.ts
Normal file
28
packages/core/src/middlewares/creator/static.ts
Normal 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`,
|
||||
};
|
||||
39
packages/core/src/middlewares/creator/styles.ts
Normal file
39
packages/core/src/middlewares/creator/styles.ts
Normal 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;
|
||||
}
|
||||
8
packages/core/src/middlewares/creator/types.ts
Normal file
8
packages/core/src/middlewares/creator/types.ts
Normal 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;
|
||||
};
|
||||
61
packages/core/src/middlewares/creator/util.ts
Normal file
61
packages/core/src/middlewares/creator/util.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
20
packages/core/src/middlewares/info/static.ts
Normal file
20
packages/core/src/middlewares/info/static.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
>;
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
};
|
||||
168
packages/core/src/middlewares/layout-selector/dom.ts
Normal file
168
packages/core/src/middlewares/layout-selector/dom.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
55
packages/core/src/middlewares/layout-selector/static.ts
Normal file
55
packages/core/src/middlewares/layout-selector/static.ts
Normal 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`,
|
||||
};
|
||||
149
packages/core/src/middlewares/layout-selector/styles.ts
Normal file
149
packages/core/src/middlewares/layout-selector/styles.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
206
packages/core/src/middlewares/path-creator/dom.ts
Normal file
206
packages/core/src/middlewares/path-creator/dom.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
274
packages/core/src/middlewares/path-creator/index.ts
Normal file
274
packages/core/src/middlewares/path-creator/index.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
};
|
||||
61
packages/core/src/middlewares/path-creator/static.ts
Normal file
61
packages/core/src/middlewares/path-creator/static.ts
Normal 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;
|
||||
}
|
||||
4
packages/core/src/middlewares/path-creator/types.ts
Normal file
4
packages/core/src/middlewares/path-creator/types.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export type PathSharedStorage = {
|
||||
// [keySelectedPathElement]: null | Element<'path'>; // TODO
|
||||
};
|
||||
77
packages/core/src/middlewares/path-editor/calc.ts
Normal file
77
packages/core/src/middlewares/path-editor/calc.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
793
packages/core/src/middlewares/path-editor/dom.ts
Normal file
793
packages/core/src/middlewares/path-editor/dom.ts
Normal 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
|
||||
}
|
||||
166
packages/core/src/middlewares/path-editor/draw.ts
Normal file
166
packages/core/src/middlewares/path-editor/draw.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
406
packages/core/src/middlewares/path-editor/index.ts
Normal file
406
packages/core/src/middlewares/path-editor/index.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
};
|
||||
42
packages/core/src/middlewares/path-editor/parse.ts
Normal file
42
packages/core/src/middlewares/path-editor/parse.ts
Normal 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;
|
||||
}
|
||||
95
packages/core/src/middlewares/path-editor/static.ts
Normal file
95
packages/core/src/middlewares/path-editor/static.ts
Normal 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;
|
||||
}
|
||||
35
packages/core/src/middlewares/path-editor/types.ts
Normal file
35
packages/core/src/middlewares/path-editor/types.ts
Normal 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;
|
||||
};
|
||||
34
packages/core/src/middlewares/path-editor/util.ts
Normal file
34
packages/core/src/middlewares/path-editor/util.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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 || [],
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
39
packages/core/src/middlewares/ruler/static.ts
Normal file
39
packages/core/src/middlewares/ruler/static.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
>;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
};
|
||||
73
packages/core/src/middlewares/scroller/dom.ts
Normal file
73
packages/core/src/middlewares/scroller/dom.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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),
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
38
packages/core/src/middlewares/scroller/static.ts
Normal file
38
packages/core/src/middlewares/scroller/static.ts
Normal 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`,
|
||||
};
|
||||
82
packages/core/src/middlewares/scroller/styles.ts
Normal file
82
packages/core/src/middlewares/scroller/styles.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
559
packages/core/src/middlewares/selector/dom.ts
Normal file
559
packages/core/src/middlewares/selector/dom.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
// }
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
175
packages/core/src/middlewares/selector/render-frame.ts
Normal file
175
packages/core/src/middlewares/selector/render-frame.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
packages/core/src/middlewares/selector/resize.ts
Normal file
33
packages/core/src/middlewares/selector/resize.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
129
packages/core/src/middlewares/selector/static.ts
Normal file
129
packages/core/src/middlewares/selector/static.ts
Normal 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);
|
||||
};
|
||||
186
packages/core/src/middlewares/selector/styles.ts
Normal file
186
packages/core/src/middlewares/selector/styles.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
184
packages/core/src/middlewares/text-editor/dom.ts
Normal file
184
packages/core/src/middlewares/text-editor/dom.ts
Normal 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);
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
26
packages/core/src/middlewares/text-editor/static.ts
Normal file
26
packages/core/src/middlewares/text-editor/static.ts
Normal 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;
|
||||
}
|
||||
26
packages/core/src/middlewares/text-editor/types.ts
Normal file
26
packages/core/src/middlewares/text-editor/types.ts
Normal 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>;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -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
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
161
packages/idraw/__tests__/history-addMaterial.test.ts
Normal file
161
packages/idraw/__tests__/history-addMaterial.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
152
packages/idraw/__tests__/history-deleteMaterial.test.ts
Normal file
152
packages/idraw/__tests__/history-deleteMaterial.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
222
packages/idraw/__tests__/history-modifyMaterials.test.ts
Normal file
222
packages/idraw/__tests__/history-modifyMaterials.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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]);
|
||||
});
|
||||
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
169
packages/idraw/__tests__/history-updateMaterial.test.ts
Normal file
169
packages/idraw/__tests__/history-updateMaterial.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue