diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 369b5d6..d1e8336 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/package.json b/package.json index 03a9fff..7989840 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": false, - "version": "0.4.3", + "version": "1.0.0-alpha.0", "workspaces": [ "packages/*" ], diff --git a/packages/core/package.json b/packages/core/package.json index d1bf7a4..f74f0b3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/src/board/index.ts b/packages/core/src/board/index.ts index 3fc9d61..af6816b 100644 --- a/packages/core/src/board/index.ts +++ b/packages/core/src/board/index.ts @@ -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 { #eventHub: EventEmitter = new EventEmitter(); #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 { }, afterDrawFrame: (e) => { this.#handleAfterDrawFrame(e); - } + }, }); this.#init(); this.#resetActiveMiddlewareObjs(); @@ -131,6 +132,7 @@ export class Board { 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 { } } + #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 { 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 { this.#middlewareMap.set(middleware, { status: 'enable', middlewareObject: obj, - config + config, }); this.#resetActiveMiddlewareObjs(); } @@ -398,14 +410,14 @@ export class Board { } } - 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 }); } diff --git a/packages/core/src/board/sharer.ts b/packages/core/src/board/sharer.ts index 0644140..adea93d 100644 --- a/packages/core/src/board/sharer.ts +++ b/packages/core/src/board/sharer.ts @@ -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> { @@ -24,10 +24,10 @@ export class Sharer implements StoreSharer({ - 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>>> | null { - return this.#activeStore.get('overrideElementMap'); + getActiveOverrideMaterialMap(): Record> | null { + return this.#activeStore.get('overrideMaterialMap'); } - setActiveOverrideElemenentMap(map: Record>>> | null): void { - this.#activeStore.set('overrideElementMap', map); + setActiveOverrideMaterialMap(map: Record> | null): void { + this.#activeStore.set('overrideMaterialMap', map); } } diff --git a/packages/core/src/board/viewer.ts b/packages/core/src/board/viewer.ts index cdfd261..be959c0 100644 --- a/packages/core/src/board/viewer.ts +++ b/packages/core/src/board/viewer.ts @@ -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 implements BoardVi height, contextHeight, contextWidth, - devicePixelRatio + devicePixelRatio, } = snapshot.activeStore; const viewScaleInfo: ViewScaleInfo = { @@ -63,19 +63,19 @@ export class Viewer extends EventEmitter 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 implements BoardVi } } - resetVirtualFlatItemMap( + resetVirtualItemMap( data: Data, opts: { viewScaleInfo: ViewScaleInfo; @@ -106,7 +106,7 @@ export class Viewer extends EventEmitter 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 implements BoardVi const sharedStore: Record = sharer.getSharedStoreSnapshot(); // const activeStore: ActiveStore = sharer.getActiveStoreSnapshot({ deepClone: true }); // const sharedStore: Record = 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 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 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 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 implements BoardVi if (!opts?.ignoreUpdateVisibleStatus) { this.#opts.calculator.updateVisiableStatus({ viewScaleInfo: sharer.getActiveViewScaleInfo(), - viewSizeInfo: sharer.getActiveViewSizeInfo() + viewSizeInfo: sharer.getActiveViewSizeInfo(), }); } return newViewSize; diff --git a/packages/core/src/board/watcher.ts b/packages/core/src/board/watcher.ts index ae350b9..edf3f38 100644 --- a/packages/core/src/board/watcher.ts +++ b/packages/core/src/board/watcher.ts @@ -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 { constructor(opts: BoardWatcherOptions) { super(); const store = new Store({ - 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { } 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 { 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 { interface PointResult { index: number; - element: Element | 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; } } diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts new file mode 100644 index 0000000..717950d --- /dev/null +++ b/packages/core/src/core.ts @@ -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 { + #board: Board; + // #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({ 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(middleware: Middleware, config?: C) { + this.#board.use(middleware, config); + } + + disuse(middleware: Middleware) { + this.#board.disuse(middleware); + } + + resetMiddlewareConfig(middleware: Middleware, config?: Partial) { + 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) { + 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(name: T, callback: (e: E[T]) => void) { + const eventHub = this.#board.getEventHub(); + eventHub.on(name, callback); + } + + off(name: T, callback: (e: E[T]) => void) { + const eventHub = this.#board.getEventHub(); + eventHub.off(name, callback); + } + + trigger(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( + type: T, + material: RecursivePartial>, + opts?: { + viewCenter?: boolean; + } + ): StrictMaterial { + const { viewScaleInfo, viewSizeInfo } = this.getViewInfo(); + return createMaterial( + 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> & Pick + ): 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> & Pick> + ): 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 | 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 | 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; + } +} diff --git a/packages/core/src/cursor/cursor-image.ts b/packages/core/src/cursor/cursor-image.ts index 81a4993..0abe7de 100644 --- a/packages/core/src/cursor/cursor-image.ts +++ b/packages/core/src/cursor/cursor-image.ts @@ -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,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`; + +export const CURSOR_PEN = `data:image/png;base64,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`; + +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`; diff --git a/packages/core/src/cursor/cursor.ts b/packages/core/src/cursor/cursor.ts index b337d16..2f0a665 100644 --- a/packages/core/src/cursor/cursor.ts +++ b/packages/core/src/cursor/cursor.ts @@ -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; @@ -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 = {}; + 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; } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 95c8c46..f527eb0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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 { - #board: Board; - // #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({ 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(middleware: Middleware, config?: C) { - this.#board.use(middleware, config); - } - - disuse(middleware: Middleware) { - this.#board.disuse(middleware); - } - - resetMiddlewareConfig(middleware: Middleware, config?: Partial) { - 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) { - const board = this.#board; - const sharer = board.getSharer(); - const viewSizeInfo = sharer.getActiveViewSizeInfo(); - board.resize({ - ...viewSizeInfo, - ...newViewSize - }); - } - - clear() { - this.#board.clear(); - } - - on(name: T, callback: (e: E[T]) => void) { - const eventHub = this.#board.getEventHub(); - eventHub.on(name, callback); - } - - off(name: T, callback: (e: E[T]) => void) { - const eventHub = this.#board.getEventHub(); - eventHub.off(name, callback); - } - - trigger(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( - type: T, - element: RecursivePartial>, - opts?: { - viewCenter?: boolean; - } - ): Element { - const { viewScaleInfo, viewSizeInfo } = this.getViewInfo(); - return createElement( - 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> & Pick - ): 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> & Pick> - ): 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 | 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 | 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'; diff --git a/packages/core/src/middlewares/common.ts b/packages/core/src/middlewares/common.ts new file mode 100644 index 0000000..7a22d02 --- /dev/null +++ b/packages/core/src/middlewares/common.ts @@ -0,0 +1,15 @@ +import type { UtilEventEmitter, CoreEventMap, CoreEventChange } from '@idraw/types'; +import { coreEventKeys } from '../static'; + +type EventHub = UtilEventEmitter; + +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); + } +} diff --git a/packages/core/src/middlewares/creator/dom.ts b/packages/core/src/middlewares/creator/dom.ts new file mode 100644 index 0000000..b8a49f8 --- /dev/null +++ b/packages/core/src/middlewares/creator/dom.ts @@ -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); + } +} diff --git a/packages/core/src/middlewares/creator/index.ts b/packages/core/src/middlewares/creator/index.ts new file mode 100644 index 0000000..ebdba19 --- /dev/null +++ b/packages/core/src/middlewares/creator/index.ts @@ -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 = ( + 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 }) => { + 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); + } + }, + }; +}; diff --git a/packages/core/src/middlewares/creator/static.ts b/packages/core/src/middlewares/creator/static.ts new file mode 100644 index 0000000..da54a26 --- /dev/null +++ b/packages/core/src/middlewares/creator/static.ts @@ -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 = { + selectAfterCreated: true, +}; + +export const classNameMap = { + // selection area + creationAreaBox: `${prefix}-creationAreaBox`, + creative: `${prefix}-creative`, +}; diff --git a/packages/core/src/middlewares/creator/styles.ts b/packages/core/src/middlewares/creator/styles.ts new file mode 100644 index 0000000..3bf3fec --- /dev/null +++ b/packages/core/src/middlewares/creator/styles.ts @@ -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(config: C): S { + const styles: S = getMiddlewareValidStyles(config, ['zIndex', 'creationAreaBorderColor']); + return styles; +} diff --git a/packages/core/src/middlewares/creator/types.ts b/packages/core/src/middlewares/creator/types.ts new file mode 100644 index 0000000..4f40d4a --- /dev/null +++ b/packages/core/src/middlewares/creator/types.ts @@ -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 | null; +}; diff --git a/packages/core/src/middlewares/creator/util.ts b/packages/core/src/middlewares/creator/util.ts new file mode 100644 index 0000000..b9128f7 --- /dev/null +++ b/packages/core/src/middlewares/creator/util.ts @@ -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, opts: Options) { + const { fill, text, href } = getDefaultMaterialAttributes(); + const defaultMtrlAttrs: Partial = { 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; +} diff --git a/packages/core/src/middlewares/dragger/index.ts b/packages/core/src/middlewares/dragger/index.ts index ca83b7c..6383393 100644 --- a/packages/core/src/middlewares/dragger/index.ts +++ b/packages/core/src/middlewares/dragger/index.ts @@ -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 = return; } eventHub.trigger(coreEventKeys.CURSOR, { - type: 'drag-default' + type: 'drag-default', }); }, @@ -28,7 +28,7 @@ export const MiddlewareDragger: Middleware = sharer.setSharedStorage(keyPrevPoint, point); isDragging = true; eventHub.trigger(coreEventKeys.CURSOR, { - type: 'drag-active' + type: 'drag-active', }); }, @@ -48,8 +48,8 @@ export const MiddlewareDragger: Middleware = isDragging = false; sharer.setSharedStorage(keyPrevPoint, null); eventHub.trigger(coreEventKeys.CURSOR, { - type: 'drag-default' + type: 'drag-default', }); - } + }, }; }; diff --git a/packages/core/src/middlewares/info/config.ts b/packages/core/src/middlewares/info/config.ts deleted file mode 100644 index 9bdc995..0000000 --- a/packages/core/src/middlewares/info/config.ts +++ /dev/null @@ -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 -}; diff --git a/packages/core/src/middlewares/info/draw-info.ts b/packages/core/src/middlewares/info/draw-info.ts index 5542b73..f4af0be 100644 --- a/packages/core/src/middlewares/info/draw-info.ts +++ b/packages/core/src/middlewares/info/draw-info.ts @@ -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; diff --git a/packages/core/src/middlewares/info/index.ts b/packages/core/src/middlewares/info/index.ts index 34cf9b4..239559d 100644 --- a/packages/core/src/middlewares/info/index.ts +++ b/packages/core/src/middlewares/info/index.ts @@ -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, }); } } } } } - } + }, }; }; diff --git a/packages/core/src/middlewares/info/static.ts b/packages/core/src/middlewares/info/static.ts new file mode 100644 index 0000000..ddcea50 --- /dev/null +++ b/packages/core/src/middlewares/info/static.ts @@ -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(config: C): S { + const styles: S = getMiddlewareValidStyles(config, ['textBackground', 'textColor']); + return styles; +} diff --git a/packages/core/src/middlewares/info/types.ts b/packages/core/src/middlewares/info/types.ts index e237924..9343e32 100644 --- a/packages/core/src/middlewares/info/types.ts +++ b/packages/core/src/middlewares/info/types.ts @@ -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 >; diff --git a/packages/core/src/middlewares/layout-selector/config.ts b/packages/core/src/middlewares/layout-selector/config.ts deleted file mode 100644 index 747675b..0000000 --- a/packages/core/src/middlewares/layout-selector/config.ts +++ /dev/null @@ -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' -}; diff --git a/packages/core/src/middlewares/layout-selector/dom.ts b/packages/core/src/middlewares/layout-selector/dom.ts new file mode 100644 index 0000000..11b61b0 --- /dev/null +++ b/packages/core/src/middlewares/layout-selector/dom.ts @@ -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) { + 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); + } +} diff --git a/packages/core/src/middlewares/layout-selector/index.ts b/packages/core/src/middlewares/layout-selector/index.ts index a845885..fced4da 100644 --- a/packages/core/src/middlewares/layout-selector/index.ts +++ b/packages/core/src/middlewares/layout-selector/index.ts @@ -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 | null = null; + let pointStartLayoutSize: RecursivePartial | 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(); - } }; }; diff --git a/packages/core/src/middlewares/layout-selector/static.ts b/packages/core/src/middlewares/layout-selector/static.ts new file mode 100644 index 0000000..ffc7bdd --- /dev/null +++ b/packages/core/src/middlewares/layout-selector/static.ts @@ -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`, +}; diff --git a/packages/core/src/middlewares/layout-selector/styles.ts b/packages/core/src/middlewares/layout-selector/styles.ts new file mode 100644 index 0000000..99dd0a5 --- /dev/null +++ b/packages/core/src/middlewares/layout-selector/styles.ts @@ -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(config, [ + 'zIndex', + 'activeColor', + 'handlerBorderColor', + 'handlerBackground', + 'handlerHoverBackground', + 'handlerActiveBackground', + ]); + return styles; +} diff --git a/packages/core/src/middlewares/layout-selector/types.ts b/packages/core/src/middlewares/layout-selector/types.ts index b83eb8f..a1d7825 100644 --- a/packages/core/src/middlewares/layout-selector/types.ts +++ b/packages/core/src/middlewares/layout-selector/types.ts @@ -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; diff --git a/packages/core/src/middlewares/layout-selector/util.ts b/packages/core/src/middlewares/layout-selector/util.ts index 67ea0c3..c3bc061 100644 --- a/packages/core/src/middlewares/layout-selector/util.ts +++ b/packages/core/src/middlewares/layout-selector/util.ts @@ -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(); diff --git a/packages/core/src/middlewares/path-creator/dom.ts b/packages/core/src/middlewares/path-creator/dom.ts new file mode 100644 index 0000000..74dbc5c --- /dev/null +++ b/packages/core/src/middlewares/path-creator/dom.ts @@ -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(); + }); +} diff --git a/packages/core/src/middlewares/path-creator/index.ts b/packages/core/src/middlewares/path-creator/index.ts new file mode 100644 index 0000000..d8c782c --- /dev/null +++ b/packages/core/src/middlewares/path-creator/index.ts @@ -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 = ( + 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; + }, + }; +}; diff --git a/packages/core/src/middlewares/path-creator/static.ts b/packages/core/src/middlewares/path-creator/static.ts new file mode 100644 index 0000000..818872c --- /dev/null +++ b/packages/core/src/middlewares/path-creator/static.ts @@ -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( + config: C +): S { + const styles: S = getMiddlewareValidStyles(config, [ + 'anchorSize', + 'anchorBorderWidth', + 'anchorBorderColor', + 'anchorBackground', + 'anchorHoverBorderColor', + 'anchorHoverBackground', + 'anchorActiveBorderColor', + 'anchorActiveBackground', + 'defaultStroke', + 'defaultStrokeWidth', + ]); + return styles; +} diff --git a/packages/core/src/middlewares/path-creator/types.ts b/packages/core/src/middlewares/path-creator/types.ts new file mode 100644 index 0000000..4a368ed --- /dev/null +++ b/packages/core/src/middlewares/path-creator/types.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export type PathSharedStorage = { + // [keySelectedPathElement]: null | Element<'path'>; // TODO +}; diff --git a/packages/core/src/middlewares/path-editor/calc.ts b/packages/core/src/middlewares/path-editor/calc.ts new file mode 100644 index 0000000..909dd75 --- /dev/null +++ b/packages/core/src/middlewares/path-editor/calc.ts @@ -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 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 .'); + + 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, + }; +} diff --git a/packages/core/src/middlewares/path-editor/dom.ts b/packages/core/src/middlewares/path-editor/dom.ts new file mode 100644 index 0000000..34d49e5 --- /dev/null +++ b/packages/core/src/middlewares/path-editor/dom.ts @@ -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 = ` + + ${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 ``; + }) + .join('')} + + `; + + 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(` + + + + `); + assembleHTMLElement($pathPreview, {}, [$svg]); +}; + +export function calcPathSize(root: HTMLElement | null) { + // TODO + if (!root) { + return null; + } + + // TODO +} diff --git a/packages/core/src/middlewares/path-editor/draw.ts b/packages/core/src/middlewares/path-editor/draw.ts new file mode 100644 index 0000000..842be16 --- /dev/null +++ b/packages/core/src/middlewares/path-editor/draw.ts @@ -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); + }); + }); +} diff --git a/packages/core/src/middlewares/path-editor/index.ts b/packages/core/src/middlewares/path-editor/index.ts new file mode 100644 index 0000000..8fbb5e5 --- /dev/null +++ b/packages/core/src/middlewares/path-editor/index.ts @@ -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 = ( + 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; + }, + }; +}; diff --git a/packages/core/src/middlewares/path-editor/parse.ts b/packages/core/src/middlewares/path-editor/parse.ts new file mode 100644 index 0000000..1763916 --- /dev/null +++ b/packages/core/src/middlewares/path-editor/parse.ts @@ -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; +} diff --git a/packages/core/src/middlewares/path-editor/static.ts b/packages/core/src/middlewares/path-editor/static.ts new file mode 100644 index 0000000..6bd907b --- /dev/null +++ b/packages/core/src/middlewares/path-editor/static.ts @@ -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( + config: C +): S { + const styles: S = getMiddlewareValidStyles(config, [ + 'zIndex', + 'anchorSize', + 'anchorSelectedSize', + 'anchorBorderWidth', + 'anchorBorderColor', + 'anchorBackground', + 'anchorHoverBorderColor', + 'anchorHoverBackground', + 'anchorActiveBorderColor', + 'anchorActiveBackground', + 'directorSize', + 'directorBorderWidth', + 'directorBorderColor', + 'directorBackground', + 'directorHoverBorderColor', + 'directorHoverBackground', + 'directorActiveBorderColor', + 'directorActiveBackground', + 'directorLineColor', + 'helperStrokeColor', + 'helperStrokeWidth', + ]); + return styles; +} diff --git a/packages/core/src/middlewares/path-editor/types.ts b/packages/core/src/middlewares/path-editor/types.ts new file mode 100644 index 0000000..93e126f --- /dev/null +++ b/packages/core/src/middlewares/path-editor/types.ts @@ -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; +}; diff --git a/packages/core/src/middlewares/path-editor/util.ts b/packages/core/src/middlewares/path-editor/util.ts new file mode 100644 index 0000000..6cf04b1 --- /dev/null +++ b/packages/core/src/middlewares/path-editor/util.ts @@ -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; +} diff --git a/packages/core/src/middlewares/pointer/index.ts b/packages/core/src/middlewares/pointer/index.ts index 64c3965..d5f85b0 100644 --- a/packages/core/src/middlewares/pointer/index.ts +++ b/packages/core/src/middlewares/pointer/index.ts @@ -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 = (opts) => { const { boardContent, eventHub, sharer } = opts; @@ -39,11 +39,11 @@ export const MiddlewarePointer: Middleware; +export type DeepPointerSharedStorage = Pick; diff --git a/packages/core/src/middlewares/ruler/config.ts b/packages/core/src/middlewares/ruler/config.ts deleted file mode 100644 index c090cc9..0000000 --- a/packages/core/src/middlewares/ruler/config.ts +++ /dev/null @@ -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 -}; diff --git a/packages/core/src/middlewares/ruler/index.ts b/packages/core/src/middlewares/ruler/index.ts index 69cf0b1..5f4f6c2 100644 --- a/packages/core/src/middlewares/ruler/index.ts +++ b/packages/core/src/middlewares/ruler/index.ts @@ -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 = ( opts, @@ -21,9 +23,11 @@ export const MiddlewareRuler: Middleware { - 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(config: C): S { + const styles: S = getMiddlewareValidStyles(config, [ + 'background', + 'stroke', + 'scaleColor', + 'textColor', + 'gridColor', + 'gridPrimaryColor', + 'selectedAreaColor', + ]); + return styles; +} diff --git a/packages/core/src/middlewares/ruler/types.ts b/packages/core/src/middlewares/ruler/types.ts index 4202f96..dcee846 100644 --- a/packages/core/src/middlewares/ruler/types.ts +++ b/packages/core/src/middlewares/ruler/types.ts @@ -1,4 +1,7 @@ -import { keySelectedElementList, keyActionType } from '../selector'; +import { keySelectedMaterialList, keyActionType } from '../selector'; import type { DeepSelectorSharedStorage } from '../selector'; -export type DeepRulerSharedStorage = Pick; +export type DeepRulerSharedStorage = Pick< + DeepSelectorSharedStorage, + typeof keySelectedMaterialList | typeof keyActionType +>; diff --git a/packages/core/src/middlewares/ruler/util.ts b/packages/core/src/middlewares/ruler/util.ts index 8448400..3799281 100644 --- a/packages/core/src/middlewares/ruler/util.ts +++ b/packages/core/src/middlewares/ruler/util.ts @@ -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; 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; } diff --git a/packages/core/src/middlewares/scaler/index.ts b/packages/core/src/middlewares/scaler/index.ts index a37319f..89cc1cd 100644 --- a/packages/core/src/middlewares/scaler/index.ts +++ b/packages/core/src/middlewares/scaler/index.ts @@ -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, CoreEventMap> = (opts) => { const { viewer, sharer, eventHub } = opts; @@ -27,6 +27,6 @@ export const MiddlewareScaler: Middleware, CoreEventMap> = ( viewer.drawFrame(); const scaleNum = formatNumber(scale); eventHub.trigger(coreEventKeys.SCALE, { scale: scaleNum }); - } + }, }; }; diff --git a/packages/core/src/middlewares/scroller/config.ts b/packages/core/src/middlewares/scroller/config.ts deleted file mode 100644 index 20c1af0..0000000 --- a/packages/core/src/middlewares/scroller/config.ts +++ /dev/null @@ -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' -}; diff --git a/packages/core/src/middlewares/scroller/dom.ts b/packages/core/src/middlewares/scroller/dom.ts new file mode 100644 index 0000000..b528436 --- /dev/null +++ b/packages/core/src/middlewares/scroller/dom.ts @@ -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; +} diff --git a/packages/core/src/middlewares/scroller/index.ts b/packages/core/src/middlewares/scroller/index.ts index 1a2f19c..cb456a1 100644 --- a/packages/core/src/middlewares/scroller/index.ts +++ b/packages/core/src/middlewares/scroller/index.ts @@ -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 = ( 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 { - 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 { 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 { - 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 { isBusy = false; @@ -149,31 +239,18 @@ export const MiddlewareScroller: Middleware `${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`, +}; diff --git a/packages/core/src/middlewares/scroller/styles.ts b/packages/core/src/middlewares/scroller/styles.ts new file mode 100644 index 0000000..967a80a --- /dev/null +++ b/packages/core/src/middlewares/scroller/styles.ts @@ -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(config: C): S { + const styles: S = getMiddlewareValidStyles(config, [ + 'zIndex', + 'thumbBackground', + 'thumbBorderColor', + 'hoverThumbBackground', + 'hoverThumbBorderColor', + 'activeThumbBackground', + 'activeThumbBorderColor', + ]); + return styles; +} diff --git a/packages/core/src/middlewares/scroller/types.ts b/packages/core/src/middlewares/scroller/types.ts index 721d794..81befd8 100644 --- a/packages/core/src/middlewares/scroller/types.ts +++ b/packages/core/src/middlewares/scroller/types.ts @@ -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; +}; diff --git a/packages/core/src/middlewares/scroller/util.ts b/packages/core/src/middlewares/scroller/util.ts index f10af66..2d1b977 100644 --- a/packages/core/src/middlewares/scroller/util.ts +++ b/packages/core/src/middlewares/scroller/util.ts @@ -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; } diff --git a/packages/core/src/middlewares/selector/config.ts b/packages/core/src/middlewares/selector/config.ts deleted file mode 100644 index 941e5e8..0000000 --- a/packages/core/src/middlewares/selector/config.ts +++ /dev/null @@ -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 | [] -export const keyHoverElementVertexes = Symbol(`${key}_hoverElementVertexes`); // ViewRectVertexes | null -export const keySelectedElementList = Symbol(`${key}_selectedElementList`); // Array> | [] -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> | [] -export const keyGroupQueueVertexesList = Symbol(`${key}_groupQueueVertexesList`); // Array | [] -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 -}; diff --git a/packages/core/src/middlewares/selector/dom.ts b/packages/core/src/middlewares/selector/dom.ts new file mode 100644 index 0000000..497fde4 --- /dev/null +++ b/packages/core/src/middlewares/selector/dom.ts @@ -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; + +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 | null; + viewGroupSizeQueue: Required[]; + }) => 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; +} diff --git a/packages/core/src/middlewares/selector/draw-auxiliary.ts b/packages/core/src/middlewares/selector/draw-auxiliary.ts deleted file mode 100644 index 70249cc..0000000 --- a/packages/core/src/middlewares/selector/draw-auxiliary.ts +++ /dev/null @@ -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); -// } diff --git a/packages/core/src/middlewares/selector/draw-base.ts b/packages/core/src/middlewares/selector/draw-base.ts index 88ee007..20161ed 100644 --- a/packages/core/src/middlewares/selector/draw-base.ts +++ b/packages/core/src/middlewares/selector/draw-base.ts @@ -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, }); } diff --git a/packages/core/src/middlewares/selector/draw-debug.ts b/packages/core/src/middlewares/selector/draw-debug.ts deleted file mode 100644 index 73050a8..0000000 --- a/packages/core/src/middlewares/selector/draw-debug.ts +++ /dev/null @@ -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 }); - } -} diff --git a/packages/core/src/middlewares/selector/draw-reference.ts b/packages/core/src/middlewares/selector/draw-reference.ts index 79c87b5..2600c3e 100644 --- a/packages/core/src/middlewares/selector/draw-reference.ts +++ b/packages/core/src/middlewares/selector/draw-reference.ts @@ -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; - yLines?: Array; - style: MiddlewareSelectorStyle; + xLines?: Array; + yLines?: Array; + 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 }; diff --git a/packages/core/src/middlewares/selector/draw-wrapper.ts b/packages/core/src/middlewares/selector/draw-wrapper.ts deleted file mode 100644 index f8cc1b3..0000000 --- a/packages/core/src/middlewares/selector/draw-wrapper.ts +++ /dev/null @@ -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[], - opts?: Omit -) { - 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); - } -} diff --git a/packages/core/src/middlewares/selector/index.ts b/packages/core/src/middlewares/selector/index.ts index 7e98a3f..3ca553d 100644 --- a/packages/core/src/middlewares/selector/index.ts +++ b/packages/core/src/middlewares/selector/index.ts @@ -1,100 +1,87 @@ import { is, - calcElementsViewInfo, - calcElementVertexesInGroup, - calcElementQueueVertexesQueueInGroup, - calcElementSizeController, - calcElementCenterFromVertexes, + calcMaterialsViewInfo, rotatePointInGroup, getGroupQueueFromList, - findElementsFromList, - findElementsFromListByPositions, - getElementPositionFromList, - getElementPositionMapFromList, - resizeEffectGroupElement, - getElementSize, - calcPointMoveElementInGroup, - isSameElementSize, - toFlattenElement + findMaterialsFromList, + findMaterialsFromListByPositions, + getMaterialPositionFromList, + getMaterialPositionMapFromList, + resizeEffectGroupMaterial, + getMaterialSize, + calcPointMoveMaterialInGroup, + toFlattenMaterial, + isPointInMiddlewareElement, } from '@idraw/util'; import type { - Data, - ViewRectVertexes, CoreEventMap, ViewScaleInfo, ViewSizeInfo, - ElementSizeController, MiddlewareSelectorConfig, - ElementSize, - ModifyRecord + MaterialSize, + StrictMaterial, + ModifyRecord, + Material, + ModifyType, } from '@idraw/types'; import type { Point, - PointSize, PointWatcherEvent, Middleware, - Element, ActionType, ResizeType, DeepSelectorSharedStorage, - ElementType, - PointTarget + PointTarget, } from './types'; -import { - drawHoverVertexesWrapper, - drawLockedVertexesWrapper, - drawArea, - drawListArea, - drawGroupQueueVertexesWrappers, - drawSelectedElementControllersVertexes -} from './draw-wrapper'; -import { drawReferenceLines } from './draw-reference'; import { getPointTarget, - resizeElement, - rotateElement, + resizeMaterial, + rotateMaterial, getSelectedListArea, - calcSelectedElementsArea, - isElementInGroup, - isPointInViewActiveGroup + calcSelectedMaterialsArea, + isMaterialInGroup, } from './util'; import { + keyPrevPoint, + keyPointStartMaterialSizeList, + keyMoveOriginalStartPoint, + keyMoveOriginalStartMaterialSize, + keyInBusyMode, + keyHasChangedData, + keyStartResizeGroupRecord, + keyEndResizeGroupRecord, + + // legacy keyActionType, keyResizeType, keyAreaStart, keyAreaEnd, keyGroupQueue, - keyGroupQueueVertexesList, - keyHoverElement, - keyHoverElementVertexes, - keySelectedElementList, - keySelectedElementListVertexes, - keySelectedElementController, - keySelectedElementPosition, + keyHoverMaterial, + keySelectedMaterialList, + keySelectedMaterialPosition, keyIsMoving, keyEnableSelectInGroup, keyEnableSnapToGrid, - controllerSize, - rotateControllerSize, - rotateControllerPosition, - defaultStyle - // keyDebugElemCenter, - // keyDebugEnd0, - // keyDebugEndHorizontal, - // keyDebugEndVertical, - // keyDebugStartHorizontal, - // keyDebugStartVertical -} from './config'; + defaultStyle, + clearStorage, +} from './static'; import { calcReferenceInfo } from './reference'; -import { coreEventKeys } from '../../config'; +import { coreEventKeys } from '../../static'; import { keyLayoutIsSelected, keyLayoutIsBusyMoving } from '../layout-selector'; -import { createRotateControllerPattern } from './pattern'; import { MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE } from '../info'; -// import { drawDebugStoreSelectedElementController } from './draw-debug'; +import { getRootClassName } from './static'; +import { initRoot, isPointInActiveGroup } from './dom'; +import { initStyles, destroyStyles, getMiddlewareSelectorStyles } from './styles'; +import { dragAndResizeMaterial } from './resize'; +import { renderFrame } from './render-frame'; +import { triggerChangeEvent } from '../common'; -export { keySelectedElementList, keyHoverElement, keyActionType, keyResizeType, keyGroupQueue }; +export { keySelectedMaterialList, keyHoverMaterial, keyActionType, keyResizeType, keyGroupQueue }; export type { DeepSelectorSharedStorage, ActionType }; +export { getMiddlewareSelectorStyles }; + export const MiddlewareSelector: Middleware< DeepSelectorSharedStorage, CoreEventMap & { @@ -104,163 +91,106 @@ export const MiddlewareSelector: Middleware< > = (opts, config) => { let innerConfig = { ...defaultStyle, - ...config + ...config, }; + const styles = getMiddlewareSelectorStyles(innerConfig); + + const rootClassName = getRootClassName(); + let $root: HTMLDivElement | null = null; + const { viewer, sharer, boardContent, calculator, eventHub } = opts; const { overlayContext } = boardContent; - let prevPoint: Point | null = null; - let pointStartElementSizeList: Array & { uuid: string }> = []; - let moveOriginalStartPoint: Point | null = null; - let moveOriginalStartElementSize: ElementSize | null = null; - let inBusyMode: 'resize' | 'drag' | 'drag-list' | 'area' | null = null; - let hasChangedData: boolean | null = null; - - let rotateControllerPattern = createRotateControllerPattern({ - fill: innerConfig.activeColor, - devicePixelRatio: sharer.getActiveViewSizeInfo().devicePixelRatio - }); - - let startResizeGroupRecord: ModifyRecord<'resizeElements'> | null = null; - let endResizeGroupRecord: ModifyRecord<'resizeElements'> | null = null; sharer.setSharedStorage(keyActionType, null); sharer.setSharedStorage(keyEnableSnapToGrid, true); - const getActiveElements = () => { - return sharer.getSharedStorage(keySelectedElementList); - }; - - const pushGroupQueue = (elem: Element<'group'>) => { + const pushGroupQueue = (mtrl: Material) => { let groupQueue = sharer.getSharedStorage(keyGroupQueue); if (!Array.isArray(groupQueue)) { groupQueue = []; } if (groupQueue.length > 0) { - if (isElementInGroup(elem, groupQueue[groupQueue.length - 1])) { - groupQueue.push(elem); + if (isMaterialInGroup(mtrl, groupQueue[groupQueue.length - 1])) { + groupQueue.push(mtrl as StrictMaterial<'group'>); } else { groupQueue = []; } } else if (groupQueue.length === 0) { - groupQueue.push(elem); + groupQueue.push(mtrl as StrictMaterial<'group'>); } - const vertexesList = calcElementQueueVertexesQueueInGroup(groupQueue); sharer.setSharedStorage(keyGroupQueue, groupQueue); - sharer.setSharedStorage(keyGroupQueueVertexesList, vertexesList); return groupQueue.length > 0; }; - const updateHoverElement = (elem: Element | null) => { - sharer.setSharedStorage(keyHoverElement, elem); - let vertexes: ViewRectVertexes | null = null; - if (elem) { - vertexes = calcElementVertexesInGroup(elem, { - groupQueue: sharer.getSharedStorage(keyGroupQueue) - }); - } - sharer.setSharedStorage(keyHoverElementVertexes, vertexes); - }; - - const updateSelectedElemenetController = () => { - const list = sharer.getSharedStorage(keySelectedElementList); + const updateSelectedMaterialList = (list: Material[], opts?: { triggerEvent?: boolean }) => { + sharer.setSharedStorage(keySelectedMaterialList, list); if (list.length === 1) { - const controller = calcElementSizeController(list[0], { - groupQueue: sharer.getSharedStorage(keyGroupQueue), - controllerSize, - viewScaleInfo: sharer.getActiveViewScaleInfo(), - rotateControllerPosition, - rotateControllerSize - }); - sharer.setSharedStorage(keySelectedElementController, controller); - } - }; - - const updateSelectedElementList = (list: Element[], opts?: { triggerEvent?: boolean }) => { - sharer.setSharedStorage(keySelectedElementList, list); - if (list.length === 1) { - updateSelectedElemenetController(); sharer.setSharedStorage( - keySelectedElementPosition, - getElementPositionFromList(list[0].uuid, sharer.getActiveStorage('data')?.elements || []) + keySelectedMaterialPosition, + getMaterialPositionFromList(list[0].id, sharer.getActiveStorage('data')?.materials || []) ); } else { - sharer.setSharedStorage(keySelectedElementController, null); - sharer.setSharedStorage(keySelectedElementPosition, []); + sharer.setSharedStorage(keySelectedMaterialPosition, []); } if (opts?.triggerEvent === true) { - const uuids = list.map((elem) => elem.uuid); + const ids = list.map((mtrl) => mtrl.id); const data = sharer.getActiveStorage('data'); - const positionMap = getElementPositionMapFromList(uuids, data?.elements || []); + const positionMap = getMaterialPositionMapFromList(ids, data?.materials || []); eventHub.trigger(coreEventKeys.SELECT, { type: 'clickCanvas', - uuids, - positions: list.map((elem) => [...positionMap[elem.uuid]]) + ids, + positions: list.map((mtrl) => [...positionMap[mtrl.id]]), }); } }; - const pointTargetBaseOptions = () => { + const pointTargetBaseOptions = (pwe: PointWatcherEvent) => { return { ctx: overlayContext, calculator, data: sharer.getActiveStorage('data'), - selectedElements: getActiveElements(), + selectedMaterials: sharer.getSharedStorage(keySelectedMaterialList), viewScaleInfo: sharer.getActiveViewScaleInfo(), viewSizeInfo: sharer.getActiveViewSizeInfo(), groupQueue: sharer.getSharedStorage(keyGroupQueue), areaSize: null, - selectedElementController: sharer.getSharedStorage(keySelectedElementController), - selectedElementPosition: sharer.getSharedStorage(keySelectedElementPosition) + selectedMaterialPosition: sharer.getSharedStorage(keySelectedMaterialPosition), + nativeEvent: pwe.nativeEvent, }; }; - const clear = () => { - startResizeGroupRecord = null; - endResizeGroupRecord = null; - sharer.setSharedStorage(keyActionType, null); - sharer.setSharedStorage(keyResizeType, null); - sharer.setSharedStorage(keyAreaStart, null); - sharer.setSharedStorage(keyAreaEnd, null); - sharer.setSharedStorage(keyGroupQueue, []); - sharer.setSharedStorage(keyGroupQueueVertexesList, []); - sharer.setSharedStorage(keyHoverElement, null); - sharer.setSharedStorage(keyHoverElementVertexes, null); - sharer.setSharedStorage(keySelectedElementList, []); - sharer.setSharedStorage(keySelectedElementListVertexes, null); - sharer.setSharedStorage(keySelectedElementController, null); - sharer.setSharedStorage(keySelectedElementPosition, []); - sharer.setSharedStorage(keyIsMoving, null); - }; + const clear = () => clearStorage(sharer); clear(); - const selectCallback = ({ uuids = [], positions }: CoreEventMap[typeof coreEventKeys.SELECT]) => { - let elements: Element[] = []; + const selectCallback = ({ ids = [], positions }: CoreEventMap[typeof coreEventKeys.SELECT]) => { + let materials: Material[] = []; const actionType = sharer.getSharedStorage(keyActionType); const data = sharer.getActiveStorage('data'); if (positions && Array.isArray(positions)) { - elements = findElementsFromListByPositions(positions, data?.elements || []); + materials = findMaterialsFromListByPositions(positions, data?.materials || []); } else { - elements = findElementsFromList(uuids, data?.elements || []); + materials = findMaterialsFromList(ids, data?.materials || []); } let needRefresh = false; - if (!actionType && elements.length === 1) { - // TODO + if (!actionType && materials.length === 1) { sharer.setSharedStorage(keyActionType, 'select'); needRefresh = true; - } else if (actionType === 'select' && elements.length === 1) { - // TODO + } else if (actionType === 'select' && materials.length === 1) { needRefresh = true; } if (needRefresh) { - const elem = elements[0]; - const groupQueue = getGroupQueueFromList(elem.uuid, data?.elements || []); + const mtrl = materials[0]; + const groupQueue = getGroupQueueFromList(mtrl.id, data?.materials || []); sharer.setSharedStorage(keyGroupQueue, groupQueue); - updateSelectedElementList(elements); - pointStartElementSizeList = [{ ...getElementSize(elements[0]), uuid: elements[0].uuid }]; + updateSelectedMaterialList(materials); + + sharer.setSharedStorage(keyPointStartMaterialSizeList, [ + { ...getMaterialSize(materials[0]), id: materials[0].id }, + ]); viewer.drawFrame(); } }; @@ -281,6 +211,9 @@ export const MiddlewareSelector: Middleware< return { name: '@middleware/selector', use() { + initStyles(rootClassName, styles); + $root = initRoot({ rootClassName, $container: opts.container as HTMLElement }); + eventHub.on(coreEventKeys.SELECT, selectCallback); eventHub.on(coreEventKeys.CLEAR_SELECT, selectClearCallback); eventHub.on(coreEventKeys.SELECT_IN_GROUP, selectInGroupCallback); @@ -288,12 +221,19 @@ export const MiddlewareSelector: Middleware< }, disuse() { + destroyStyles(rootClassName); eventHub.off(coreEventKeys.SELECT, selectCallback); eventHub.off(coreEventKeys.CLEAR_SELECT, selectClearCallback); eventHub.off(coreEventKeys.SELECT_IN_GROUP, selectInGroupCallback); eventHub.off(coreEventKeys.SNAP_TO_GRID, setSnapToSnapCallback); + + // clear dom + $root?.remove(); + + // clear data clear(); innerConfig = null as any; + $root = null; }, resetConfig(config) { @@ -301,6 +241,13 @@ export const MiddlewareSelector: Middleware< }, hover: (e: PointWatcherEvent) => { + if (!isPointInMiddlewareElement(e.nativeEvent, { $root, rootClassName })) { + if (sharer.getSharedStorage(keyHoverMaterial)) { + sharer.setSharedStorage(keyHoverMaterial, null); + viewer.drawFrame(); + } + return; + } const layoutIsSelected = sharer.getSharedStorage(keyLayoutIsSelected); const layoutIsBusyMoving = sharer.getSharedStorage(keyLayoutIsBusyMoving); if (layoutIsBusyMoving === true) { @@ -316,153 +263,149 @@ export const MiddlewareSelector: Middleware< return; } const cursor: string | null = target.type; - if (inBusyMode === null) { + if (sharer.getSharedStorage(keyInBusyMode) === null) { eventHub.trigger(coreEventKeys.CURSOR, { type: cursor, groupQueue: target.groupQueue, - element: target.elements[0] + material: target.materials[0], }); } }; if (groupQueue?.length > 0) { // in group - const isInActiveGroup = isPointInViewActiveGroup(e.point, { - ctx: overlayContext, - viewScaleInfo: sharer.getActiveViewScaleInfo(), - viewSizeInfo: sharer.getActiveViewSizeInfo(), - groupQueue: sharer.getSharedStorage(keyGroupQueue) + const isInActiveGroup = isPointInActiveGroup(e.nativeEvent, { + $root, + groupQueue: sharer.getSharedStorage(keyGroupQueue), }); if (!isInActiveGroup) { - updateHoverElement(null); + sharer.setSharedStorage(keyHoverMaterial, null); viewer.drawFrame(); return; } - const target = getPointTarget(e.point, pointTargetBaseOptions()); + const target = getPointTarget(e.point, pointTargetBaseOptions(e)); triggerCursor(target); if (resizeType || (['area', 'drag', 'drag-list'] as ActionType[]).includes(actionType)) { - updateHoverElement(null); + sharer.setSharedStorage(keyHoverMaterial, null); viewer.drawFrame(); return; } - if (target?.elements?.length === 1) { - updateHoverElement(target.elements[0]); + if (target?.materials?.length === 1) { + sharer.setSharedStorage(keyHoverMaterial, target.materials[0]); viewer.drawFrame(); return; } - updateHoverElement(null); + sharer.setSharedStorage(keyHoverMaterial, null); viewer.drawFrame(); return; } // not in group if (resizeType || (['area', 'drag', 'drag-list'] as ActionType[]).includes(actionType)) { - updateHoverElement(null); + sharer.setSharedStorage(keyHoverMaterial, null); return; } if (actionType === 'drag') { - updateHoverElement(null); + sharer.setSharedStorage(keyHoverMaterial, null); return; } - const selectedElements = getActiveElements(); + const selectedMaterials = sharer.getSharedStorage(keySelectedMaterialList); const viewScaleInfo = sharer.getActiveViewScaleInfo(); const viewSizeInfo = sharer.getActiveViewSizeInfo(); const target = getPointTarget(e.point, { - ...pointTargetBaseOptions(), - areaSize: calcSelectedElementsArea(selectedElements, { + ...pointTargetBaseOptions(e), + areaSize: calcSelectedMaterialsArea(selectedMaterials, { viewScaleInfo, viewSizeInfo, - calculator - }) + calculator, + }), }); triggerCursor(target); if (target.type === null) { - if (sharer.getSharedStorage(keyHoverElement) || sharer.getSharedStorage(keyHoverElementVertexes)) { - sharer.setSharedStorage(keyHoverElement, null); - sharer.setSharedStorage(keyHoverElementVertexes, null); + if (sharer.getSharedStorage(keyHoverMaterial)) { + sharer.setSharedStorage(keyHoverMaterial, null); viewer.drawFrame(); } return; } if ( - target.type === 'over-element' && + target.type === 'over-material' && sharer.getSharedStorage(keyActionType) === 'select' && - target.elements.length === 1 && - target.elements[0].uuid === getActiveElements()?.[0]?.uuid + target.materials.length === 1 && + target.materials[0].id === sharer.getSharedStorage(keySelectedMaterialList)?.[0]?.id ) { return; } if ( - target.type === 'over-element' && + target.type === 'over-material' && sharer.getSharedStorage(keyActionType) === null && - target.elements.length === 1 && - target.elements[0].uuid === sharer.getSharedStorage(keyHoverElement)?.uuid + target.materials.length === 1 && + target.materials[0].id === sharer.getSharedStorage(keyHoverMaterial)?.id ) { return; } - if (target.type === 'over-element' && target?.elements?.length === 1) { - updateHoverElement(target.elements[0]); + if (target.type === 'over-material' && target?.materials?.length === 1) { + sharer.setSharedStorage(keyHoverMaterial, target.materials[0]); viewer.drawFrame(); return; } - if (sharer.getSharedStorage(keyHoverElement)) { - updateHoverElement(null); + if (sharer.getSharedStorage(keyHoverMaterial)) { + sharer.setSharedStorage(keyHoverMaterial, null); viewer.drawFrame(); return; } }, pointStart: (e: PointWatcherEvent) => { - prevPoint = e.point; - moveOriginalStartPoint = e.point; + if (!isPointInMiddlewareElement(e.nativeEvent, { $root, rootClassName })) { + return; + } + sharer.setSharedStorage(keyPrevPoint, e.point); + sharer.setSharedStorage(keyMoveOriginalStartPoint, e.point); + sharer.setSharedStorage(keyStartResizeGroupRecord, null); + sharer.setSharedStorage(keyEndResizeGroupRecord, null); - startResizeGroupRecord = null; - endResizeGroupRecord = null; sharer.setSharedStorage(keyActionType, null); sharer.setSharedStorage(keyResizeType, null); sharer.setSharedStorage(keyAreaStart, null); sharer.setSharedStorage(keyAreaEnd, null); - sharer.setSharedStorage(keyHoverElement, null); + sharer.setSharedStorage(keyHoverMaterial, null); const groupQueue = sharer.getSharedStorage(keyGroupQueue); if (groupQueue?.length > 0) { - if ( - isPointInViewActiveGroup(e.point, { - ctx: overlayContext, - viewScaleInfo: sharer.getActiveViewScaleInfo(), - viewSizeInfo: sharer.getActiveViewSizeInfo(), - groupQueue - }) - ) { - const target = getPointTarget(e.point, pointTargetBaseOptions()); - const isLockedElement = target?.elements?.length === 1 && target.elements[0]?.operations?.locked === true; + if (isPointInActiveGroup(e.nativeEvent, { $root, groupQueue })) { + const target = getPointTarget(e.point, pointTargetBaseOptions(e)); + const isLockedMaterial = target?.materials?.length === 1 && target.materials[0]?.operations?.locked === true; - updateHoverElement(null); + sharer.setSharedStorage(keyHoverMaterial, null); - if (target?.elements?.length === 1) { - moveOriginalStartElementSize = getElementSize(target?.elements[0]); + if (target?.materials?.length === 1) { + sharer.setSharedStorage(keyMoveOriginalStartMaterialSize, getMaterialSize(target?.materials[0])); } - if (isLockedElement === true) { + if (isLockedMaterial === true) { clear(); - } else if (target.type === 'over-element' && target?.elements?.length === 1) { - updateSelectedElementList([target.elements[0]], { triggerEvent: true }); + } else if (target.type === 'over-material' && target?.materials?.length === 1) { + updateSelectedMaterialList([target.materials[0]], { triggerEvent: true }); sharer.setSharedStorage(keyActionType, 'drag'); - pointStartElementSizeList = [{ ...getElementSize(target?.elements[0]), uuid: target?.elements[0].uuid }]; + + sharer.setSharedStorage(keyPointStartMaterialSizeList, [ + { ...getMaterialSize(target?.materials[0]), id: target?.materials[0].id }, + ]); } else if (target.type?.startsWith('resize-')) { sharer.setSharedStorage(keyResizeType, target.type as ResizeType); sharer.setSharedStorage(keyActionType, 'resize'); } else { - updateSelectedElementList([], { triggerEvent: true }); + updateSelectedMaterialList([], { triggerEvent: true }); } } else { // TODO @@ -473,37 +416,39 @@ export const MiddlewareSelector: Middleware< } // not in group - const listAreaSize = calcSelectedElementsArea(getActiveElements(), { + const listAreaSize = calcSelectedMaterialsArea(sharer.getSharedStorage(keySelectedMaterialList), { viewScaleInfo: sharer.getActiveViewScaleInfo(), viewSizeInfo: sharer.getActiveViewSizeInfo(), - calculator + calculator, }); const target = getPointTarget(e.point, { - ...pointTargetBaseOptions(), + ...pointTargetBaseOptions(e), areaSize: listAreaSize, - groupQueue: [] + groupQueue: [], }); - const isLockedElement = target?.elements?.length === 1 && target.elements[0]?.operations?.locked === true; - // if (!isLockedElement) { - updateHoverElement(null); - // } + const isLockedMaterial = target?.materials?.length === 1 && target.materials[0]?.operations?.locked === true; + sharer.setSharedStorage(keyHoverMaterial, null); - if (target?.elements?.length === 1) { - moveOriginalStartElementSize = getElementSize(target?.elements[0]); + if (target?.materials?.length === 1) { + sharer.setSharedStorage(keyMoveOriginalStartMaterialSize, getMaterialSize(target?.materials[0])); } - if (isLockedElement === true) { + if (isLockedMaterial === true) { clear(); + sharer.setSharedStorage(keyHoverMaterial, target?.materials[0]); sharer.setSharedStorage(keyActionType, 'area'); sharer.setSharedStorage(keyAreaStart, e.point); - updateSelectedElementList([], { triggerEvent: true }); + updateSelectedMaterialList([], { triggerEvent: true }); } else if (target.type === 'list-area') { sharer.setSharedStorage(keyActionType, 'drag-list'); - } else if (target.type === 'over-element' && target?.elements?.length === 1) { - updateSelectedElementList([target.elements[0]], { triggerEvent: true }); + } else if (target.type === 'over-material' && target?.materials?.length === 1) { + updateSelectedMaterialList([target.materials[0]], { triggerEvent: true }); sharer.setSharedStorage(keyActionType, 'drag'); - pointStartElementSizeList = [{ ...getElementSize(target?.elements[0]), uuid: target?.elements[0].uuid }]; + + sharer.setSharedStorage(keyPointStartMaterialSizeList, [ + { ...getMaterialSize(target?.materials[0]), id: target?.materials[0].id }, + ]); } else if (target.type?.startsWith('resize-')) { sharer.setSharedStorage(keyResizeType, target.type as ResizeType); sharer.setSharedStorage(keyActionType, 'resize'); @@ -511,54 +456,60 @@ export const MiddlewareSelector: Middleware< clear(); sharer.setSharedStorage(keyActionType, 'area'); sharer.setSharedStorage(keyAreaStart, e.point); - updateSelectedElementList([], { triggerEvent: true }); + updateSelectedMaterialList([], { triggerEvent: true }); } viewer.drawFrame(); }, pointMove: (e: PointWatcherEvent) => { + if (!isPointInMiddlewareElement(e.nativeEvent, { $root, rootClassName })) { + return; + } sharer.setSharedStorage(keyIsMoving, true); const data = sharer.getActiveStorage('data'); - const elems = getActiveElements(); + const mtrls = sharer.getSharedStorage(keySelectedMaterialList); const scale = sharer.getActiveStorage('scale') || 1; const viewScaleInfo: ViewScaleInfo = sharer.getActiveViewScaleInfo() as unknown as ViewScaleInfo; const viewSizeInfo: ViewSizeInfo = sharer.getActiveViewSizeInfo() as unknown as ViewSizeInfo; - const start = prevPoint; - const originalStart = moveOriginalStartPoint; + + const start = sharer.getSharedStorage(keyPrevPoint); + const originalStart = sharer.getSharedStorage(keyMoveOriginalStartPoint); + const end = e.point; const resizeType = sharer.getSharedStorage(keyResizeType); const actionType = sharer.getSharedStorage(keyActionType); const groupQueue = sharer.getSharedStorage(keyGroupQueue); const enableSnapToGrid = sharer.getSharedStorage(keyEnableSnapToGrid); + let modifyType: ModifyType = 'unknown'; if (actionType === 'drag') { - hasChangedData = true; - inBusyMode = 'drag'; + sharer.setSharedStorage(keyHasChangedData, true); + sharer.setSharedStorage(keyInBusyMode, 'drag'); eventHub.trigger(MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE, { show: false }); if ( data && - elems?.length === 1 && - moveOriginalStartElementSize && + mtrls?.length === 1 && + sharer.getSharedStorage(keyMoveOriginalStartMaterialSize) && originalStart && end && - elems[0]?.operations?.locked !== true + mtrls[0]?.operations?.locked !== true ) { - const { moveX, moveY } = calcPointMoveElementInGroup(originalStart, end, groupQueue); + const { moveX, moveY } = calcPointMoveMaterialInGroup(originalStart, end, groupQueue); let totalMoveX = calculator.toGridNum(moveX / scale); let totalMoveY = calculator.toGridNum(moveY / scale); if (enableSnapToGrid === true) { - const referenceInfo = calcReferenceInfo(elems[0].uuid, { + const referenceInfo = calcReferenceInfo(mtrls[0].id, { calculator, data, groupQueue, viewScaleInfo, - viewSizeInfo + viewSizeInfo, }); try { if (referenceInfo) { @@ -574,44 +525,54 @@ export const MiddlewareSelector: Middleware< console.error(err); } } + const moveOriginalStartMaterialSize = sharer.getSharedStorage(keyMoveOriginalStartMaterialSize) as Point; + const newX = calculator.toGridNum(moveOriginalStartMaterialSize.x + totalMoveX); + const newY = calculator.toGridNum(moveOriginalStartMaterialSize.y + totalMoveY); - elems[0].x = calculator.toGridNum(moveOriginalStartElementSize.x + totalMoveX); - elems[0].y = calculator.toGridNum(moveOriginalStartElementSize.y + totalMoveY); - updateSelectedElementList([elems[0]]); - calculator.modifyVirtualFlatItemMap(data, { + dragAndResizeMaterial(mtrls[0], { + x: newX, + y: newY, + width: mtrls[0].width, + height: mtrls[0].height, + }); + + updateSelectedMaterialList([mtrls[0]]); + modifyType = 'updateMaterial'; + calculator.modifyVirtualItemMap(data, { modifyInfo: { - type: 'updateElement', + type: modifyType, content: { - element: elems[0], - position: sharer.getSharedStorage(keySelectedElementPosition) || [] - } + material: mtrls[0], + position: sharer.getSharedStorage(keySelectedMaterialPosition) || [], + }, }, viewSizeInfo, - viewScaleInfo + viewScaleInfo, }); } viewer.drawFrame(); } else if (actionType === 'drag-list') { - hasChangedData = true; - inBusyMode = 'drag-list'; - if (data && originalStart && start && end && elems?.length > 1) { + sharer.setSharedStorage(keyHasChangedData, true); + sharer.setSharedStorage(keyInBusyMode, 'drag-list'); + + if (data && originalStart && start && end && mtrls?.length > 1) { const moveX = (end.x - start.x) / scale; const moveY = (end.y - start.y) / scale; - elems.forEach((elem: Element) => { - if (elem && elem?.operations?.locked !== true) { - elem.x = calculator.toGridNum(elem.x + moveX); - elem.y = calculator.toGridNum(elem.y + moveY); - - calculator.modifyVirtualFlatItemMap(data, { + mtrls.forEach((mtrl: Material) => { + if (mtrl && mtrl?.operations?.locked !== true) { + mtrl.x = calculator.toGridNum(mtrl.x + moveX); + mtrl.y = calculator.toGridNum(mtrl.y + moveY); + modifyType = 'updateMaterial'; + calculator.modifyVirtualItemMap(data, { modifyInfo: { - type: 'updateElement', + type: modifyType, content: { - element: elem, - position: getElementPositionFromList(elem.uuid, data.elements) || [] - } + material: mtrl, + position: getMaterialPositionFromList(mtrl.id, data.materials) || [], + }, }, viewSizeInfo, - viewScaleInfo + viewScaleInfo, }); } }); @@ -622,125 +583,149 @@ export const MiddlewareSelector: Middleware< } else if (actionType === 'resize') { if ( data && - elems?.length === 1 && + mtrls?.length === 1 && originalStart && - moveOriginalStartElementSize && + sharer.getSharedStorage(keyMoveOriginalStartMaterialSize) && resizeType?.startsWith('resize-') ) { - hasChangedData = true; - inBusyMode = 'resize'; - const pointGroupQueue: Element<'group'>[] = []; + sharer.setSharedStorage(keyHasChangedData, true); + sharer.setSharedStorage(keyInBusyMode, 'resize'); + + const pointGroupQueue: StrictMaterial<'group'>[] = []; groupQueue.forEach((group) => { - const { x, y, w, h, angle = 0 } = group; + const { x, y, width, height, angle = 0 } = group; pointGroupQueue.push({ x, y, - w, - h, - angle: 0 - angle - } as Element<'group'>); + width, + height, + angle: 0 - angle, + } as StrictMaterial<'group'>); }); - let resizeStart: PointSize = originalStart; - let resizeEnd: PointSize = end; + let resizeStart: Point = originalStart; + let resizeEnd: Point = end; if (groupQueue.length > 0) { resizeStart = rotatePointInGroup(originalStart, pointGroupQueue); resizeEnd = rotatePointInGroup(end, pointGroupQueue); } if (resizeType === 'resize-rotate') { - const controller: ElementSizeController = sharer.getSharedStorage( - keySelectedElementController - ) as ElementSizeController; - const viewVertexes: ViewRectVertexes = [ - controller.topLeft.center, - controller.topRight.center, - controller.bottomLeft.center, - controller.bottomRight.center - ]; + const moveOriginalStartMaterialSize = sharer.getSharedStorage( + keyMoveOriginalStartMaterialSize + ) as MaterialSize; - const viewCenter: PointSize = calcElementCenterFromVertexes(viewVertexes); - const resizedElemSize = rotateElement(moveOriginalStartElementSize, { - center: viewCenter, + const virtualItem = calculator.getVirtualItem(mtrls?.[0]?.id as string); + const worldCenter = virtualItem?.worldCenter as Point; + + const resizedMtrlSize = rotateMaterial(moveOriginalStartMaterialSize, { + center: worldCenter, viewScaleInfo, viewSizeInfo, start: originalStart, end, resizeType, - sharer + sharer, + calculator, }); - elems[0].angle = calculator.toGridNum(resizedElemSize.angle || 0); + mtrls[0].angle = calculator.toGridNum(resizedMtrlSize.angle || 0); } else { - const resizedElemSize = resizeElement( - { ...moveOriginalStartElementSize, operations: elems[0].operations }, - { scale, start: resizeStart, end: resizeEnd, resizeType, sharer } + const moveOriginalStartMaterialSize = sharer.getSharedStorage( + keyMoveOriginalStartMaterialSize + ) as MaterialSize; + const resizedMtrlSize = resizeMaterial( + { ...moveOriginalStartMaterialSize, operations: mtrls[0].operations }, + { scale, start: resizeStart, end: resizeEnd, resizeType, sharer, calculator } ); - const calcOpts = { ignore: !!moveOriginalStartElementSize.angle }; - const gridX = calculator.toGridNum(resizedElemSize.x, calcOpts); - const gridY = calculator.toGridNum(resizedElemSize.y, calcOpts); - const gridW = calculator.toGridNum(resizedElemSize.w, calcOpts); - const gridH = calculator.toGridNum(resizedElemSize.h, calcOpts); - if (elems[0].type === 'group') { - endResizeGroupRecord = resizeEffectGroupElement( - elems[0] as Element<'group'>, - { - x: gridX, - y: gridY, - w: gridW, - h: gridH - }, - { resizeEffect: elems[0].operations?.resizeEffect } + const calcOpts = { ignore: !!moveOriginalStartMaterialSize.angle }; + const gridX = calculator.toGridNum(resizedMtrlSize.x, calcOpts); + const gridY = calculator.toGridNum(resizedMtrlSize.y, calcOpts); + const gridW = calculator.toGridNum(resizedMtrlSize.width, calcOpts); + const gridH = calculator.toGridNum(resizedMtrlSize.height, calcOpts); + if (mtrls[0].type === 'group') { + sharer.setSharedStorage( + keyEndResizeGroupRecord, + resizeEffectGroupMaterial( + mtrls[0] as StrictMaterial<'group'>, + { + x: gridX, + y: gridY, + width: gridW, + height: gridH, + }, + { resizeEffect: mtrls[0].operations?.resizeEffect } + ) ); - if (!startResizeGroupRecord) { - startResizeGroupRecord = endResizeGroupRecord; + if (!sharer.getSharedStorage(keyStartResizeGroupRecord)) { + sharer.setSharedStorage(keyStartResizeGroupRecord, sharer.getSharedStorage(keyEndResizeGroupRecord)); } - elems[0].x = gridX; - elems[0].y = gridY; + mtrls[0].x = gridX; + mtrls[0].y = gridY; } else { - elems[0].x = gridX; - elems[0].y = gridY; - elems[0].w = gridW; - elems[0].h = gridH; + dragAndResizeMaterial(mtrls[0], { + x: gridX, + y: gridY, + width: gridW, + height: gridH, + }); } } - updateSelectedElementList([elems[0]]); - calculator.modifyVirtualFlatItemMap(data, { + updateSelectedMaterialList([mtrls[0]]); + modifyType = 'updateMaterial'; + calculator.modifyVirtualItemMap(data, { modifyInfo: { - type: 'updateElement', + type: modifyType, content: { - element: elems[0], - position: sharer.getSharedStorage(keySelectedElementPosition) || [] - } + material: mtrls[0], + position: sharer.getSharedStorage(keySelectedMaterialPosition) || [], + }, }, viewSizeInfo, - viewScaleInfo + viewScaleInfo, }); viewer.drawFrame(); } } else if (actionType === 'area') { - inBusyMode = 'area'; + sharer.setSharedStorage(keyInBusyMode, 'area'); sharer.setSharedStorage(keyAreaEnd, e.point); viewer.drawFrame(); } - prevPoint = e.point; + + const selectedMaterials = sharer.getSharedStorage(keySelectedMaterialList); + triggerChangeEvent( + eventHub, + { + data, + type: 'updatingMaterial', + selectedMaterials, + hoverMaterial: null, + modifyRecord: null, + }, + 'continuous' + ); + + sharer.setSharedStorage(keyPrevPoint, e.point); }, pointEnd(e: PointWatcherEvent) { - inBusyMode = null; + if (!isPointInMiddlewareElement(e.nativeEvent, { $root, rootClassName })) { + return; + } + sharer.setSharedStorage(keyInBusyMode, null); sharer.setSharedStorage(keyIsMoving, false); const data = sharer.getActiveStorage('data'); - const selectedElements = sharer.getSharedStorage(keySelectedElementList); - const hoverElement = sharer.getSharedStorage(keyHoverElement); + const selectedMaterials = sharer.getSharedStorage(keySelectedMaterialList); + const hoverMaterial = sharer.getSharedStorage(keyHoverMaterial); const resizeType = sharer.getSharedStorage(keyResizeType); const actionType = sharer.getSharedStorage(keyActionType); const viewSizeInfo = sharer.getActiveViewSizeInfo(); let needDrawFrame = false; - prevPoint = null; - moveOriginalStartPoint = null; - moveOriginalStartElementSize = null; + sharer.setSharedStorage(keyPrevPoint, null); + sharer.setSharedStorage(keyMoveOriginalStartPoint, null); + sharer.setSharedStorage(keyMoveOriginalStartMaterialSize, null); if (actionType === 'drag') { eventHub.trigger(MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE, { show: true }); @@ -750,22 +735,27 @@ export const MiddlewareSelector: Middleware< sharer.setSharedStorage(keyResizeType, null); needDrawFrame = true; } else if (actionType === 'area') { - sharer.setSharedStorage(keyActionType, null); + if (hoverMaterial?.operations?.locked) { + sharer.setSharedStorage(keyActionType, 'hover'); + } else { + sharer.setSharedStorage(keyActionType, null); + } + if (data) { const start = sharer.getSharedStorage(keyAreaStart); const end = sharer.getSharedStorage(keyAreaEnd); if (start && end) { - const { elements } = getSelectedListArea(data, { + const { materials } = getSelectedListArea(data, { start, end, calculator, viewScaleInfo: sharer.getActiveViewScaleInfo(), - viewSizeInfo: sharer.getActiveViewSizeInfo() + viewSizeInfo: sharer.getActiveViewSizeInfo(), }); - if (elements.length > 0) { + if (materials.length > 0) { sharer.setSharedStorage(keyActionType, 'drag-list'); - updateSelectedElementList(elements, { triggerEvent: true }); + updateSelectedMaterialList(materials, { triggerEvent: true }); needDrawFrame = true; } } @@ -774,18 +764,19 @@ export const MiddlewareSelector: Middleware< sharer.setSharedStorage(keyActionType, 'drag-list-end'); needDrawFrame = true; } else if (data) { - const result = calculator.getPointElement(e.point, { + const result = calculator.getPointMaterial(e.point, { data, viewScaleInfo: sharer.getActiveViewScaleInfo(), - viewSizeInfo: sharer.getActiveViewSizeInfo() + viewSizeInfo: sharer.getActiveViewSizeInfo(), }); - if (result.element) { + if (result.material) { sharer.setSharedStorage(keyActionType, 'select'); needDrawFrame = true; } else { sharer.setSharedStorage(keyActionType, null); } } + if (sharer.getSharedStorage(keyActionType) === null) { clear(); needDrawFrame = true; @@ -795,62 +786,65 @@ export const MiddlewareSelector: Middleware< if (!needDrawFrame) { return; } - if (data && Array.isArray(data?.elements) && (['drag', 'drag-list'] as ActionType[]).includes(actionType)) { - const viewInfo = calcElementsViewInfo(data.elements, viewSizeInfo, { extend: true }); + if (data && Array.isArray(data?.materials) && (['drag', 'drag-list'] as ActionType[]).includes(actionType)) { + const viewInfo = calcMaterialsViewInfo(data.materials, viewSizeInfo, { extend: true }); sharer.setActiveStorage('contextHeight', viewInfo.contextSize.contextHeight); sharer.setActiveStorage('contextWidth', viewInfo.contextSize.contextWidth); } if (data && (['drag', 'drag-list', 'drag-list-end', 'resize'] as ActionType[]).includes(actionType)) { - let type: any = 'resizeElement'; + let type: any = 'resizeMaterial'; if (type === 'resize') { - type = 'resizeElement'; + type = 'resizeMaterial'; } - if (hasChangedData) { + if (sharer.getSharedStorage(keyHasChangedData)) { let modifyRecord: ModifyRecord | null | undefined = null; - if (Array.isArray(pointStartElementSizeList) && pointStartElementSizeList.length) { - const startSize = pointStartElementSizeList[0] as ElementSize & { uuid: string }; + const pointStartMaterialSizeList = sharer.getSharedStorage(keyPointStartMaterialSizeList); + if (Array.isArray(pointStartMaterialSizeList) && pointStartMaterialSizeList.length) { + const startSize = pointStartMaterialSizeList[0] as MaterialSize & { id: string }; - if (selectedElements.length === 1) { + if (selectedMaterials.length === 1) { modifyRecord = { - type: 'resizeElement', + type: 'resizeMaterial', time: 0, content: { - method: 'modifyElement', - uuid: startSize.uuid, - before: toFlattenElement(startSize), - after: toFlattenElement(getElementSize(selectedElements[0])) - } + method: 'modifyMaterial', + id: startSize.id, + before: toFlattenMaterial(startSize), + after: toFlattenMaterial(getMaterialSize(selectedMaterials[0])), + }, }; - if (selectedElements[0].type === 'group' && startResizeGroupRecord && endResizeGroupRecord) { + const startResizeGroupRecord = sharer.getSharedStorage(keyStartResizeGroupRecord); + const endResizeGroupRecord = sharer.getSharedStorage(keyEndResizeGroupRecord); + if (selectedMaterials[0].type === 'group' && startResizeGroupRecord && endResizeGroupRecord) { modifyRecord = { ...endResizeGroupRecord, content: { ...endResizeGroupRecord.content, - before: startResizeGroupRecord.content.before - } + before: startResizeGroupRecord.content.before, + }, }; } - } else if (selectedElements.length > 1) { + } else if (selectedMaterials.length > 1) { modifyRecord = { - type: 'resizeElements', + type: 'resizeMaterials', time: 0, content: { - method: 'modifyElements', - before: pointStartElementSizeList.map((item) => ({ - ...toFlattenElement(item), - uuid: item.uuid + method: 'modifyMaterials', + before: pointStartMaterialSizeList.map((item) => ({ + ...toFlattenMaterial(item), + id: item.id, })), - after: selectedElements.map((item) => ({ - ...toFlattenElement(getElementSize(item)), - uuid: item.uuid - })) - } + after: selectedMaterials.map((item) => ({ + ...toFlattenMaterial(getMaterialSize(item)), + id: item.id, + })), + }, }; } } - eventHub.trigger(coreEventKeys.CHANGE, { data, type, selectedElements, hoverElement, modifyRecord }); - hasChangedData = false; + triggerChangeEvent(eventHub, { data, type, selectedMaterials, hoverMaterial, modifyRecord }); + sharer.setSharedStorage(keyHasChangedData, false); } } viewer.drawFrame(); @@ -860,76 +854,71 @@ export const MiddlewareSelector: Middleware< }, pointLeave() { - inBusyMode = null; + sharer.setSharedStorage(keyInBusyMode, null); sharer.setSharedStorage(keyResizeType, null); eventHub.trigger(coreEventKeys.CURSOR, { - type: 'default' + type: 'default', }); }, doubleClick(e: PointWatcherEvent) { - if (sharer.getSharedStorage(keyEnableSelectInGroup) === false) { + if (!isPointInMiddlewareElement(e.nativeEvent, { $root, rootClassName })) { + return; + } + const enableSelectInGroup = sharer.getSharedStorage(keyEnableSelectInGroup); + if (enableSelectInGroup === false) { return; } - const target = getPointTarget(e.point, pointTargetBaseOptions()); - sharer.setSharedStorage(keySelectedElementController, null); - sharer.setSharedStorage(keySelectedElementList, []); + const target = getPointTarget(e.point, pointTargetBaseOptions(e)); + sharer.setSharedStorage(keySelectedMaterialList, []); - if (target.elements.length === 1 && target.elements[0]?.operations?.locked === true) { + if ( + target.materials.length !== 1 || + target.materials[0]?.operations?.locked === true || + target.materials[0]?.operations?.invisible === true + ) { return; } - if (target.elements.length === 1 && target.elements[0]?.type === 'group') { - const pushResult = pushGroupQueue(target.elements[0] as Element<'group'>); + const mtrl = target.materials[0]; + + innerConfig?.afterDoubleClickMaterial?.({ material: mtrl }); + + if (mtrl?.type === 'group') { + const pushResult = pushGroupQueue(mtrl as StrictMaterial<'group'>); if (pushResult === true) { sharer.setSharedStorage(keyActionType, null); viewer.drawFrame(); return; } - } else if ( - target.elements.length === 1 && - target.elements[0]?.type === 'text' && - !target.elements[0]?.operations?.invisible - ) { + } else if (mtrl?.type === 'text') { eventHub.trigger(coreEventKeys.TEXT_EDIT, { - element: target.elements[0] as Element<'text'>, - groupQueue: sharer.getSharedStorage(keyGroupQueue) || [], - position: getElementPositionFromList( - target.elements[0]?.uuid, - sharer.getActiveStorage('data')?.elements || [] - ), - viewScaleInfo: sharer.getActiveViewScaleInfo() + id: mtrl.id, }); } + // else if (mtrl?.type === 'path') { + // eventHub.trigger(coreEventKeys.PATH_EDIT, { + // id: mtrl.id, + // }); + // } sharer.setSharedStorage(keyActionType, null); }, - wheel() { - updateSelectedElemenetController(); - }, - wheelScale() { - updateSelectedElemenetController(); - }, - contextMenu: (e: PointWatcherEvent) => { + if (!isPointInMiddlewareElement(e.nativeEvent, { $root, rootClassName })) { + return; + } const groupQueue = sharer.getSharedStorage(keyGroupQueue); if (groupQueue?.length > 0) { - if ( - isPointInViewActiveGroup(e.point, { - ctx: overlayContext, - viewScaleInfo: sharer.getActiveViewScaleInfo(), - viewSizeInfo: sharer.getActiveViewSizeInfo(), - groupQueue - }) - ) { - const target = getPointTarget(e.point, pointTargetBaseOptions()); - if (target?.elements?.length === 1 && target.elements[0]?.operations?.locked !== true) { + if (isPointInActiveGroup(e.nativeEvent, { $root, groupQueue })) { + const target = getPointTarget(e.point, pointTargetBaseOptions(e)); + if (target?.materials?.length === 1 && target.materials[0]?.operations?.locked !== true) { clear(); - updateSelectedElementList([target.elements[0]], { triggerEvent: true }); + updateSelectedMaterialList([target.materials[0]], { triggerEvent: true }); viewer.drawFrame(); - } else if (!target?.elements?.length) { + } else if (!target?.materials?.length) { clear(); } } @@ -938,223 +927,36 @@ export const MiddlewareSelector: Middleware< } // not in group - const listAreaSize = calcSelectedElementsArea(getActiveElements(), { + const listAreaSize = calcSelectedMaterialsArea(sharer.getSharedStorage(keySelectedMaterialList), { viewScaleInfo: sharer.getActiveViewScaleInfo(), viewSizeInfo: sharer.getActiveViewSizeInfo(), - calculator + calculator, }); const target = getPointTarget(e.point, { - ...pointTargetBaseOptions(), + ...pointTargetBaseOptions(e), areaSize: listAreaSize, - groupQueue: [] + groupQueue: [], }); - if (target?.elements?.length === 1 && target.elements[0]?.operations?.locked !== true) { + if (target?.materials?.length === 1 && target.materials[0]?.operations?.locked !== true) { clear(); - updateSelectedElementList([target.elements[0]], { triggerEvent: true }); + updateSelectedMaterialList([target.materials[0]], { triggerEvent: true }); viewer.drawFrame(); return; - } else if (!target?.elements?.length) { + } else if (!target?.materials?.length) { clear(); } }, beforeDrawFrame({ snapshot }) { - const { activeColor, activeAreaColor, lockedColor, referenceColor } = innerConfig; - const style = { activeColor, activeAreaColor, lockedColor, referenceColor }; - - const { activeStore, sharedStore } = snapshot; - const { - scale, - offsetLeft, - offsetTop, - offsetRight, - offsetBottom, - width, - height, - contextHeight, - contextWidth, - devicePixelRatio - } = activeStore; - if (rotateControllerPattern.fill !== activeColor) { - rotateControllerPattern = createRotateControllerPattern({ - fill: innerConfig.activeColor, - devicePixelRatio - }); - } - - const sharer = opts.sharer; - const viewScaleInfo = { scale, offsetLeft, offsetTop, offsetRight, offsetBottom }; - const viewSizeInfo = { width, height, contextHeight, contextWidth, devicePixelRatio }; - const selectedElements = sharedStore[keySelectedElementList]; - const elem = selectedElements[0]; - const hoverElement: Element = sharedStore[keyHoverElement] as Element; - const hoverElementVertexes: ViewRectVertexes | null = sharedStore[keyHoverElementVertexes]; - const actionType: ActionType = sharedStore[keyActionType] as ActionType; - const areaStart: Point | null = sharedStore[keyAreaStart]; - const areaEnd: Point | null = sharedStore[keyAreaEnd]; - const groupQueue: Element<'group'>[] = sharedStore[keyGroupQueue]; - const groupQueueVertexesList: ViewRectVertexes[] = sharedStore[keyGroupQueueVertexesList]; - const isMoving = sharedStore[keyIsMoving]; - const enableSnapToGrid = sharedStore[keyEnableSnapToGrid]; - - const drawBaseOpts = { calculator, viewScaleInfo, viewSizeInfo, style }; - - let selectedElementController = sharedStore[keySelectedElementController]; - if (selectedElementController && selectedElements.length === 1 && elem) { - if (!isSameElementSize(elem, selectedElementController.originalElementSize)) { - selectedElementController = calcElementSizeController(elem, { - groupQueue: groupQueue || [], - controllerSize, - viewScaleInfo, - rotateControllerPosition, - rotateControllerSize - }); - sharer.setSharedStorage(keySelectedElementController, selectedElementController); - } - } - - const isHoverLocked: boolean = !!hoverElement?.operations?.locked; - - if (groupQueue?.length > 0) { - // in group - drawGroupQueueVertexesWrappers(overlayContext, groupQueueVertexesList, drawBaseOpts); - if (hoverElement && actionType !== 'drag') { - if (isHoverLocked) { - drawLockedVertexesWrapper(overlayContext, hoverElementVertexes, { - ...drawBaseOpts, - controller: selectedElementController, - style - }); - } else { - drawHoverVertexesWrapper(overlayContext, hoverElementVertexes, drawBaseOpts); - } - } - if (elem && (['select', 'drag', 'resize'] as ActionType[]).includes(actionType)) { - drawSelectedElementControllersVertexes(overlayContext, selectedElementController, { - ...drawBaseOpts, - element: elem, - calculator, - hideControllers: !!isMoving && actionType === 'drag', - rotateControllerPattern: rotateControllerPattern.context2d, - style - }); - if (actionType === 'drag') { - if (enableSnapToGrid === true) { - const referenceInfo = calcReferenceInfo(elem.uuid, { - 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, - style - }); - } - } - } - } - } - } else { - // in root - if (hoverElement && actionType !== 'drag') { - if (isHoverLocked) { - drawLockedVertexesWrapper(overlayContext, hoverElementVertexes, { - ...drawBaseOpts, - controller: selectedElementController, - style - }); - } else { - drawHoverVertexesWrapper(overlayContext, hoverElementVertexes, drawBaseOpts); - } - } - if (elem && (['select', 'drag', 'resize'] as ActionType[]).includes(actionType)) { - drawSelectedElementControllersVertexes(overlayContext, selectedElementController, { - ...drawBaseOpts, - element: elem, - calculator, - hideControllers: !!isMoving && actionType === 'drag', - rotateControllerPattern: rotateControllerPattern.context2d, - style - }); - if (actionType === 'drag') { - if (enableSnapToGrid === true) { - const referenceInfo = calcReferenceInfo(elem.uuid, { - 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, - style - }); - } - } - } - } - } 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 = calcSelectedElementsArea(getActiveElements(), { - viewScaleInfo: sharer.getActiveViewScaleInfo(), - viewSizeInfo: sharer.getActiveViewSizeInfo(), - calculator - }); - if (listAreaSize) { - drawListArea(overlayContext, { areaSize: listAreaSize, style }); - } - } - } - - // // TODO: debug - // drawDebugStoreSelectedElementController(overlayContext, sharer.getSharedStorage(keySelectedElementController), { - // viewScaleInfo, - // viewSizeInfo - // }); - - // // TODO mock data - // const elemCenter: any = sharer.getSharedStorage(keyDebugElemCenter); - // const startVertical = sharer.getSharedStorage(keyDebugStartVertical); - // const endVertical: any = sharer.getSharedStorage(keyDebugEndVertical); - // const startHorizontal = sharer.getSharedStorage(keyDebugStartHorizontal); - // const endHorizontal: any = sharer.getSharedStorage(keyDebugEndHorizontal); - // const end0: any = sharer.getSharedStorage(keyDebugEnd0); - // if (elemCenter && end0) { - // overlayContext.beginPath(); - // overlayContext.moveTo(elemCenter.x, elemCenter.y); - // overlayContext.lineTo(end0.x, end0.y); - // overlayContext.closePath(); - // overlayContext.strokeStyle = 'black'; - // overlayContext.stroke(); - // } - // if (elemCenter && endVertical) { - // overlayContext.beginPath(); - // overlayContext.moveTo(elemCenter.x, elemCenter.y); - // overlayContext.lineTo(endVertical.x, endVertical.y); - // overlayContext.closePath(); - // overlayContext.strokeStyle = 'red'; - // overlayContext.stroke(); - // } - // if (elemCenter && endHorizontal) { - // overlayContext.beginPath(); - // overlayContext.moveTo(elemCenter.x, elemCenter.y); - // overlayContext.lineTo(endHorizontal.x, endHorizontal.y); - // overlayContext.closePath(); - // overlayContext.strokeStyle = 'blue'; - // overlayContext.stroke(); - // } - } + renderFrame({ + $root, + styles, + boardContent, + snapshot, + calculator, + sharer: opts.sharer, + }); + }, }; }; diff --git a/packages/core/src/middlewares/selector/pattern/icon-rotate.ts b/packages/core/src/middlewares/selector/pattern/icon-rotate.ts deleted file mode 100644 index c698aec..0000000 --- a/packages/core/src/middlewares/selector/pattern/icon-rotate.ts +++ /dev/null @@ -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; -}; diff --git a/packages/core/src/middlewares/selector/pattern/index.ts b/packages/core/src/middlewares/selector/pattern/index.ts deleted file mode 100644 index 739b1cb..0000000 --- a/packages/core/src/middlewares/selector/pattern/index.ts +++ /dev/null @@ -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 }; -} diff --git a/packages/core/src/middlewares/selector/reference.ts b/packages/core/src/middlewares/selector/reference.ts index f68ab99..6b32291 100644 --- a/packages/core/src/middlewares/selector/reference.ts +++ b/packages/core/src/middlewares/selector/reference.ts @@ -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; @@ -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 = []; + const yLines: Array = []; 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 = []; + const xLines: Array = []; 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, }; } diff --git a/packages/core/src/middlewares/selector/render-frame.ts b/packages/core/src/middlewares/selector/render-frame.ts new file mode 100644 index 0000000..ceb1fa2 --- /dev/null +++ b/packages/core/src/middlewares/selector/render-frame.ts @@ -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, + 'sharer' | 'calculator' | 'boardContent' +> & { + snapshot: BoardViewerFrameSnapshot; + $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 }); + } + } + } +} diff --git a/packages/core/src/middlewares/selector/resize.ts b/packages/core/src/middlewares/selector/resize.ts new file mode 100644 index 0000000..c1c1237 --- /dev/null +++ b/packages/core/src/middlewares/selector/resize.ts @@ -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); + } +}; diff --git a/packages/core/src/middlewares/selector/static.ts b/packages/core/src/middlewares/selector/static.ts new file mode 100644 index 0000000..2af5789 --- /dev/null +++ b/packages/core/src/middlewares/selector/static.ts @@ -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 & { 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> | [] +export const keySelectedMaterialListVertexes = Symbol(`${key}_selectedMaterialListVertexes`); // ViewRectVertexes | null +export const keySelectedMaterialPosition = Symbol(`${key}_selectedMaterialPosition`); // MaterialPosition | [] +export const keyGroupQueue = Symbol(`${key}_groupQueue`); // Array> | [] +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 +) => ` + +`; + +export const clearStorage = (sharer: StoreSharer) => { + 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); +}; diff --git a/packages/core/src/middlewares/selector/styles.ts b/packages/core/src/middlewares/selector/styles.ts new file mode 100644 index 0000000..7913f1b --- /dev/null +++ b/packages/core/src/middlewares/selector/styles.ts @@ -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(config: C): S { + const styles: S = getMiddlewareValidStyles(config, [ + 'zIndex', + 'activeColor', + 'handlerBorderColor', + 'handlerBackground', + 'handlerHoverBackground', + 'handlerActiveBackground', + 'selectionAreaBackground', + 'selectionAreaBorderColor', + 'lockedColor', + 'referenceColor', + ]); + return styles; +} diff --git a/packages/core/src/middlewares/selector/types.ts b/packages/core/src/middlewares/selector/types.ts index 5785f5f..f915e8c 100644 --- a/packages/core/src/middlewares/selector/types.ts +++ b/packages/core/src/middlewares/selector/types.ts @@ -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; +export type SelectedMaterialSizeController = Record; 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[]; - groupQueue: Element<'group'>[]; - elementVertexesList: ViewRectVertexes[]; - groupQueueVertexesList: ViewRectVertexes[]; + materials: StrictMaterial[]; + 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 & { 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 | null; - [keyHoverElementVertexes]: ViewRectVertexes | null; - [keySelectedElementList]: Array>; - [keySelectedElementListVertexes]: ViewRectVertexes | null; - [keySelectedElementController]: ElementSizeController | null; - [keySelectedElementPosition]: ElementPosition; + [keyGroupQueue]: StrictMaterial<'group'>[]; + [keyHoverMaterial]: StrictMaterial | null; + [keySelectedMaterialList]: Array>; + [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; diff --git a/packages/core/src/middlewares/selector/util.ts b/packages/core/src/middlewares/selector/util.ts index 43ed18c..2ee76a9 100644 --- a/packages/core/src/middlewares/selector/util.ts +++ b/packages/core/src/middlewares/selector/util.ts @@ -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[]; + selectedMaterials?: StrictMaterial[]; 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[] } { +): { indexes: number[]; ids: string[]; materials: StrictMaterial[] } { const indexes: number[] = []; - const uuids: string[] = []; - const elements: Element[] = []; + const ids: string[] = []; + const materials: StrictMaterial[] = []; 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[], +export function calcSelectedMaterialsArea( + materials: StrictMaterial[], 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, 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, 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; } } diff --git a/packages/core/src/middlewares/text-editor/dom.ts b/packages/core/src/middlewares/text-editor/dom.ts new file mode 100644 index 0000000..3b82bf5 --- /dev/null +++ b/packages/core/src/middlewares/text-editor/dom.ts @@ -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); +}; diff --git a/packages/core/src/middlewares/text-editor/index.ts b/packages/core/src/middlewares/text-editor/index.ts index b823156..5c29540 100644 --- a/packages/core/src/middlewares/text-editor/index.ts +++ b/packages/core/src/middlewares/text-editor/index.ts @@ -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 & - Record; - -const defaultElementDetail = getDefaultElementDetailConfig(); - -export const MiddlewareTextEditor: Middleware = (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 { - 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 { + 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, }; }; diff --git a/packages/core/src/middlewares/text-editor/static.ts b/packages/core/src/middlewares/text-editor/static.ts new file mode 100644 index 0000000..1ff1fd0 --- /dev/null +++ b/packages/core/src/middlewares/text-editor/static.ts @@ -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( + config: C +): S { + const styles: S = getMiddlewareValidStyles(config, ['zIndex', 'boxBorderColor']); + return styles; +} diff --git a/packages/core/src/middlewares/text-editor/types.ts b/packages/core/src/middlewares/text-editor/types.ts new file mode 100644 index 0000000..bf15dc9 --- /dev/null +++ b/packages/core/src/middlewares/text-editor/types.ts @@ -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 & + Record; diff --git a/packages/core/src/record.ts b/packages/core/src/record.ts index 2fa6b46..e2e7769 100644 --- a/packages/core/src/record.ts +++ b/packages/core/src/record.ts @@ -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> & Pick; - 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> & Pick; + 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; diff --git a/packages/core/src/config.ts b/packages/core/src/static.ts similarity index 61% rename from packages/core/src/config.ts rename to packages/core/src/static.ts index d220792..24313d1 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/static.ts @@ -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, }); }); diff --git a/packages/figma/package.json b/packages/figma/package.json index 0eb97dd..c62cb27 100644 --- a/packages/figma/package.json +++ b/packages/figma/package.json @@ -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", diff --git a/packages/idraw/__tests__/data.ts b/packages/idraw/__tests__/data.ts deleted file mode 100644 index 1b89e82..0000000 --- a/packages/idraw/__tests__/data.ts +++ /dev/null @@ -1,61 +0,0 @@ -const data = { - elements: [ - { - name: 'rect-001', - x: 10, - y: 10, - w: 200, - h: 100, - type: 'rect', - detail: { - color: '#f0f0f0', - borderRadius: 20, - borderWidth: 10, - borderColor: '#bd0b64' - } - }, - { - name: 'text-002', - x: 80, - y: 80, - w: 200, - h: 120, - // angle: 30, - type: 'text', - detail: { - fontSize: 20, - text: 'Hello Text', - color: '#666666', - borderRadius: 60, - borderWidth: 2, - borderColor: '#bd0b64' - } - }, - { - name: 'image-003', - x: 160, - y: 160, - w: 200, - h: 100, - type: 'image', - detail: { - src: `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOgAAACqCAYAAACnOQMfAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAgAElEQVR4nOy9ebylV1Xn/V17P88Z71xzhYTMZDYkAdQAioDyiiAoDbS0/dqtttptD0LLIIIi2ioBbGca7XZ4G7UVpG1FoZHRhCGQkHmokKQqNd95PtOz93r/2Pt5zrlVt5K6t+6tITm/zyepe8895zz7PGevvdb6rQn66KOPPvroo48++uijjz766KOPPvroo48++uijjz766KOPPvroo48++uijjz766KOPPvroo48++uijjz766KOPPvroo48++uijjz766KOPPvroo48++uijjz766KOPPvroo48++uijjz766KOPPvroo48++uijjz766KOPPvroo48++uijjz76OItwVHVgyfvdZ3odffTRRw9UVaZarddPdTqPzbaz2fl29rY9e/aUz/S6+ujjGY+ZVuuGqVb2uaOtjh9vZX6i43Sy43Smkz3Q8P57zvT6jsV82794upN9ZaLjPjDn/ZYzvZ4++tgULHi/Y7LZ+dB4q9OZ6Did6DidaHk/2XY60fF6tO39eDPzs233141G4+Izvd5l758913Z/Ptlsu4lW5sdb3k+0OpMz7exnnvC+eqbX93SGnOkFPJOgqumsc/8u8/rzImaL96rGiAB4FRVBBEW9qogRAKN+0VhuGTLmFmNM43Su13tfXcz8WzrKW50wgAKY/K8AGOGbieFdw9b+pYjo6VzfMwF9AT0NUFWZzrLv8cj78VxV3PW44fNdHUUVjQ8I4MUAnlR4rIx5cy2Rv91sQVBVWc70tW11t2TKRYoNi1G/YunSI6xW+UpStm8dErl1M9f2TENfQDcZc95fnnl/i3P+VV3tE/4tNrt4utrJh3+jsIrmAuwRVZLE/n3V8JaKMQ9vxnoXvL+209EPZOjLwGDwKArYKJTdhWvUouEhA95pauzHJeHnho3Zsxnre6ahL6CbBO/90Kxz73TO/3s1pgICao57ngBrUYeCx0DTJOY3Roz8qogsbsR6Z1VHXbv9HlX5N86YktEeLSng1YEITpW9E3DeiKVaBtEopBoOFw9Y1ZZY8+E0Me8dEpnciPU9U9EX0A2Gqtp5xw9nLvsvDtlZaBwF5XgBXTOitg3vq/tTI289Ff9PVZMF5/51x/n3Osw2MIh4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` - } - }, - { - name: 'svg-004', - x: 400 - 10, - y: 300 - 100, - w: 200, - h: 200, - type: 'svg', - detail: { - svg: '' - } - } - ] -}; - -export function getData() { - return data; -} diff --git a/packages/idraw/__tests__/history-addElement.test.ts b/packages/idraw/__tests__/history-addElement.test.ts deleted file mode 100644 index 33085e6..0000000 --- a/packages/idraw/__tests__/history-addElement.test.ts +++ /dev/null @@ -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([]); - }); -}); diff --git a/packages/idraw/__tests__/history-addMaterial.test.ts b/packages/idraw/__tests__/history-addMaterial.test.ts new file mode 100644 index 0000000..035e36b --- /dev/null +++ b/packages/idraw/__tests__/history-addMaterial.test.ts @@ -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([]); + }); +}); diff --git a/packages/idraw/__tests__/history-deleteElement.test.ts b/packages/idraw/__tests__/history-deleteElement.test.ts deleted file mode 100644 index 60d5b55..0000000 --- a/packages/idraw/__tests__/history-deleteElement.test.ts +++ /dev/null @@ -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([]); - }); -}); diff --git a/packages/idraw/__tests__/history-deleteMaterial.test.ts b/packages/idraw/__tests__/history-deleteMaterial.test.ts new file mode 100644 index 0000000..8a06575 --- /dev/null +++ b/packages/idraw/__tests__/history-deleteMaterial.test.ts @@ -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([]); + }); +}); diff --git a/packages/idraw/__tests__/history-modifyGlobal.test.ts b/packages/idraw/__tests__/history-modifyGlobal.test.ts index 300ea40..683fc07 100644 --- a/packages/idraw/__tests__/history-modifyGlobal.test.ts +++ b/packages/idraw/__tests__/history-modifyGlobal.test.ts @@ -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 | null = null; + const beforeInfo1: Record | 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; idraw.modifyGlobal({ ...modifiedInfo2 }); const expectedData2 = deepClone(expectedData1); const flattenModifiedInfo2 = toFlattenGlobal(modifiedInfo2); - const beforeInfo2: Record = {}; + const beforeInfo2: Record = {}; 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]); diff --git a/packages/idraw/__tests__/history-modifyLayout.test.ts b/packages/idraw/__tests__/history-modifyLayout.test.ts index eed9735..be91018 100644 --- a/packages/idraw/__tests__/history-modifyLayout.test.ts +++ b/packages/idraw/__tests__/history-modifyLayout.test.ts @@ -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 | null = null; + const beforeInfo1: Record | 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; idraw.modifyLayout({ ...modifiedInfo2 }); const expectedData2 = deepClone(expectedData1); const flattenModifiedInfo2 = toFlattenLayout(modifiedInfo2); - const beforeInfo2: Record = {}; + const beforeInfo2: Record = {}; 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]); diff --git a/packages/idraw/__tests__/history-modifyElement.test.ts b/packages/idraw/__tests__/history-modifyMaterial.test.ts similarity index 51% rename from packages/idraw/__tests__/history-modifyElement.test.ts rename to packages/idraw/__tests__/history-modifyMaterial.test.ts index 23ce658..990dbf8 100644 --- a/packages/idraw/__tests__/history-modifyElement.test.ts +++ b/packages/idraw/__tests__/history-modifyMaterial.test.ts @@ -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 = {}; + const flattenModifiedInfo1 = toFlattenMaterial(modifiedInfo1); + const beforeInfo1: Record = {}; 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>; + cornerRadius: [2, 4, 6, 8], + } as unknown as RecursivePartial>; - idraw.modifyElement({ - uuid: targetElement.uuid, - ...deepClone(modifiedInfo2) - } as RecursivePartial> & Pick); + idraw.modifyMaterial({ + id: targetMaterial.id, + ...deepClone(modifiedInfo2), + } as RecursivePartial> & Pick); const expectedData2 = deepClone(expectedData1); - const flattenModifiedInfo2 = toFlattenElement(modifiedInfo2); - const beforeInfo2: Record = {}; + const flattenModifiedInfo2 = toFlattenMaterial(modifiedInfo2); + const beforeInfo2: Record = {}; 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]); diff --git a/packages/idraw/__tests__/history-modifyMaterials.test.ts b/packages/idraw/__tests__/history-modifyMaterials.test.ts new file mode 100644 index 0000000..8d885e2 --- /dev/null +++ b/packages/idraw/__tests__/history-modifyMaterials.test.ts @@ -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 = {}; + 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>; + + idraw.modifyMaterial({ + id: targetMaterial.id, + ...deepClone(modifiedInfo2), + } as RecursivePartial> & Pick); + + const expectedData2 = deepClone(expectedData1); + const flattenModifiedInfo2 = toFlattenMaterial(modifiedInfo2); + const beforeInfo2: Record = {}; + 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([]); + }); +}); diff --git a/packages/idraw/__tests__/history-moveElement.test.ts b/packages/idraw/__tests__/history-moveMaterial.test.ts similarity index 61% rename from packages/idraw/__tests__/history-moveElement.test.ts rename to packages/idraw/__tests__/history-moveMaterial.test.ts index d61222e..f88b993 100644 --- a/packages/idraw/__tests__/history-moveElement.test.ts +++ b/packages/idraw/__tests__/history-moveMaterial.test.ts @@ -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]); }); diff --git a/packages/idraw/__tests__/history-updateElement.test.ts b/packages/idraw/__tests__/history-updateElement.test.ts deleted file mode 100644 index 7d07de1..0000000 --- a/packages/idraw/__tests__/history-updateElement.test.ts +++ /dev/null @@ -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 = toFlattenElement(targetElement); - const afterInfo1: Record = 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 = toFlattenElement(updatedElement1); - const afterInfo2: Record = 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([]); - }); -}); diff --git a/packages/idraw/__tests__/history-updateMaterial.test.ts b/packages/idraw/__tests__/history-updateMaterial.test.ts new file mode 100644 index 0000000..39d98cb --- /dev/null +++ b/packages/idraw/__tests__/history-updateMaterial.test.ts @@ -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 = toFlattenMaterial(targetMaterial); + const afterInfo1: Record = 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 = toFlattenMaterial(updatedMaterial1); + const afterInfo2: Record = 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([]); + }); +}); diff --git a/packages/idraw/__tests__/history.test.ts b/packages/idraw/__tests__/history.test.ts index 23ce658..1914958 100644 --- a/packages/idraw/__tests__/history.test.ts +++ b/packages/idraw/__tests__/history.test.ts @@ -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 = {}; 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>; + cornerRadius: [2, 4, 6, 8], + } as unknown as RecursivePartial>; - idraw.modifyElement({ - uuid: targetElement.uuid, - ...deepClone(modifiedInfo2) - } as RecursivePartial> & Pick); + idraw.modifyMaterial({ + id: targetMaterial.id, + ...deepClone(modifiedInfo2), + } as RecursivePartial> & Pick); const expectedData2 = deepClone(expectedData1); - const flattenModifiedInfo2 = toFlattenElement(modifiedInfo2); + const flattenModifiedInfo2 = toFlattenMaterial(modifiedInfo2); const beforeInfo2: Record = {}; 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]); diff --git a/packages/idraw/package.json b/packages/idraw/package.json index 7ba3d5c..f1d726c 100644 --- a/packages/idraw/package.json +++ b/packages/idraw/package.json @@ -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", diff --git a/packages/idraw/src/idraw.ts b/packages/idraw/src/idraw.ts index 30c96b1..3ce8b8f 100644 --- a/packages/idraw/src/idraw.ts +++ b/packages/idraw/src/idraw.ts @@ -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; #opts: IDrawOptions; #store: Store = new Store({ - 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(mode: IDrawMode, e?: IDrawModeEventMap[T]) { const core = this.#core; const store = this.#store; - changeMode(mode || defaultMode, core, store); + changeMode(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( + createMaterial( type: T, - element: RecursivePartial>, + material: RecursivePartial, 'id' | 'type'>>, opts?: { viewCenter?: boolean } - ): Element { - return createElement({ core: this.#core }, type, element, opts); + ): StrictMaterial { + return createMaterial({ 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> & Pick) { - return modifyElement({ core: this.#core }, element); + modifyMaterial(material: RecursivePartial> & Pick) { + 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 | null) { @@ -206,7 +218,7 @@ export class iDraw { } async getImageBlobURL(opts?: ExportImageFileBaseOptions): Promise { - 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; } diff --git a/packages/idraw/src/index.ts b/packages/idraw/src/index.ts index f52d97a..2a120a1 100644 --- a/packages/idraw/src/index.ts +++ b/packages/idraw/src/index.ts @@ -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'; diff --git a/packages/idraw/src/methods/element.ts b/packages/idraw/src/methods/element.ts deleted file mode 100644 index c06bcc0..0000000 --- a/packages/idraw/src/methods/element.ts +++ /dev/null @@ -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( - depOptions: { - core: Core; - }, - type: T, - element: RecursivePartial>, - opts?: { - viewCenter?: boolean; - } -): Element { - const { core } = depOptions; - return core.createElement(type, element, opts); -} - -export function updateElement( - depOptions: { - core: Core; - }, - 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; - }, - element: RecursivePartial> & Pick -) { - 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; - }, - 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 }, 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 }, 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 }); -} diff --git a/packages/idraw/src/methods/feature.ts b/packages/idraw/src/methods/feature.ts index ca5d727..2730f75 100644 --- a/packages/idraw/src/methods/feature.ts +++ b/packages/idraw/src/methods/feature.ts @@ -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, }); } } diff --git a/packages/idraw/src/methods/image.ts b/packages/idraw/src/methods/image.ts index 95a6ec3..fcd7752 100644 --- a/packages/idraw/src/methods/image.ts +++ b/packages/idraw/src/methods/image.ts @@ -15,15 +15,15 @@ export async function getImageBlobURL( const outputSize = calcVisiableViewSize(data); return await exportImageFileBlobURL({ - width: outputSize.w, - height: outputSize.h, + width: outputSize.width, + height: outputSize.height, devicePixelRatio, data, viewScaleInfo: { scale: 1, offsetLeft: -outputSize.x, offsetTop: -outputSize.y, offsetBottom: 0, offsetRight: 0 }, viewSizeInfo: { ...viewSizeInfo, - ...{ devicePixelRatio } + ...{ devicePixelRatio }, }, - loadItemMap: core.getLoadItemMap() + loadItemMap: core.getLoadItemMap(), }); } diff --git a/packages/idraw/src/methods/material.ts b/packages/idraw/src/methods/material.ts new file mode 100644 index 0000000..8d00135 --- /dev/null +++ b/packages/idraw/src/methods/material.ts @@ -0,0 +1,85 @@ +import type { Data, StrictMaterial, MaterialType, MaterialPosition, RecursivePartial } from '@idraw/types'; +import { Core, coreEventKeys } from '@idraw/core'; +import { IDrawEvent } from '../event'; + +export function createMaterial( + depOptions: { + core: Core; + }, + type: T, + material: RecursivePartial>, + opts?: { + viewCenter?: boolean; + } +): StrictMaterial { + const { core } = depOptions; + return core.createMaterial(type, material, opts); +} + +export function updateMaterial( + depOptions: { + core: Core; + }, + material: StrictMaterial +) { + const { core } = depOptions; + + const modifyRecord = core.updateMaterial(material); + if (!modifyRecord) { + return; + } + const data = core.getData(); + if (!data) { + return; + } + core.trigger(coreEventKeys.CHANGE, { data, type: 'updateMaterial', modifyRecord }); +} + +export function modifyMaterial( + depOptions: { + core: Core; + }, + material: RecursivePartial> & Pick +) { + const { core } = depOptions; + const modifyRecord = core.modifyMaterial(material); + if (!modifyRecord) { + return; + } + const data = core.getData(); + if (!data) { + return; + } + core.trigger(coreEventKeys.CHANGE, { data, type: 'modifyMaterial', modifyRecord }); +} + +export function addMaterial( + depOptions: { + core: Core; + }, + material: StrictMaterial, + opts?: { + position: MaterialPosition; + } +): Data { + const { core } = depOptions; + const modifyRecord = core.addMaterial(material, opts); + const data = core.getData() as Data; + core.trigger(coreEventKeys.CHANGE, { data, type: 'addMaterial', modifyRecord }); + return data; +} + +export function deleteMaterial(depOptions: { core: Core }, id: string) { + const { core } = depOptions; + const modifyRecord = core.deleteMaterial(id); + const data = core.getData() as Data; + core.trigger(coreEventKeys.CHANGE, { data, type: 'deleteMaterial', modifyRecord }); + core.trigger(coreEventKeys.CLEAR_SELECT, {}); +} + +export function moveMaterial(depOptions: { core: Core }, id: string, to: MaterialPosition) { + const { core } = depOptions; + const modifyRecord = core.moveMaterial(id, to); + const data = core.getData() as Data; + core.trigger(coreEventKeys.CHANGE, { data, type: 'moveMaterial', modifyRecord }); +} diff --git a/packages/idraw/src/methods/reset.ts b/packages/idraw/src/methods/reset.ts index 8230d74..d39ab55 100644 --- a/packages/idraw/src/methods/reset.ts +++ b/packages/idraw/src/methods/reset.ts @@ -16,10 +16,11 @@ export function reset( const newOpts: IDrawSettings = {}; store.clear(); if (mode) { - changeMode(mode, core, store); + changeMode(mode, undefined, core, store); newOpts.mode = mode; needFresh = true; } + if (styles) { changeStyles(styles, core, store); newOpts.styles = styles; diff --git a/packages/idraw/src/middlewares/use-history.ts b/packages/idraw/src/middlewares/use-history.ts index 35f8119..b933cb9 100644 --- a/packages/idraw/src/middlewares/use-history.ts +++ b/packages/idraw/src/middlewares/use-history.ts @@ -1,11 +1,11 @@ import type { Middleware, ModifyRecord, - Element, + Material, DataLayout, RecursivePartial, DataGlobal, - HistoryHandler + HistoryHandler, } from '@idraw/types'; import { unflatObject, calcResultMovePosition } from '@idraw/util'; import { Core } from '@idraw/core'; @@ -13,16 +13,16 @@ import type { IDrawEvent } from '../event'; import { eventKeys } from '../event'; const supportRecordTypes = [ - 'updateElement', - 'modifyElement', - 'deleteElement', - 'moveElement', - 'addElement', - 'resizeElement', - 'resizeElements', + 'updateMaterial', + 'modifyMaterial', + 'deleteMaterial', + 'moveMaterial', + 'addMaterial', + 'resizeMaterial', + 'resizeMaterials', 'resizeLayout', 'modifyLayout', - 'modifyGlobal' + 'modifyGlobal', ]; const LIMIT = 100; @@ -45,37 +45,37 @@ export const useHistory = (opts: { core: Core; limit?: number }) => { return; } let undoRecord: ModifyRecord = { ...record }; - if (record.content.method === 'modifyElement') { + if (record.content.method === 'modifyMaterial') { const info = unflatObject(record.content.before || {}); - undoRecord = core.modifyElement({ + undoRecord = core.modifyMaterial({ ...info, - uuid: (record as ModifyRecord<'modifyElement'>).content?.uuid + id: (record as ModifyRecord<'modifyMaterial'>).content?.id, }) as ModifyRecord; - } else if (record.content.method === 'updateElement') { - const info = unflatObject(record.content.before || {}) as Element; - undoRecord = core.updateElement({ ...info, uuid: record.content.uuid }) as ModifyRecord; - } else if (record.content.method === 'addElement') { - const uuid = record.content.uuid; - undoRecord = core.deleteElement(uuid) as ModifyRecord; - } else if (record.content.method === 'deleteElement') { - const { element, position } = record.content; - if (!element) { + } else if (record.content.method === 'updateMaterial') { + const info = unflatObject(record.content.before || {}) as Material; + undoRecord = core.updateMaterial({ ...info, id: record.content.id }) as ModifyRecord; + } else if (record.content.method === 'addMaterial') { + const id = record.content.id; + undoRecord = core.deleteMaterial(id) as ModifyRecord; + } else if (record.content.method === 'deleteMaterial') { + const { material, position } = record.content; + if (!material) { return; } - if (!element) { + if (!material) { return; } - undoRecord = core.addElement(element, { position }) as ModifyRecord; - } else if (record.content.method === 'moveElement') { - const uuid = record.content.uuid; + undoRecord = core.addMaterial(material, { position }) as ModifyRecord; + } else if (record.content.method === 'moveMaterial') { + const id = record.content.id; const moveResult = calcResultMovePosition({ from: record.content.from, - to: record.content.to + to: record.content.to, }); if (!moveResult) { return; } - undoRecord = core.moveElement(uuid, moveResult.from) as ModifyRecord; + undoRecord = core.moveMaterial(id, moveResult.from) as ModifyRecord; } else if (record.content.method === 'modifyLayout') { const info = record.content.before === null @@ -88,10 +88,10 @@ export const useHistory = (opts: { core: Core; limit?: number }) => { ? null : (unflatObject(record.content.before || {}) as RecursivePartial); undoRecord = core.modifyGlobal(info) as ModifyRecord; - } else if (record.content.method === 'modifyElements') { - undoRecord = core.modifyElements( + } else if (record.content.method === 'modifyMaterials') { + undoRecord = core.modifyMaterials( record.content.before.map((item) => unflatObject(item)) as unknown as Array< - RecursivePartial> & Pick + RecursivePartial> & Pick > ) as ModifyRecord; } @@ -111,34 +111,34 @@ export const useHistory = (opts: { core: Core; limit?: number }) => { return; } let redoRecord: ModifyRecord = { ...record }; - if (record.content.method === 'modifyElement') { + if (record.content.method === 'modifyMaterial') { const info = unflatObject(record.content.before || {}); - redoRecord = core.modifyElement({ + redoRecord = core.modifyMaterial({ ...info, - uuid: (record as ModifyRecord<'modifyElement'>).content.uuid + id: (record as ModifyRecord<'modifyMaterial'>).content.id, }) as ModifyRecord; - } else if (record.content.method === 'updateElement') { - const info = unflatObject(record.content.before || {}) as Element; - redoRecord = core.updateElement({ ...info, uuid: record.content.uuid }) as ModifyRecord; - } else if (record.content.method === 'addElement') { - const uuid = record.content.uuid; - redoRecord = core.deleteElement(uuid) as ModifyRecord; - } else if (record.content.method === 'deleteElement') { - const { element, position } = record.content; - if (!element) { + } else if (record.content.method === 'updateMaterial') { + const info = unflatObject(record.content.before || {}) as Material; + redoRecord = core.updateMaterial({ ...info, id: record.content.id }) as ModifyRecord; + } else if (record.content.method === 'addMaterial') { + const id = record.content.id; + redoRecord = core.deleteMaterial(id) as ModifyRecord; + } else if (record.content.method === 'deleteMaterial') { + const { material, position } = record.content; + if (!material) { return; } - redoRecord = core.addElement(element, { position }) as ModifyRecord; - } else if (record.content.method === 'moveElement') { - const uuid = record.content.uuid; + redoRecord = core.addMaterial(material, { position }) as ModifyRecord; + } else if (record.content.method === 'moveMaterial') { + const id = record.content.id; const moveResult = calcResultMovePosition({ from: record.content.from, - to: record.content.to + to: record.content.to, }); if (!moveResult) { return; } - redoRecord = core.moveElement(uuid, moveResult.from) as ModifyRecord; + redoRecord = core.moveMaterial(id, moveResult.from) as ModifyRecord; } else if (record.content.method === 'modifyLayout') { const info = record.content.before === null @@ -151,10 +151,10 @@ export const useHistory = (opts: { core: Core; limit?: number }) => { ? null : (unflatObject(record.content.before || {}) as RecursivePartial); redoRecord = core.modifyGlobal(info) as ModifyRecord; - } else if (record.content.method === 'modifyElements') { - redoRecord = core.modifyElements( + } else if (record.content.method === 'modifyMaterials') { + redoRecord = core.modifyMaterials( record.content.before.map((item) => unflatObject(item)) as unknown as Array< - RecursivePartial> & Pick + RecursivePartial> & Pick > ) as ModifyRecord; } @@ -190,7 +190,7 @@ export const useHistory = (opts: { core: Core; limit?: number }) => { }, disuse() { offEvents(); - } + }, }; }; @@ -222,11 +222,11 @@ export const useHistory = (opts: { core: Core; limit?: number }) => { canUndo: () => doRecords.length > 0, canRedo: () => undoRecords.length > 0, __getDoRecords: getDoRecords, - __getUndoRecords: getUndoRecords + __getUndoRecords: getUndoRecords, }; return { MiddlewareHistory, - historyHandler + historyHandler, } as const; }; diff --git a/packages/idraw/src/setting/config.ts b/packages/idraw/src/setting/config.ts index dd1fa15..de09ffb 100644 --- a/packages/idraw/src/setting/config.ts +++ b/packages/idraw/src/setting/config.ts @@ -1,34 +1,54 @@ import type { IDrawSettings, IDrawOptions, IDrawStorage, IDrawMode } from '@idraw/types'; import { istype } from '@idraw/util'; +import { + getMiddlewareCreatorStyles, + getMiddlewareSelectorStyles, + getMiddlewareScrollerStyles, + getMiddlewareRulerStyles, + getMiddlewareTextEditorStyles, + getMiddlewareInfoStyles, + getMiddlewarePathEditorStyles, + getMiddlewarePathCreatorStyles, +} from '@idraw/core'; export const defaultMode: IDrawMode = 'select'; export const defaultSettings: Required> = { mode: defaultMode, - history: false + history: false, }; export const defaultOptions: Required> = { - devicePixelRatio: window.devicePixelRatio + devicePixelRatio: window.devicePixelRatio, }; export function getDefaultStorage(): IDrawStorage { const storage: IDrawStorage = { mode: defaultMode, - enableRuler: false, - enableScale: false, - enableScroll: false, + enableCreate: false, + enablePathCreate: false, + enablePathEdit: false, enableSelect: false, + enableSelectLayout: false, enableTextEdit: false, enableDrag: false, - enableInfo: false, + + enableRuler: true, + enableScroll: true, + enableInfo: true, + enableScale: true, + middlewareStyles: { selector: {}, info: {}, ruler: {}, scroller: {}, - layoutSelector: {} - } + layoutSelector: {}, + creator: {}, + textEditor: {}, + pathCreator: {}, + pathEditor: {}, + }, }; return storage; } @@ -77,76 +97,42 @@ export function parseStyles(settings: IDrawSettings): Required, store: Store) { - const { enableRuler, enableScale, enableScroll, enableSelect, enableTextEdit, enableDrag, enableInfo } = - store.getSnapshot(); +export function runMiddlewares( + e: IDrawModeEventMap[T] | undefined | null, + core: Core, + store: Store +) { + const { + enableCreate, + enableRuler, + enableScale, + enableScroll, + enableSelect, + enableSelectLayout, + enableTextEdit, + enableDrag, + enableInfo, + enablePathCreate, + enablePathEdit, + } = store.getSnapshot(); + const styles = store.get('middlewareStyles'); if (enableScroll === true) { core.use(MiddlewareScroller, styles?.scroller); - } else if (enableScroll === false) { + } else { core.disuse(MiddlewareScroller); } + if (enableCreate === true) { + core.use(MiddlewareCreator, { + ...styles?.creator, + afterCreated: () => { + changeMode('select', undefined, core, store); + }, + }); + core.trigger(eventKeys.CREATE, e as IDrawModeEventMap['create']); + } else { + core.disuse(MiddlewareCreator); + } + + if (enableTextEdit === true) { + core.use(MiddlewareTextEditor, styles.textEditor); + } else { + core.disuse(MiddlewareTextEditor); + } + + if (enablePathEdit === true) { + core.use>(MiddlewarePathEditor, { + ...styles.pathEditor, + afterClickAway: () => { + changeMode('select', undefined, core, store); + }, + }); + // core.trigger(eventKeys.PATH_EDIT, undefined); + } else { + core.disuse(MiddlewarePathEditor /* TODO: style */); + // core.trigger(eventKeys.CLEAR_PATH_EDIT, undefined); + } + if (enableSelect === true) { - core.use(MiddlewareLayoutSelector, styles?.layoutSelector); - core.use(MiddlewareSelector, styles?.selector); - } else if (enableSelect === false) { - core.disuse(MiddlewareLayoutSelector); + core.use>(MiddlewareSelector, { + ...styles?.selector, + afterDoubleClickMaterial: ({ material }) => { + if (material?.type === 'path') { + changeMode('edit-path', undefined, core, store); + core.trigger(eventKeys.PATH_EDIT, { + id: material.id, + }); + } + }, + }); + } else { core.disuse(MiddlewareSelector); } + if (enableSelectLayout === true) { + core.use(MiddlewareLayoutSelector, styles?.layoutSelector); + } else { + core.disuse(MiddlewareLayoutSelector); + } + if (enableScale === true) { core.use(MiddlewareScaler); - } else if (enableScale === false) { + } else { core.disuse(MiddlewareScaler); } if (enableRuler === true) { core.use(MiddlewareRuler, styles?.ruler); - } else if (enableRuler === false) { + } else { core.disuse(MiddlewareRuler); } - if (enableTextEdit === true) { - core.use(MiddlewareTextEditor); - } else if (enableTextEdit === false) { - core.disuse(MiddlewareTextEditor); - } - if (enableDrag === true) { core.use(MiddlewareDragger); - } else if (enableDrag === false) { + } else { core.disuse(MiddlewareDragger); } if (enableInfo === true) { core.use(MiddlewareInfo, styles?.info); - } else if (enableInfo === false) { + } else { core.disuse(MiddlewareInfo); } + if (enablePathCreate === true) { + core.use(MiddlewarePathCreator, styles.pathCreator); + core.trigger(eventKeys.PATH_CREATE, undefined); + } else { + core.trigger(eventKeys.CLEAR_PATH_CREATE, undefined); + core.disuse(MiddlewarePathCreator /* TODO: style */); + } + core.use(MiddlewarePointer); } -export function changeMode(mode: IDrawMode, core: Core, store: Store) { - let enableScale: boolean = false; - let enableScroll: boolean = false; +export function changeMode( + mode: IDrawMode, + e: IDrawModeEventMap[T] | undefined, + core: Core, + store: Store +) { + let enableCreate: boolean = false; let enableSelect: boolean = false; + let enableSelectLayout: boolean = false; let enableTextEdit: boolean = false; let enableDrag: boolean = false; - let enableRuler: boolean = false; - let enableInfo: boolean = false; + let enablePathCreate: boolean = false; + let enablePathEdit: boolean = false; + + const enableRuler: boolean = store.get('enableRuler'); + const enableScroll: boolean = store.get('enableScroll'); + const enableInfo: boolean = store.get('enableInfo'); + const enableScale: boolean = store.get('enableScale'); + + let innerMode: IDrawMode = mode || 'select'; - let innerMode: IDrawMode = 'select'; - store.set('mode', innerMode); if (isValidMode(mode)) { innerMode = mode; } else { // eslint-disable-next-line no-console console.warn(`${mode} is invalid mode of iDraw.js`); } + store.set('mode', innerMode); - if (innerMode === 'select') { - enableScale = true; - enableScroll = true; + if (innerMode === 'create') { + enableCreate = true; enableSelect = true; - enableInfo = true; - enableTextEdit = true; + enableSelectLayout = false; + enableTextEdit = false; enableDrag = false; - enableRuler = true; + enablePathCreate = false; + enablePathEdit = true; + + // enableRuler = true; + // enableScroll = true; + // enableInfo = false; + // enableScale = true; } else if (innerMode === 'drag') { - enableScale = true; - enableScroll = true; + enableCreate = false; enableSelect = false; + enableSelectLayout = false; enableTextEdit = false; enableDrag = true; - enableRuler = true; - } else if (innerMode === 'readOnly') { - enableScale = false; - enableScroll = false; + enablePathCreate = false; + enablePathEdit = false; + + // enableRuler = true; + // enableScale = true; + // enableScroll = true; + // enableInfo = false; + } else if (innerMode === 'readonly') { + enableCreate = false; enableSelect = false; + enableSelectLayout = false; enableTextEdit = false; enableDrag = false; - enableRuler = false; + enablePathCreate = false; + enablePathEdit = false; + + // enableRuler = false; + // enableScale = false; + // enableScroll = false; + // enableInfo = false; + } else if (innerMode === 'create-path') { + enableCreate = false; + enableSelect = false; + enableSelectLayout = false; + enableTextEdit = false; + enableDrag = false; + enablePathCreate = true; + enablePathEdit = false; + + // enableScale = true; + // enableScroll = true; + // enableInfo = false; + // enableRuler = true; + } else if (innerMode === 'edit-path') { + enableCreate = false; + enableSelect = false; + enableSelectLayout = false; + enableTextEdit = false; + enableDrag = false; + enablePathCreate = false; + enablePathEdit = true; + + // enableScale = true; + // enableScroll = true; + // enableInfo = false; + // enableRuler = true; + } else if (mode === 'select-layout') { + enableCreate = false; + enableSelect = false; + enableSelectLayout = true; + enableTextEdit = false; + enableDrag = false; + enablePathCreate = false; + enablePathEdit = true; + + // enableScale = true; + // enableScroll = true; + // enableInfo = false; + // enableRuler = true; + } else { + // default is "select" mode + enableCreate = false; + enableSelect = true; + enableSelectLayout = false; + enableTextEdit = true; + enableDrag = false; + enablePathCreate = false; + enablePathEdit = true; + + // enableScale = true; + // enableScroll = true; + // enableInfo = true; + // enableRuler = true; } - store.set('enableScale', enableScale); - store.set('enableScroll', enableScroll); + store.set('enableCreate', enableCreate); + store.set('enableSelect', enableSelect); + store.set('enableSelectLayout', enableSelectLayout); store.set('enableTextEdit', enableTextEdit); store.set('enableDrag', enableDrag); + store.set('enablePathCreate', enablePathCreate); + store.set('enablePathEdit', enablePathEdit); + store.set('enableRuler', enableRuler); store.set('enableInfo', enableInfo); - runMiddlewares(core, store); + store.set('enableScale', enableScale); + store.set('enableScroll', enableScroll); + + runMiddlewares(e, core, store); + core.trigger(eventKeys.MODE_CHANGE, { mode }); } diff --git a/packages/idraw/src/setting/style.ts b/packages/idraw/src/setting/style.ts index 696c42d..ae7ae1b 100644 --- a/packages/idraw/src/setting/style.ts +++ b/packages/idraw/src/setting/style.ts @@ -1,9 +1,20 @@ -import { Core, MiddlewareInfo, MiddlewareLayoutSelector, MiddlewareRuler, MiddlewareScroller, MiddlewareSelector } from '@idraw/core'; +import { + Core, + MiddlewareInfo, + MiddlewareLayoutSelector, + MiddlewareRuler, + MiddlewareScroller, + MiddlewareSelector, +} from '@idraw/core'; import { Store } from '@idraw/util'; import type { IDrawSettings, IDrawStorage } from '@idraw/types'; import type { IDrawEvent } from '../event'; -export function changeStyles(styles: Required['styles'], core: Core, store: Store) { +export function changeStyles( + styles: Required['styles'], + core: Core, + store: Store +) { const { selector, info, ruler, scroller, layoutSelector } = styles; const middlewareStyles = store.get('middlewareStyles'); if (selector) { diff --git a/packages/renderer/package.json b/packages/renderer/package.json index 002a064..74825dc 100644 --- a/packages/renderer/package.json +++ b/packages/renderer/package.json @@ -1,6 +1,6 @@ { "name": "@idraw/renderer", - "version": "0.4.0", + "version": "1.0.0", "description": "", "main": "dist/esm/index.js", "module": "dist/esm/index.js", @@ -21,11 +21,11 @@ "author": "idrawjs", "license": "MIT", "devDependencies": { - "@idraw/types": "workspace:^0.4" + "@idraw/types": "workspace:*" }, "dependencies": {}, "peerDependencies": { - "@idraw/util": "workspace:^0.4" + "@idraw/util": "workspace:*" }, "publishConfig": { "access": "public", diff --git a/packages/renderer/src/calculator.ts b/packages/renderer/src/calculator.ts index 68c4b12..22176a5 100644 --- a/packages/renderer/src/calculator.ts +++ b/packages/renderer/src/calculator.ts @@ -1,31 +1,30 @@ import type { Point, Data, - Element, - ElementType, + Material, + StrictMaterial, + MaterialType, ViewCalculator, ViewCalculatorOptions, ViewScaleInfo, ViewSizeInfo, VirtualFlatStorage, - ViewRectInfo, + BoundingInfo, ModifyInfo, - VirtualFlatItem + VirtualItem, } from '@idraw/types'; import { is, - getViewPointAtElement, + getViewPointAtMaterial, Store, - calcViewPointSize, - findElementFromListByPosition, - getGroupQueueByElementPosition, - calcElementOriginRectInfo, - originRectInfoToRangeRectInfo - // elementToBoxInfo + calcViewPoint, + findMaterialFromListByPosition, + getGroupQueueByMaterialPosition, + calcMaterialBoundingInfo, + boundingInfoToRangeBoundingInfo, } from '@idraw/util'; -import { sortElementsViewVisiableInfoMap, updateVirtualFlatItemMapStatus } from './view-visible'; -import { calcVirtualFlatDetail } from './virtual-flat'; -import { calcVirtualTextDetail } from './virtual-flat/text'; +import { sortMaterialsViewVisiableInfoMap, updateVirtualItemMapStatus } from './visible'; +import { calcVirtualAttributes } from './virtual'; export class Calculator implements ViewCalculator { #opts: ViewCalculatorOptions; @@ -35,10 +34,10 @@ export class Calculator implements ViewCalculator { this.#opts = opts; this.#store = new Store({ defaultStorage: { - virtualFlatItemMap: {}, + virtualItemMap: {}, visibleCount: 0, - invisibleCount: 0 - } + invisibleCount: 0, + }, }); } @@ -55,24 +54,32 @@ export class Calculator implements ViewCalculator { this.#opts = null as any; } - needRender(elem: Element): boolean { - const virtualFlatItemMap = this.#store.get('virtualFlatItemMap'); - const info = virtualFlatItemMap[elem.uuid]; + needRender(mtrl: StrictMaterial): boolean { + const virtualItemMap = this.#store.get('virtualItemMap'); + const info = virtualItemMap[mtrl.id]; if (!info) { return true; } return info.isVisibleInView; } - getPointElement( - p: Point, - opts: { data: Data; viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo } - ): { index: number; element: null | Element; groupQueueIndex: number } { - const context2d = this.#opts.tempContext; - return getViewPointAtElement(p, { ...opts, ...{ context2d } }); + forceVisiable(id: string) { + const virtualItemMap = this.#store.get('virtualItemMap'); + const info = virtualItemMap[id]; + if (info) { + info.isVisibleInView = true; + } } - resetVirtualFlatItemMap( + getPointMaterial( + p: Point, + opts: { data: Data; viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo } + ): { index: number; material: null | StrictMaterial; groupQueueIndex: number } { + const context2d = this.#opts.tempContext; + return getViewPointAtMaterial(p, { ...opts, ...{ context2d } }); + } + + resetVirtualItemMap( data: Data, opts: { viewScaleInfo: ViewScaleInfo; @@ -80,112 +87,130 @@ export class Calculator implements ViewCalculator { } ): void { if (data) { - const { virtualFlatItemMap, invisibleCount, visibleCount } = sortElementsViewVisiableInfoMap(data.elements, { + const { virtualItemMap, invisibleCount, visibleCount } = sortMaterialsViewVisiableInfoMap(data.materials, { ...opts, ...{ - tempContext: this.#opts.tempContext - } + tempContext: this.#opts.tempContext, + }, }); - this.#store.set('virtualFlatItemMap', virtualFlatItemMap); + this.#store.set('virtualItemMap', virtualItemMap); this.#store.set('invisibleCount', invisibleCount); this.#store.set('visibleCount', visibleCount); } } updateVisiableStatus(opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }) { - const { virtualFlatItemMap, invisibleCount, visibleCount } = updateVirtualFlatItemMapStatus( - this.#store.get('virtualFlatItemMap'), + const { virtualItemMap, invisibleCount, visibleCount } = updateVirtualItemMapStatus( + this.#store.get('virtualItemMap'), opts ); - this.#store.set('virtualFlatItemMap', virtualFlatItemMap); + this.#store.set('virtualItemMap', virtualItemMap); this.#store.set('invisibleCount', invisibleCount); this.#store.set('visibleCount', visibleCount); } - calcViewRectInfoFromOrigin( - uuid: string, + calcViewBoundingInfoFromOrigin( + id: string, opts: { checkVisible?: boolean; viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo; } - ): ViewRectInfo | null { - const infoData = this.#store.get('virtualFlatItemMap')[uuid]; - if (!infoData?.originRectInfo) { + ): BoundingInfo | null { + const infoData = this.#store.get('virtualItemMap')[id]; + if (!infoData?.boundingInfo) { return null; } const { checkVisible, viewScaleInfo, viewSizeInfo } = opts; - const { center, left, right, bottom, top, topLeft, topRight, bottomLeft, bottomRight } = infoData.originRectInfo; + const { center, left, right, bottom, top, topLeft, topRight, bottomLeft, bottomRight } = infoData.boundingInfo; if (checkVisible === true && infoData.isVisibleInView === false) { return null; } const calcOpts = { viewScaleInfo, viewSizeInfo }; - const viewRectInfo: ViewRectInfo = { - center: calcViewPointSize(center, calcOpts), - left: calcViewPointSize(left, calcOpts), - right: calcViewPointSize(right, calcOpts), - bottom: calcViewPointSize(bottom, calcOpts), - top: calcViewPointSize(top, calcOpts), - topLeft: calcViewPointSize(topLeft, calcOpts), - topRight: calcViewPointSize(topRight, calcOpts), - bottomLeft: calcViewPointSize(bottomLeft, calcOpts), - bottomRight: calcViewPointSize(bottomRight, calcOpts) + const viewBoundingBox: BoundingInfo = { + center: calcViewPoint(center, calcOpts), + left: calcViewPoint(left, calcOpts), + right: calcViewPoint(right, calcOpts), + bottom: calcViewPoint(bottom, calcOpts), + top: calcViewPoint(top, calcOpts), + topLeft: calcViewPoint(topLeft, calcOpts), + topRight: calcViewPoint(topRight, calcOpts), + bottomLeft: calcViewPoint(bottomLeft, calcOpts), + bottomRight: calcViewPoint(bottomRight, calcOpts), }; - return viewRectInfo; + return viewBoundingBox; } - calcViewRectInfoFromRange( - uuid: string, + calcViewBoundingInfoFromRange( + id: string, opts: { checkVisible?: boolean; viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo; } - ): ViewRectInfo | null { - const infoData = this.#store.get('virtualFlatItemMap')[uuid]; - if (!infoData?.originRectInfo) { + ): BoundingInfo | null { + const infoData = this.#store.get('virtualItemMap')[id]; + if (!infoData?.boundingInfo) { return null; } const { checkVisible, viewScaleInfo, viewSizeInfo } = opts; - const { center, left, right, bottom, top, topLeft, topRight, bottomLeft, bottomRight } = infoData.rangeRectInfo; + const { center, left, right, bottom, top, topLeft, topRight, bottomLeft, bottomRight } = infoData.rangeBoundingInfo; if (checkVisible === true && infoData.isVisibleInView === false) { return null; } const calcOpts = { viewScaleInfo, viewSizeInfo }; - const viewRectInfo: ViewRectInfo = { - center: calcViewPointSize(center, calcOpts), - left: calcViewPointSize(left, calcOpts), - right: calcViewPointSize(right, calcOpts), - bottom: calcViewPointSize(bottom, calcOpts), - top: calcViewPointSize(top, calcOpts), - topLeft: calcViewPointSize(topLeft, calcOpts), - topRight: calcViewPointSize(topRight, calcOpts), - bottomLeft: calcViewPointSize(bottomLeft, calcOpts), - bottomRight: calcViewPointSize(bottomRight, calcOpts) + const info: BoundingInfo = { + center: calcViewPoint(center, calcOpts), + left: calcViewPoint(left, calcOpts), + right: calcViewPoint(right, calcOpts), + bottom: calcViewPoint(bottom, calcOpts), + top: calcViewPoint(top, calcOpts), + topLeft: calcViewPoint(topLeft, calcOpts), + topRight: calcViewPoint(topRight, calcOpts), + bottomLeft: calcViewPoint(bottomLeft, calcOpts), + bottomRight: calcViewPoint(bottomRight, calcOpts), }; - return viewRectInfo; + return info; } - modifyText(element: Element<'text'>): void { - const virtualFlatItemMap = this.#store.get('virtualFlatItemMap'); - const flatItem = virtualFlatItemMap[element.uuid]; - if (element && element.type === 'text') { - const newVirtualFlatItem: VirtualFlatItem = { - ...flatItem, - ...calcVirtualTextDetail(element, { - tempContext: this.#opts.tempContext - }) + modifyVirtualAttributes( + material: StrictMaterial, + opts: { + viewScaleInfo: ViewScaleInfo; + viewSizeInfo: ViewSizeInfo; + groupQueue: StrictMaterial<'group'>[]; + } + ): void { + const { viewSizeInfo, groupQueue } = opts; + const virtualItemMap = this.#store.get('virtualItemMap'); + const vItem = virtualItemMap[material.id]; + // const position = vItem.position; + + const vAttributes = calcVirtualAttributes(material, { + tempContext: this.#opts.tempContext, + dpr: viewSizeInfo.devicePixelRatio, + groupQueue, + }); + if (vAttributes) { + const boundingInfo = calcMaterialBoundingInfo(material, { + groupQueue, + }); + const newVirtualItem: VirtualItem = { + ...vItem, + ...vAttributes, + boundingInfo, + rangeBoundingInfo: is.angle(material.angle) ? boundingInfoToRangeBoundingInfo(boundingInfo) : boundingInfo, }; - virtualFlatItemMap[element.uuid] = newVirtualFlatItem; - this.#store.set('virtualFlatItemMap', virtualFlatItemMap); + virtualItemMap[material.id] = newVirtualItem; + this.#store.set('virtualItemMap', virtualItemMap); } } - modifyVirtualFlatItemMap( + modifyVirtualItemMap( data: Data, opts: { modifyInfo: ModifyInfo; // TODO @@ -195,65 +220,67 @@ export class Calculator implements ViewCalculator { ): void { const { modifyInfo, viewScaleInfo, viewSizeInfo } = opts; const { type, content } = modifyInfo; - const list = data.elements; - const virtualFlatItemMap = this.#store.get('virtualFlatItemMap'); - if (type === 'deleteElement') { - const { element } = content as ModifyInfo<'deleteElement'>['content']; - const uuids: string[] = []; - const _walk = (e: Element) => { - uuids.push(e.uuid); - if (e.type === 'group' && Array.isArray((e as Element<'group'>).detail.children)) { - (e as Element<'group'>).detail.children.forEach((child) => { + const list = data.materials; + const virtualItemMap = this.#store.get('virtualItemMap'); + if (type === 'deleteMaterial') { + const { material } = content as ModifyInfo<'deleteMaterial'>['content']; + const ids: string[] = []; + const _walk = (e: Material) => { + ids.push(e.id); + if (e.type === 'group' && Array.isArray((e as StrictMaterial<'group'>).children)) { + (e as StrictMaterial<'group'>).children.forEach((child) => { _walk(child); }); } }; - _walk(element); - uuids.forEach((uuid) => { - delete virtualFlatItemMap[uuid]; + _walk(material); + ids.forEach((id) => { + delete virtualItemMap[id]; }); - this.#store.set('virtualFlatItemMap', virtualFlatItemMap); + this.#store.set('virtualItemMap', virtualItemMap); } - // else if (type === 'updateElement') { + // else if (type === 'updateMaterial') { // // TODO - // this.resetVirtualFlatItemMap(data, { viewScaleInfo, viewSizeInfo }); + // this.resetVirtualItemMap(data, { viewScaleInfo, viewSizeInfo }); // } - else if (type === 'addElement' || type === 'updateElement') { - const { position } = content as ModifyInfo<'addElement'>['content']; - const element = findElementFromListByPosition(position, data.elements); - const groupQueue = getGroupQueueByElementPosition(list, position); - if (element) { - if (type === 'updateElement' && element.type === 'group') { + else if (type === 'addMaterial' || type === 'updateMaterial') { + const { position } = content as ModifyInfo<'addMaterial'>['content']; + const material = findMaterialFromListByPosition(position, data.materials); + const groupQueue = getGroupQueueByMaterialPosition(list, position); + if (material) { + if (type === 'updateMaterial' && material.type === 'group') { // TODO - this.resetVirtualFlatItemMap(data, { viewScaleInfo, viewSizeInfo }); + this.resetVirtualItemMap(data, { viewScaleInfo, viewSizeInfo }); } else { - const originRectInfo = calcElementOriginRectInfo(element, { - groupQueue: groupQueue || [] + const boundingInfo = calcMaterialBoundingInfo(material, { + groupQueue: groupQueue || [], }); - const newVirtualFlatItem: VirtualFlatItem = { - type: element.type, - originRectInfo, - rangeRectInfo: is.angle(element.angle) ? originRectInfoToRangeRectInfo(originRectInfo) : originRectInfo, + const newVirtualItem: VirtualItem = { + type: material.type, + boundingInfo, + rangeBoundingInfo: is.angle(material.angle) ? boundingInfoToRangeBoundingInfo(boundingInfo) : boundingInfo, isVisibleInView: true, position: [...position], - ...calcVirtualFlatDetail(element, { - tempContext: this.#opts.tempContext - }) + ...calcVirtualAttributes(material, { + tempContext: this.#opts.tempContext, + dpr: viewSizeInfo.devicePixelRatio, + groupQueue: groupQueue || [], + }), }; - virtualFlatItemMap[element.uuid] = newVirtualFlatItem; - this.#store.set('virtualFlatItemMap', virtualFlatItemMap); - if (type === 'updateElement') { + virtualItemMap[material.id] = newVirtualItem; + this.#store.set('virtualItemMap', virtualItemMap); + if (type === 'updateMaterial') { this.updateVisiableStatus({ viewScaleInfo, viewSizeInfo }); } } } - } else if (type === 'moveElement') { - this.resetVirtualFlatItemMap(data, { viewScaleInfo, viewSizeInfo }); + } else if (type === 'moveMaterial') { + this.resetVirtualItemMap(data, { viewScaleInfo, viewSizeInfo }); } } - getVirtualFlatItem(uuid: string): VirtualFlatItem | null { - const itemMap = this.#store.get('virtualFlatItemMap'); - return itemMap[uuid] || null; + getVirtualItem(id: string): VirtualItem | null { + const itemMap = this.#store.get('virtualItemMap'); + return itemMap[id] || null; } } diff --git a/packages/renderer/src/draw/base.ts b/packages/renderer/src/draw/base.ts new file mode 100644 index 0000000..63eaa7b --- /dev/null +++ b/packages/renderer/src/draw/base.ts @@ -0,0 +1,223 @@ +import type { + Point, + Material, + MaterialSize, + RendererDrawMaterialOptions, + ViewContext2D, + VirtualRectAttributes, + DefaultMaterialAttributes, + ViewScaleInfo, + ViewSizeInfo, + ViewCalculator, +} from '@idraw/types'; +import { + scalePathCommands, + shiftPathCommands, + convertPathCommandsToStr, + getDefaultMaterialAttributes, + omit, + calcViewPoint, + calcViewMaterialSize, + rotateByCenter, + is, +} from '@idraw/util'; +import { createColor } from './color'; + +let defaultAttrs: DefaultMaterialAttributes = getDefaultMaterialAttributes(); +defaultAttrs = omit(defaultAttrs, ['fill']) as DefaultMaterialAttributes; + +export function drawBase(ctx: ViewContext2D, mtrl: Material, opts: RendererDrawMaterialOptions) { + const { viewScaleInfo, viewSizeInfo, calculator } = opts; + const { + opacity, + fill, + fillOpacity, + fillRule, + stroke, + strokeWidth, + strokeOpacity, + strokeLinecap, + strokeLinejoin, + strokeDasharray, + strokeDashoffset, + strokeMiterlimit, + } = { ...defaultAttrs, ...mtrl }; + const { scale, offsetLeft, offsetTop } = viewScaleInfo; + const { devicePixelRatio } = viewSizeInfo; + const virtualAttributes = calculator.getVirtualItem(mtrl.id) as VirtualRectAttributes; + const { commands, worldCenter } = virtualAttributes; + const { width, height } = mtrl; + + let cmds = commands; + cmds = scalePathCommands(cmds, scale, scale); + cmds = shiftPathCommands( + cmds, + (offsetLeft + (worldCenter.x - width / 2) * scale) * devicePixelRatio, + (offsetTop + (worldCenter.y - height / 2) * scale) * devicePixelRatio + ); + + const originGlobalAlpha = ctx.globalAlpha; + + // next version + const pathStr = convertPathCommandsToStr(cmds); + const path2d = new Path2D(pathStr); + + // shadow + drawShadow(ctx, mtrl, { ...opts, path2d }); + + // clip (overflow=hidden) + drawClipPath(ctx, mtrl, { + path2d, + viewScaleInfo, + viewSizeInfo, + calculator, + renderContent: () => { + // fill + if (fill) { + const viewMaterialSize = calcViewMaterialSize(mtrl, { viewScaleInfo }); + if (typeof fillOpacity === 'number' && fillOpacity > 0) { + ctx.globalAlpha = originGlobalAlpha * fillOpacity * opacity; + } + ctx.fillStyle = createColor(ctx, fill, { viewMaterialSize, viewScaleInfo, opacity: mtrl.opacity || 1 }); + ctx.fill(path2d, fillRule); + ctx.globalAlpha = originGlobalAlpha; + } + + // stroke + if (typeof strokeWidth === 'number' && strokeWidth > 0) { + if (typeof strokeOpacity === 'number' && strokeOpacity > 0) { + ctx.globalAlpha = originGlobalAlpha * strokeOpacity * opacity; + } + + ctx.lineCap = strokeLinecap; + ctx.lineJoin = strokeLinejoin; + ctx.lineDashOffset = strokeDashoffset; + ctx.miterLimit = strokeMiterlimit; + + if (Array.isArray(strokeDasharray)) { + const lineDash = strokeDasharray.map((dash) => scale * dash); + ctx.setLineDash(lineDash); + } + ctx.lineWidth = strokeWidth * scale; + ctx.strokeStyle = stroke as string; // TODO + ctx.stroke(path2d); + ctx.setLineDash([]); + + // reset + ctx.lineCap = defaultAttrs.strokeLinecap; + ctx.lineJoin = defaultAttrs.strokeLinejoin; + ctx.lineDashOffset = defaultAttrs.strokeDashoffset; + ctx.miterLimit = defaultAttrs.strokeMiterlimit; + ctx.globalAlpha = originGlobalAlpha; + } + }, + }); +} + +export function drawShadow( + ctx: ViewContext2D, + viewMtrl: Material, + opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo; tempContext: ViewContext2D; path2d: Path2D } +): void { + const { ...detail } = viewMtrl; + const { viewScaleInfo, viewSizeInfo, tempContext, path2d } = opts; + const { width, height } = viewSizeInfo; + const { shadowColor, shadowOffsetX, shadowOffsetY, shadowBlur } = detail; + if (is.number(shadowBlur) && shadowColor) { + tempContext.clearRect(0, 0, tempContext.canvas.width, tempContext.canvas.height); + + tempContext.save(); + tempContext.shadowColor = shadowColor; + tempContext.shadowOffsetX = (shadowOffsetX || 0) * viewScaleInfo.scale; + tempContext.shadowOffsetY = (shadowOffsetY || 0) * viewScaleInfo.scale; + tempContext.shadowBlur = (shadowBlur || 0) * viewScaleInfo.scale; + tempContext.fillStyle = '#ffffff'; + tempContext.fill(path2d); + tempContext.restore(); + + tempContext.save(); + tempContext.globalCompositeOperation = 'destination-out'; + tempContext.fillStyle = '#ffffff'; + tempContext.fill(path2d); + tempContext.restore(); + + ctx.drawImage(tempContext.canvas, 0, 0, width, height); + tempContext.clearRect(0, 0, tempContext.canvas.width, tempContext.canvas.height); + } +} + +export function drawClipPath( + ctx: ViewContext2D, + mtrl: Material, + opts: { + path2d?: Path2D; + viewScaleInfo: ViewScaleInfo; + viewSizeInfo: ViewSizeInfo; + renderContent?: () => void; + calculator: ViewCalculator; + } +) { + const { renderContent, calculator, viewScaleInfo, viewSizeInfo } = opts; + if (mtrl.overflow === 'hidden') { + let path2d: Path2D | undefined = opts.path2d; + if (!path2d) { + const { scale, offsetLeft, offsetTop } = viewScaleInfo; + const { devicePixelRatio } = viewSizeInfo; + const virtualAttributes = calculator.getVirtualItem(mtrl.id) as VirtualRectAttributes; + const { commands, worldCenter } = virtualAttributes; + + const { width, height } = mtrl; + + let cmds = commands; + cmds = scalePathCommands(cmds, scale, scale); + cmds = shiftPathCommands( + cmds, + (offsetLeft + (worldCenter.x - width / 2) * scale) * devicePixelRatio, + (offsetTop + (worldCenter.y - height / 2) * scale) * devicePixelRatio + ); + const pathStr = convertPathCommandsToStr(cmds); + path2d = new Path2D(pathStr); + } + + ctx.save(); + ctx.clip(path2d, 'nonzero'); + + // rotateElement(ctx, { ...viewElem }, () => { + // renderContent?.(); + // }); + renderContent?.(); + + ctx.restore(); + } else { + renderContent?.(); + } +} + +export function rotateViewMaterial( + ctx: ViewContext2D, + mtrl: Material, + opts: RendererDrawMaterialOptions, + callback: (e: { viewWorldCenter: Point; viewWorldSize: MaterialSize }) => void +) { + const { viewScaleInfo, calculator } = opts; + const virtualAttributes = calculator?.getVirtualItem(mtrl.id) as VirtualRectAttributes; + if (virtualAttributes) { + const { worldAngle, worldCenter } = virtualAttributes; + const viewWorldCenter = calcViewPoint(worldCenter, { viewScaleInfo }); + const { scale } = viewScaleInfo; + + rotateByCenter(ctx, worldAngle, viewWorldCenter, () => { + const width = mtrl.width * scale; + const height = mtrl.height * scale; + const viewWorldSize: MaterialSize = { + x: viewWorldCenter.x - width / 2, + y: viewWorldCenter.y - height / 2, + width, + height, + }; + callback({ viewWorldCenter, viewWorldSize }); + }); + } else { + // callback({ viewWorldCenter }); + } +} diff --git a/packages/renderer/src/draw/box.ts b/packages/renderer/src/draw/box.ts index 5d4647d..6a82417 100644 --- a/packages/renderer/src/draw/box.ts +++ b/packages/renderer/src/draw/box.ts @@ -1,489 +1,9 @@ -import { - ViewContext2D, - Element, - ElementType, - ElementSize, - ViewScaleInfo, - ViewSizeInfo, - TransformAction - // PointSize -} from '@idraw/types'; -import { - istype, - isColorStr, - generateSVGPath, - rotateElement, - is, - getDefaultElementDetailConfig, - calcViewBoxSize - // elementToBoxInfo, - // calcViewPointSize -} from '@idraw/util'; -import { Calculator } from '../calculator'; -import { createColorStyle } from './color'; +import { StrictMaterial } from '@idraw/types'; -const defaultElemConfig = getDefaultElementDetailConfig(); - -export function getOpacity(elem: Element): number { +export function getOpacity(mtrl: StrictMaterial): number { let opacity = 1; - if (elem?.detail?.opacity !== undefined && elem?.detail?.opacity >= 0 && elem?.detail?.opacity <= 1) { - opacity = elem?.detail?.opacity; + if (mtrl?.opacity !== undefined && mtrl?.opacity >= 0 && mtrl?.opacity <= 1) { + opacity = mtrl?.opacity; } return opacity; } - -export function drawBox( - ctx: ViewContext2D, - viewElem: Element, - opts: { - originElem: Element; - calcElemSize: ElementSize; - pattern?: string | CanvasPattern | null; - renderContent?: () => void; - viewScaleInfo: ViewScaleInfo; - viewSizeInfo: ViewSizeInfo; - parentOpacity: number; - } -): void { - const { pattern, renderContent, originElem, calcElemSize, viewScaleInfo, viewSizeInfo } = opts || {}; - const { parentOpacity } = opts; - const opacity = getOpacity(originElem) * parentOpacity; - - const { clipPath, clipPathStrokeColor, clipPathStrokeWidth } = originElem.detail; - - const mainRender = () => { - ctx.globalAlpha = opacity; - drawBoxBackground(ctx, viewElem, { pattern, viewScaleInfo, viewSizeInfo }); - renderContent?.(); - drawBoxBorder(ctx, viewElem, { originElem, viewScaleInfo, viewSizeInfo }); - ctx.globalAlpha = parentOpacity; - }; - if (clipPath) { - drawClipPath(ctx, viewElem, { - originElem, - calcElemSize, - viewScaleInfo, - viewSizeInfo, - renderContent: () => { - mainRender(); - } - }); - - if (typeof clipPathStrokeWidth === 'number' && clipPathStrokeWidth > 0 && clipPathStrokeColor) { - drawClipPathStroke(ctx, viewElem, { - originElem, - calcElemSize, - viewScaleInfo, - viewSizeInfo, - parentOpacity - }); - } - } else { - mainRender(); - } -} - -function drawClipPath( - ctx: ViewContext2D, - viewElem: Element, - opts: { - originElem?: Element; - calcElemSize?: ElementSize; - renderContent: () => void; - viewScaleInfo: ViewScaleInfo; - viewSizeInfo: ViewSizeInfo; - } -) { - const { renderContent, originElem, calcElemSize, viewSizeInfo } = opts; - const totalScale = viewSizeInfo.devicePixelRatio; - const { clipPath } = originElem?.detail || {}; - if (clipPath && calcElemSize && clipPath.commands) { - const { x, y, w, h } = calcElemSize; - const { originW, originH, originX, originY } = clipPath; - const scaleW = w / originW; - const scaleH = h / originH; - const viewOriginX = originX * scaleW; - const viewOriginY = originY * scaleH; - const internalX = x - viewOriginX; - const internalY = y - viewOriginY; - - ctx.save(); - ctx.translate(internalX as number, internalY as number); - ctx.scale(totalScale * scaleW, totalScale * scaleH); - const pathStr = generateSVGPath(clipPath.commands || []); - const path2d = new Path2D(pathStr); - ctx.clip(path2d, 'nonzero'); - - ctx.translate(0 - (internalX as number), 0 - (internalY as number)); - ctx.setTransform(1, 0, 0, 1, 0, 0); - - rotateElement(ctx, { ...viewElem }, () => { - renderContent?.(); - }); - - ctx.restore(); - } else { - renderContent?.(); - } -} - -function drawClipPathStroke( - ctx: ViewContext2D, - viewElem: Element, - opts: { - originElem?: Element; - calcElemSize?: ElementSize; - renderContent?: () => void; - viewScaleInfo: ViewScaleInfo; - viewSizeInfo: ViewSizeInfo; - parentOpacity: number; - } -) { - const { renderContent, originElem, calcElemSize, viewSizeInfo, parentOpacity } = opts; - const totalScale = viewSizeInfo.devicePixelRatio; - const { clipPath, clipPathStrokeColor, clipPathStrokeWidth } = originElem?.detail || {}; - if ( - clipPath && - calcElemSize && - clipPath.commands && - typeof clipPathStrokeWidth === 'number' && - clipPathStrokeWidth > 0 && - clipPathStrokeColor - ) { - const { x, y, w, h } = calcElemSize; - const { originW, originH, originX, originY } = clipPath; - const scaleW = w / originW; - const scaleH = h / originH; - const viewOriginX = originX * scaleW; - const viewOriginY = originY * scaleH; - const internalX = x - viewOriginX; - const internalY = y - viewOriginY; - - ctx.save(); - ctx.globalAlpha = parentOpacity; - ctx.translate(internalX as number, internalY as number); - ctx.scale(totalScale * scaleW, totalScale * scaleH); - const pathStr = generateSVGPath(clipPath.commands || []); - const path2d = new Path2D(pathStr); - - ctx.strokeStyle = clipPathStrokeColor; - ctx.lineWidth = clipPathStrokeWidth; - ctx.stroke(path2d); - - ctx.translate(0 - (internalX as number), 0 - (internalY as number)); - ctx.setTransform(1, 0, 0, 1, 0, 0); - - rotateElement(ctx, { ...viewElem }, () => { - renderContent?.(); - }); - - ctx.restore(); - } else { - renderContent?.(); - } -} - -export function drawBoxBackground( - ctx: ViewContext2D, - viewElem: Element, - opts: { pattern?: string | CanvasPattern | null; viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo } -): void { - const { pattern, viewScaleInfo, viewSizeInfo } = opts; - const transform: TransformAction[] = []; - if (viewElem.detail.background || pattern) { - const { x, y, w, h, radiusList } = calcViewBoxSize(viewElem, { - viewScaleInfo, - viewSizeInfo - }); - - ctx.beginPath(); - ctx.moveTo(x + radiusList[0], y); - ctx.arcTo(x + w, y, x + w, y + h, radiusList[1]); - ctx.arcTo(x + w, y + h, x, y + h, radiusList[2]); - ctx.arcTo(x, y + h, x, y, radiusList[3]); - ctx.arcTo(x, y, x + w, y, radiusList[0]); - ctx.closePath(); - if (typeof pattern === 'string') { - ctx.fillStyle = pattern; - } else if (['CanvasPattern'].includes(istype.type(pattern))) { - ctx.fillStyle = pattern as CanvasPattern; - } else if (typeof viewElem.detail.background === 'string') { - ctx.fillStyle = viewElem.detail.background; - } else if (viewElem.detail.background?.type === 'linear-gradient') { - const colorStyle = createColorStyle(ctx, viewElem.detail.background, { - viewElementSize: { x, y, w, h }, - viewScaleInfo, - opacity: ctx.globalAlpha - }); - ctx.fillStyle = colorStyle; - } else if (viewElem.detail.background?.type === 'radial-gradient') { - const colorStyle = createColorStyle(ctx, viewElem.detail.background, { - viewElementSize: { x, y, w, h }, - viewScaleInfo, - opacity: ctx.globalAlpha - }); - ctx.fillStyle = colorStyle; - if (transform && transform.length > 0) { - for (let i = 0; i < transform?.length; i++) { - const action = transform[i]; - if (action.method === 'translate') { - ctx.translate(action.args[0] + x, action.args[1] + y); - } else if (action.method === 'rotate') { - ctx.rotate(...action.args); - } else if (action.method === 'scale') { - ctx.scale(...action.args); - } - } - } - } - ctx.fill('nonzero'); - - if (transform && transform.length > 0) { - ctx.setTransform(1, 0, 0, 1, 0, 0); - } - } -} - -export function drawBoxBorder( - ctx: ViewContext2D, - viewElem: Element, - opts: { originElem: Element; viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo; calculator?: Calculator } -): void { - if (viewElem.detail.borderWidth === 0) { - return; - } - if (!isColorStr(viewElem.detail.borderColor)) { - return; - } - const { viewScaleInfo } = opts; - const { scale } = viewScaleInfo; - let borderColor = defaultElemConfig.borderColor; - if (isColorStr(viewElem.detail.borderColor) === true) { - borderColor = viewElem.detail.borderColor as string; - } - - const { borderDash, borderWidth, borderRadius, boxSizing = defaultElemConfig.boxSizing } = viewElem.detail; - let viewBorderDash: number[] = []; - if (Array.isArray(borderDash) && borderDash.length > 0) { - viewBorderDash = borderDash.map((num) => Math.ceil(num * scale)); - } - if (viewBorderDash.length > 0) { - ctx.lineCap = 'butt'; - } else { - ctx.lineCap = 'square'; - } - - let radiusList: [number, number, number, number] = [0, 0, 0, 0]; - if (typeof borderRadius === 'number') { - const br = borderRadius * scale; - radiusList = [br, br, br, br]; - } else if (Array.isArray(borderRadius) && borderRadius?.length === 4) { - radiusList = [borderRadius[0] * scale, borderRadius[1] * scale, borderRadius[2] * scale, borderRadius[3] * scale]; - } - - // // TODO - // const boxInfo = elementToBoxInfo(opts.originElem); - // const calcViewOpts = { viewScaleInfo, viewSizeInfo: opts.viewSizeInfo }; - // const op0: PointSize = calcViewPointSize(boxInfo.op0, calcViewOpts); - // const op1: PointSize = calcViewPointSize(boxInfo.op1, calcViewOpts); - // const op2: PointSize = calcViewPointSize(boxInfo.op2, calcViewOpts); - // const op3: PointSize = calcViewPointSize(boxInfo.op3, calcViewOpts); - // const op0s: PointSize = calcViewPointSize(boxInfo.op0s, calcViewOpts); - // const op0e: PointSize = calcViewPointSize(boxInfo.op0e, calcViewOpts); - // const op1s: PointSize = calcViewPointSize(boxInfo.op1s, calcViewOpts); - // const op1e: PointSize = calcViewPointSize(boxInfo.op1e, calcViewOpts); - // const op2s: PointSize = calcViewPointSize(boxInfo.op2s, calcViewOpts); - // const op2e: PointSize = calcViewPointSize(boxInfo.op2e, calcViewOpts); - // const op3s: PointSize = calcViewPointSize(boxInfo.op3s, calcViewOpts); - // const op3e: PointSize = calcViewPointSize(boxInfo.op3e, calcViewOpts); - - // const ip0: PointSize = calcViewPointSize(boxInfo.ip0, calcViewOpts); - // const ip1: PointSize = calcViewPointSize(boxInfo.ip1, calcViewOpts); - // const ip2: PointSize = calcViewPointSize(boxInfo.ip2, calcViewOpts); - // const ip3: PointSize = calcViewPointSize(boxInfo.ip3, calcViewOpts); - // const ip0s: PointSize = calcViewPointSize(boxInfo.ip0s, calcViewOpts); - // const ip0e: PointSize = calcViewPointSize(boxInfo.ip0e, calcViewOpts); - // const ip1s: PointSize = calcViewPointSize(boxInfo.ip1s, calcViewOpts); - // const ip1e: PointSize = calcViewPointSize(boxInfo.ip1e, calcViewOpts); - // const ip2s: PointSize = calcViewPointSize(boxInfo.ip2s, calcViewOpts); - // const ip2e: PointSize = calcViewPointSize(boxInfo.ip2e, calcViewOpts); - // const ip3s: PointSize = calcViewPointSize(boxInfo.ip3s, calcViewOpts); - // const ip3e: PointSize = calcViewPointSize(boxInfo.ip3e, calcViewOpts); - - // ctx.fillStyle = borderColor; - // ctx.beginPath(); - // ctx.moveTo(op0s.x, op0s.y); - // ctx.quadraticCurveTo(op0.x, op0.y, op0e.x, op0e.y); - // ctx.lineTo(op1s.x, op1s.y); - // ctx.quadraticCurveTo(op1.x, op1.y, op1e.x, op1e.y); - // ctx.lineTo(op2s.x, op2s.y); - // ctx.quadraticCurveTo(op2.x, op2.y, op2e.x, op2e.y); - // ctx.lineTo(op3s.x, op3s.y); - // ctx.quadraticCurveTo(op3.x, op3.y, op3e.x, op3e.y); - // ctx.lineTo(op0s.x, op0s.y); - // ctx.closePath(); - // ctx.fill('nonzero'); - - // ctx.fillStyle = '#000000'; - // ctx.globalCompositeOperation = 'destination-out'; - // ctx.beginPath(); - // ctx.moveTo(ip0s.x, ip0s.y); - // ctx.quadraticCurveTo(ip0.x, ip0.y, ip0e.x, ip0e.y); - // ctx.lineTo(ip1s.x, ip1s.y); - // ctx.quadraticCurveTo(ip1.x, ip1.y, ip1e.x, ip1e.y); - // ctx.lineTo(ip2s.x, ip2s.y); - // ctx.quadraticCurveTo(ip2.x, ip2.y, ip2e.x, ip2e.y); - // ctx.lineTo(ip3s.x, ip3s.y); - // ctx.quadraticCurveTo(ip3.x, ip3.y, ip3e.x, ip3e.y); - // ctx.lineTo(ip0s.x, ip0s.y); - // ctx.closePath(); - // ctx.fill('nonzero'); - // ctx.globalCompositeOperation = 'source-over'; - // return; - // // TODO - - let bw: number = 0; - if (typeof borderWidth === 'number') { - bw = borderWidth || 1; - } - bw = bw * scale; - - ctx.strokeStyle = borderColor; - - let borderTop = 0; - let borderRight = 0; - let borderBottom = 0; - let borderLeft = 0; - if (Array.isArray(borderWidth)) { - borderTop = (borderWidth[0] || 0) * scale; - borderRight = (borderWidth[1] || 0) * scale; - borderBottom = (borderWidth[2] || 0) * scale; - borderLeft = (borderWidth[3] || 0) * scale; - } - - if (borderLeft || borderRight || borderTop || borderBottom) { - ctx.lineCap = 'butt'; - let { x, y, w, h } = viewElem; - if (boxSizing === 'border-box') { - x = x + borderLeft / 2; - y = y + borderTop / 2; - w = w - borderLeft / 2 - borderRight / 2; - h = h - borderTop / 2 - borderBottom / 2; - } else if (boxSizing === 'content-box') { - x = x - borderLeft / 2; - y = y - borderTop / 2; - w = w + borderLeft / 2 + borderRight / 2; - h = h + borderTop / 2 + borderBottom / 2; - } else { - // center-line - x = viewElem.x; - y = viewElem.y; - w = viewElem.w; - h = viewElem.h; - } - - if (borderTop) { - ctx.beginPath(); - ctx.lineWidth = borderTop; - ctx.moveTo(x - borderLeft / 2, y); - ctx.lineTo(x + w + borderRight / 2, y); - ctx.closePath(); - ctx.stroke(); - } - if (borderRight) { - ctx.beginPath(); - ctx.lineWidth = borderRight; - ctx.moveTo(x + w, y - borderTop / 2); - ctx.lineTo(x + w, y + h + borderBottom / 2); - ctx.closePath(); - ctx.stroke(); - } - if (borderBottom) { - ctx.beginPath(); - ctx.lineWidth = borderBottom; - ctx.moveTo(x - borderLeft / 2, y + h); - ctx.lineTo(x + w + borderRight / 2, y + h); - ctx.closePath(); - ctx.stroke(); - } - if (borderLeft) { - ctx.beginPath(); - ctx.lineWidth = borderLeft; - ctx.moveTo(x, y - borderTop / 2); - ctx.lineTo(x, y + h + borderBottom / 2); - ctx.closePath(); - ctx.stroke(); - } - } else { - let { x, y, w, h } = viewElem; - - if (boxSizing === 'border-box') { - x = viewElem.x + bw / 2; - y = viewElem.y + bw / 2; - w = viewElem.w - bw; - h = viewElem.h - bw; - } else if (boxSizing === 'content-box') { - x = viewElem.x - bw / 2; - y = viewElem.y - bw / 2; - w = viewElem.w + bw; - h = viewElem.h + bw; - } else { - // center-line - x = viewElem.x; - y = viewElem.y; - w = viewElem.w; - h = viewElem.h; - } - - // r = Math.min(r, w / 2, h / 2); - // if (r < w / 2 && r < h / 2) { - // r = r + bw / 2; - // } - - // TODO - w = Math.max(w, 1); - h = Math.max(h, 1); - radiusList = radiusList.map((r) => { - return Math.min(r, w / 2, h / 2); - }) as [number, number, number, number]; - - ctx.setLineDash(viewBorderDash); - ctx.lineWidth = bw; - ctx.beginPath(); - ctx.moveTo(x + radiusList[0], y); - ctx.arcTo(x + w, y, x + w, y + h, radiusList[1]); - ctx.arcTo(x + w, y + h, x, y + h, radiusList[2]); - ctx.arcTo(x, y + h, x, y, radiusList[3]); - ctx.arcTo(x, y, x + w, y, radiusList[0]); - ctx.closePath(); - ctx.stroke(); - } - ctx.setLineDash([]); -} - -export function drawBoxShadow( - ctx: ViewContext2D, - viewElem: Element, - opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo; renderContent: () => void } -): void { - const { detail } = viewElem; - const { viewScaleInfo, renderContent } = opts; - const { shadowColor, shadowOffsetX, shadowOffsetY, shadowBlur } = detail; - if (is.number(shadowBlur)) { - ctx.save(); - ctx.shadowColor = shadowColor || defaultElemConfig.shadowColor; - ctx.shadowOffsetX = (shadowOffsetX || 0) * viewScaleInfo.scale; - ctx.shadowOffsetY = (shadowOffsetY || 0) * viewScaleInfo.scale; - ctx.shadowBlur = (shadowBlur || 0) * viewScaleInfo.scale; - renderContent(); - ctx.restore(); - } else { - ctx.save(); - ctx.shadowColor = 'transparent'; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 0; - ctx.shadowBlur = 0; - renderContent(); - ctx.restore(); - } -} diff --git a/packages/renderer/src/draw/circle.ts b/packages/renderer/src/draw/circle.ts index 88df609..3680e33 100644 --- a/packages/renderer/src/draw/circle.ts +++ b/packages/renderer/src/draw/circle.ts @@ -1,85 +1,8 @@ -import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types'; -import { rotateElement, calcViewElementSize } from '@idraw/util'; -import { createColorStyle } from './color'; -import { drawBoxShadow, getOpacity } from './box'; +import type { StrictMaterial, RendererDrawMaterialOptions, ViewContext2D } from '@idraw/types'; +import { drawBase, rotateViewMaterial } from './base'; -export function drawCircle(ctx: ViewContext2D, elem: Element<'circle'>, opts: RendererDrawElementOptions) { - const { detail, angle } = elem; - const { viewScaleInfo, viewSizeInfo, parentOpacity } = opts; - const { background = '#000000', borderColor = '#000000', boxSizing, borderWidth = 0, borderDash } = detail; - let bw: number = 0; - if (typeof borderWidth === 'number' && borderWidth > 0) { - bw = borderWidth as number; - } else if (Array.isArray(borderWidth) && typeof borderWidth[0] === 'number' && borderWidth[0] > 0) { - bw = borderWidth[0] as number; - } - bw = bw * viewScaleInfo.scale; - - // const { scale, offsetTop, offsetBottom, offsetLeft, offsetRight } = viewScaleInfo; - const { x, y, w, h } = calcViewElementSize({ x: elem.x, y: elem.y, w: elem.w, h: elem.h }, { viewScaleInfo }) || elem; - const viewElem = { ...elem, ...{ x, y, w, h, angle } }; - - rotateElement(ctx, { x, y, w, h, angle }, () => { - drawBoxShadow(ctx, viewElem, { - viewScaleInfo, - viewSizeInfo, - renderContent: () => { - let a = w / 2; - let b = h / 2; - // 'content-box' - const centerX = x + a; - const centerY = y + b; - const radiusA = a; - const radiusB = b; - if (bw > 0) { - if (boxSizing === 'content-box') { - // a = a; - // b = b; - } else if (boxSizing === 'center-line') { - a = a - bw / 2; - b = b - bw / 2; - } else { - // 'border-box' - a = a - bw; - b = b - bw; - } - } - - if (a >= 0 && b >= 0) { - const opacity = getOpacity(viewElem) * parentOpacity; - ctx.globalAlpha = opacity; - - // draw content - ctx.beginPath(); - const fillStyle = createColorStyle(ctx, background, { - viewElementSize: { x, y, w, h }, - viewScaleInfo, - opacity: ctx.globalAlpha - }); - ctx.fillStyle = fillStyle; - ctx.circle(centerX, centerY, radiusA, radiusB, 0, 0, 2 * Math.PI); - ctx.closePath(); - ctx.fill('nonzero'); - ctx.globalAlpha = parentOpacity; - - // draw border - if (typeof bw === 'number' && bw > 0) { - const ba = bw / 2 + a; - const bb = bw / 2 + b; - ctx.beginPath(); - if (borderDash) { - const lineDash = borderDash.map((n) => n * viewScaleInfo.scale); - ctx.setLineDash(lineDash); - } - ctx.strokeStyle = borderColor; - ctx.lineWidth = bw; - ctx.circle(centerX, centerY, ba, bb, 0, 0, 2 * Math.PI); - ctx.closePath(); - ctx.stroke(); - ctx.setLineDash([]); - } - } - } - }); +export function drawCircle(ctx: ViewContext2D, mtrl: StrictMaterial<'circle'>, opts: RendererDrawMaterialOptions) { + rotateViewMaterial(ctx, mtrl, opts, () => { + drawBase(ctx, mtrl, opts); }); } diff --git a/packages/renderer/src/draw/color.ts b/packages/renderer/src/draw/color.ts index 43fadff..c50107a 100644 --- a/packages/renderer/src/draw/color.ts +++ b/packages/renderer/src/draw/color.ts @@ -1,11 +1,17 @@ -import type { ViewContext2D, ViewScaleInfo, ElementSize, LinearGradientColor, RadialGradientColor } from '@idraw/types'; +import type { + ViewContext2D, + ViewScaleInfo, + MaterialSize, + LinearGradientColor, + RadialGradientColor, +} from '@idraw/types'; import { mergeHexColorAlpha } from '@idraw/util'; -export function createColorStyle( +export function createColor( ctx: ViewContext2D, color: string | LinearGradientColor | RadialGradientColor | undefined, opts: { - viewElementSize: ElementSize; + viewMaterialSize: MaterialSize; viewScaleInfo: ViewScaleInfo; opacity: number; } @@ -13,23 +19,25 @@ export function createColorStyle( if (typeof color === 'string') { return color; } - const { viewElementSize, viewScaleInfo, opacity = 1 } = opts; - const { x, y } = viewElementSize; + const { viewMaterialSize, viewScaleInfo, opacity = 1 } = opts; + const { x, y } = viewMaterialSize; const { scale } = viewScaleInfo; if (color?.type === 'linear-gradient') { const { start, end, stops } = color; const viewStart = { x: x + start.x * scale, - y: y + start.y * scale + y: y + start.y * scale, }; const viewEnd = { x: x + end.x * scale, - y: y + end.y * scale + y: y + end.y * scale, }; const linearGradient = ctx.createLinearGradient(viewStart.x, viewStart.y, viewEnd.x, viewEnd.y); stops.forEach((stop) => { - linearGradient.addColorStop(stop.offset, mergeHexColorAlpha(stop.color, opacity)); + if (stop.offset >= 0 && stop.color) { + linearGradient.addColorStop(stop.offset, mergeHexColorAlpha(stop.color, opacity)); + } }); return linearGradient; } @@ -39,14 +47,21 @@ export function createColorStyle( const viewInner = { x: x + inner.x * scale, y: y + inner.y * scale, - radius: inner.radius * scale + radius: inner.radius * scale, }; const viewOuter = { x: x + outer.x * scale, y: y + outer.y * scale, - radius: outer.radius * scale + radius: outer.radius * scale, }; - const radialGradient = ctx.createRadialGradient(viewInner.x, viewInner.y, viewInner.radius, viewOuter.x, viewOuter.y, viewOuter.radius); + const radialGradient = ctx.createRadialGradient( + viewInner.x, + viewInner.y, + viewInner.radius, + viewOuter.x, + viewOuter.y, + viewOuter.radius + ); stops.forEach((stop) => { radialGradient.addColorStop(stop.offset, mergeHexColorAlpha(stop.color, opacity)); }); diff --git a/packages/renderer/src/draw/elements.ts b/packages/renderer/src/draw/elements.ts deleted file mode 100644 index 287a849..0000000 --- a/packages/renderer/src/draw/elements.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Data, RendererDrawElementOptions, ViewContext2D } from '@idraw/types'; -import { getDefaultElementDetailConfig } from '@idraw/util'; -import { drawElement } from './group'; - -const defaultDetail = getDefaultElementDetailConfig(); - -export function drawElementList(ctx: ViewContext2D, data: Data, opts: RendererDrawElementOptions) { - const { elements = [] } = data; - const { parentOpacity } = opts; - for (let i = 0; i < elements.length; i++) { - const element = elements[i]; - const elem = { - ...element, - ...{ - detail: { - ...defaultDetail, - ...element?.detail - } - } - }; - if (opts.forceDrawAll !== true) { - if (!opts.calculator?.needRender(elem)) { - continue; - } - } - - try { - drawElement(ctx, elem, { - ...opts, - ...{ - parentOpacity - } - }); - } catch (err) { - console.error(err); - } - } -} diff --git a/packages/renderer/src/draw/ellipse.ts b/packages/renderer/src/draw/ellipse.ts new file mode 100644 index 0000000..b06386f --- /dev/null +++ b/packages/renderer/src/draw/ellipse.ts @@ -0,0 +1,8 @@ +import type { StrictMaterial, RendererDrawMaterialOptions, ViewContext2D } from '@idraw/types'; +import { drawBase, rotateViewMaterial } from './base'; + +export function drawEllipse(ctx: ViewContext2D, mtrl: StrictMaterial<'ellipse'>, opts: RendererDrawMaterialOptions) { + rotateViewMaterial(ctx, mtrl, opts, () => { + drawBase(ctx, mtrl, opts); + }); +} diff --git a/packages/renderer/src/draw/foreign-object.ts b/packages/renderer/src/draw/foreign-object.ts new file mode 100644 index 0000000..fe37144 --- /dev/null +++ b/packages/renderer/src/draw/foreign-object.ts @@ -0,0 +1,21 @@ +import type { StrictMaterial, RendererDrawMaterialOptions, ViewContext2D } from '@idraw/types'; +import { calcViewMaterialSize } from '@idraw/util'; +import { rotateViewMaterial } from './base'; + +export function drawForeignObject( + ctx: ViewContext2D, + mtrl: StrictMaterial<'foreignObject'>, + opts: RendererDrawMaterialOptions +) { + const content = opts.loader.getContent(mtrl); + const { viewScaleInfo } = opts; + const { x, y, width, height } = calcViewMaterialSize(mtrl, { viewScaleInfo }) || mtrl; + rotateViewMaterial(ctx, mtrl, opts, () => { + if (!content && !opts.loader.isDestroyed()) { + opts.loader.load(mtrl as StrictMaterial<'foreignObject'>, opts.materialAssets || {}); + } + if (mtrl.type === 'foreignObject' && content) { + ctx.drawImage(content, x, y, width, height); + } + }); +} diff --git a/packages/renderer/src/draw/global.ts b/packages/renderer/src/draw/global.ts index 3294104..c58d35d 100644 --- a/packages/renderer/src/draw/global.ts +++ b/packages/renderer/src/draw/global.ts @@ -1,15 +1,15 @@ -import type { RendererDrawElementOptions, ViewContext2D, ElementGlobal } from '@idraw/types'; +import type { RendererDrawMaterialOptions, ViewContext2D, DataGlobal } from '@idraw/types'; export function drawGlobalBackground( ctx: ViewContext2D, - global: ElementGlobal | undefined, - opts: RendererDrawElementOptions + global: DataGlobal | undefined, + opts: RendererDrawMaterialOptions ) { - if (typeof global?.background === 'string') { + if (typeof global?.fill === 'string') { const { viewSizeInfo } = opts; const { width, height } = viewSizeInfo; ctx.globalAlpha = 1; - ctx.fillStyle = global.background; + ctx.fillStyle = global.fill as string; ctx.fillRect(0, 0, width, height); } } diff --git a/packages/renderer/src/draw/group.ts b/packages/renderer/src/draw/group.ts index 65a6cd1..f60aab0 100644 --- a/packages/renderer/src/draw/group.ts +++ b/packages/renderer/src/draw/group.ts @@ -1,69 +1,87 @@ -import type { Element, ElementType, ElementSize, RendererDrawElementOptions, ViewContext2D } from '@idraw/types'; -import { rotateElement, calcViewBoxSize, calcViewElementSize } from '@idraw/util'; -import { drawCircle } from './circle'; +import type { + StrictMaterial, + MaterialType, + MaterialSize, + RendererDrawMaterialOptions, + ViewContext2D, +} from '@idraw/types'; +// import { calcViewMaterialSize } from '@idraw/util'; import { drawRect } from './rect'; +import { drawCircle } from './circle'; +import { drawEllipse } from './ellipse'; import { drawImage } from './image'; import { drawText } from './text'; -import { drawSVG } from './svg'; -import { drawHTML } from './html'; -import { drawBox, drawBoxShadow, getOpacity } from './box'; +import { drawSVGCode } from './svg-code'; +import { drawForeignObject } from './foreign-object'; import { drawPath } from './path'; +import { drawBase, rotateViewMaterial } from './base'; const visiableMinSize = 0.4; // px; -export function drawElement(ctx: ViewContext2D, elem: Element, opts: RendererDrawElementOptions) { - if (elem?.operations?.invisible === true) { +export function drawMaterial( + ctx: ViewContext2D, + mtrl: StrictMaterial, + opts: RendererDrawMaterialOptions +) { + if (mtrl?.operations?.invisible === true) { return; } - const { w, h } = elem; + const { width, height } = mtrl; const { scale } = opts.viewScaleInfo; - if ((scale < 1 && (w * scale < visiableMinSize || h * scale < visiableMinSize)) || opts.parentOpacity === 0) { + if ( + (scale < 1 && (width * scale < visiableMinSize || height * scale < visiableMinSize)) || + opts.parentOpacity === 0 + ) { return; } - const { overrideElementMap } = opts; - if (overrideElementMap?.[elem.uuid]?.operations?.invisible) { + const { overrideMaterialMap } = opts; + if (overrideMaterialMap?.[mtrl.id]?.operations?.invisible) { return; } try { - switch (elem.type) { + switch (mtrl.type) { case 'rect': { - drawRect(ctx, elem as Element<'rect'>, opts); + drawRect(ctx, mtrl as StrictMaterial<'rect'>, opts); break; } case 'circle': { - drawCircle(ctx, elem as Element<'circle'>, opts); + drawCircle(ctx, mtrl as StrictMaterial<'circle'>, opts); + break; + } + case 'ellipse': { + drawEllipse(ctx, mtrl as StrictMaterial<'ellipse'>, opts); break; } case 'text': { - drawText(ctx, elem as Element<'text'>, opts); + drawText(ctx, mtrl as StrictMaterial<'text'>, opts); break; } case 'image': { - drawImage(ctx, elem as Element<'image'>, opts); + drawImage(ctx, mtrl as StrictMaterial<'image'>, opts); break; } - case 'svg': { - drawSVG(ctx, elem as Element<'svg'>, opts); + case 'svgCode': { + drawSVGCode(ctx, mtrl as StrictMaterial<'svgCode'>, opts); break; } - case 'html': { - drawHTML(ctx, elem as Element<'html'>, opts); + case 'foreignObject': { + drawForeignObject(ctx, mtrl as StrictMaterial<'foreignObject'>, opts); break; } case 'path': { - drawPath(ctx, elem as Element<'path'>, opts); + drawPath(ctx, mtrl as StrictMaterial<'path'>, opts); break; } case 'group': { const assets = { - ...(opts.elementAssets || {}), - ...((elem as Element<'group'>).detail.assets || {}) + ...(opts.materialAssets || {}), + ...((mtrl as StrictMaterial<'group'>).assets || {}), }; - drawGroup(ctx, elem as Element<'group'>, { + drawGroup(ctx, mtrl as StrictMaterial<'group'>, { ...opts, - elementAssets: assets + materialAssets: assets, }); break; } @@ -77,85 +95,73 @@ export function drawElement(ctx: ViewContext2D, elem: Element, opts } } -export function drawGroup(ctx: ViewContext2D, elem: Element<'group'>, opts: RendererDrawElementOptions) { - const { viewScaleInfo, viewSizeInfo, parentOpacity } = opts; - const { x, y, w, h, angle } = - calcViewElementSize({ x: elem.x, y: elem.y, w: elem.w, h: elem.h, angle: elem.angle }, { viewScaleInfo }) || elem; - const viewElem = { ...elem, ...{ x, y, w, h, angle } }; - rotateElement(ctx, { x, y, w, h, angle }, () => { - ctx.globalAlpha = getOpacity(elem) * parentOpacity; - drawBoxShadow(ctx, viewElem, { - viewScaleInfo, - viewSizeInfo, - renderContent: () => { - drawBox(ctx, viewElem, { - originElem: elem, - calcElemSize: { x, y, w, h, angle }, - viewScaleInfo, - viewSizeInfo, - parentOpacity, - renderContent: () => { - const { x, y, w, h, radiusList } = calcViewBoxSize(viewElem, { - viewScaleInfo, - viewSizeInfo - }); - if (elem.detail.overflow === 'hidden') { - ctx.save(); +export function drawGroup(ctx: ViewContext2D, mtrl: StrictMaterial<'group'>, opts: RendererDrawMaterialOptions) { + // const { viewScaleInfo } = opts; + // const { x, y } = calcViewMaterialSize(mtrl, { viewScaleInfo }) || mtrl; - ctx.fillStyle = 'transparent'; - ctx.beginPath(); - ctx.moveTo(x + radiusList[0], y); - ctx.arcTo(x + w, y, x + w, y + h, radiusList[1]); - ctx.arcTo(x + w, y + h, x, y + h, radiusList[2]); - ctx.arcTo(x, y + h, x, y, radiusList[3]); - ctx.arcTo(x, y, x + w, y, radiusList[0]); - ctx.closePath(); - ctx.fill('nonzero'); - ctx.clip('nonzero'); - } - - if (Array.isArray(elem.detail.children)) { - const { parentElementSize: parentSize } = opts; - const newParentSize: ElementSize = { - x: parentSize.x + elem.x, - y: parentSize.y + elem.y, - w: elem.w || parentSize.w, - h: elem.h || parentSize.h, - angle: elem.angle - }; - const { calculator } = opts; - - for (let i = 0; i < elem.detail.children.length; i++) { - let child = elem.detail.children[i]; - child = { - ...child, - ...{ - x: newParentSize.x + child.x, - y: newParentSize.y + child.y - } - }; - if (opts.forceDrawAll !== true) { - if (!calculator?.needRender(child)) { - continue; - } - } - - try { - drawElement(ctx, child, { ...opts, ...{ parentOpacity: parentOpacity * getOpacity(elem) } }); - } catch (err) { - // eslint-disable-next-line no-console - console.error(err); - } - } - } - - if (elem.detail.overflow === 'hidden') { - ctx.restore(); - } - } - }); - } - }); - ctx.globalAlpha = parentOpacity; + rotateViewMaterial(ctx, mtrl, opts, () => { + drawBase(ctx, mtrl, opts); }); + + // // render group children + // const { offsetLeft, offsetTop } = viewScaleInfo; + // const translateX = x - offsetLeft; + // const translateY = y - offsetTop; + // // move start point + // ctx.translate(translateX, translateY); + // rotateViewMaterial( + // ctx, + // { + // ...mtrl, + // x: x - translateX, + // y: y - translateY, + // }, + // opts, + // () => { + // // if (mtrl.overflow === 'hidden') { + // // ctx.save(); + // // // TODO + // // } + // // if (mtrl.overflow === 'hidden') { + // // ctx.restore(); + // // } + // } + // ); + // // reset start point + // ctx.translate(-translateX, -translateY); + + if (Array.isArray(mtrl.children)) { + const { parentMaterialSize: parentSize } = opts; + const newParentSize: MaterialSize = { + x: parentSize.x + mtrl.x, + y: parentSize.y + mtrl.y, + width: mtrl.width || parentSize.width, + height: mtrl.height || parentSize.height, + angle: mtrl.angle, + }; + const { calculator } = opts; + + for (let i = 0; i < mtrl.children.length; i++) { + let child = mtrl.children[i]; + child = { + ...child, + ...{ + x: newParentSize.x + child.x, + y: newParentSize.y + child.y, + }, + }; + if (opts.forceDrawAll !== true) { + if (!calculator?.needRender(child)) { + continue; + } + } + + try { + drawMaterial(ctx, child, opts); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + } + } } diff --git a/packages/renderer/src/draw/html.ts b/packages/renderer/src/draw/html.ts deleted file mode 100644 index 1bd9195..0000000 --- a/packages/renderer/src/draw/html.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types'; -import { rotateElement, calcViewElementSize } from '@idraw/util'; -import { getOpacity } from './box'; - -export function drawHTML(ctx: ViewContext2D, elem: Element<'html'>, opts: RendererDrawElementOptions) { - const content = opts.loader.getContent(elem); - const { viewScaleInfo, parentOpacity } = opts; - const { x, y, w, h, angle } = calcViewElementSize(elem, { viewScaleInfo }) || elem; - rotateElement(ctx, { x, y, w, h, angle }, () => { - if (!content && !opts.loader.isDestroyed()) { - opts.loader.load(elem as Element<'html'>, opts.elementAssets || {}); - } - if (elem.type === 'html' && content) { - ctx.globalAlpha = getOpacity(elem) * parentOpacity; - ctx.drawImage(content, x, y, w, h); - ctx.globalAlpha = parentOpacity; - } - }); -} diff --git a/packages/renderer/src/draw/image.ts b/packages/renderer/src/draw/image.ts index 2a117c8..bc60ccf 100644 --- a/packages/renderer/src/draw/image.ts +++ b/packages/renderer/src/draw/image.ts @@ -1,106 +1,90 @@ -import type { Element, RendererDrawElementOptions, ViewContext2D, LoadContent } from '@idraw/types'; -import { rotateElement, calcViewBoxSize, calcViewElementSize } from '@idraw/util'; -import { drawBox, drawBoxShadow, getOpacity } from './box'; +import type { StrictMaterial, RendererDrawMaterialOptions, ViewContext2D, LoadContent } from '@idraw/types'; +import { calcViewMaterialSize } from '@idraw/util'; +import { getOpacity } from './box'; +import { rotateViewMaterial } from './base'; -export function drawImage(ctx: ViewContext2D, elem: Element<'image'>, opts: RendererDrawElementOptions) { - const content: LoadContent | HTMLCanvasElement | OffscreenCanvas | null = opts.loader.getContent(elem); - const { viewScaleInfo, viewSizeInfo, parentOpacity } = opts; - const { x, y, w, h, angle } = calcViewElementSize(elem, { viewScaleInfo }) || elem; +export function drawImage(ctx: ViewContext2D, mtrl: StrictMaterial<'image'>, opts: RendererDrawMaterialOptions) { + const content: LoadContent | HTMLCanvasElement | OffscreenCanvas | null = opts.loader.getContent(mtrl); + const { viewScaleInfo, parentOpacity } = opts; + const { x, y, width, height, angle } = calcViewMaterialSize(mtrl, { viewScaleInfo }) || mtrl; - const viewElem = { ...elem, ...{ x, y, w, h, angle } }; - rotateElement(ctx, { x, y, w, h, angle }, () => { - drawBoxShadow(ctx, viewElem, { - viewScaleInfo, - viewSizeInfo, - renderContent: () => { - drawBox(ctx, viewElem, { - originElem: elem, - calcElemSize: { x, y, w, h, angle }, - viewScaleInfo, - viewSizeInfo, - parentOpacity, - renderContent: () => { - if (!content && !opts.loader.isDestroyed()) { - opts.loader.load(elem as Element<'image'>, opts.elementAssets || {}); - } - if (elem.type === 'image' && content) { - ctx.globalAlpha = getOpacity(elem) * parentOpacity; - const { x, y, w, h, radiusList } = calcViewBoxSize(viewElem, { - viewScaleInfo, - viewSizeInfo - }); - const { detail } = elem; - const { scaleMode, originW = 0, originH = 0 } = detail; - const imageW = ctx.$undoPixelRatio(originW); - const imageH = ctx.$undoPixelRatio(originH); + const viewMtrl = { ...mtrl, ...{ x, y, width, height, angle } }; + rotateViewMaterial(ctx, mtrl, opts, () => { + if (!content && !opts.loader.isDestroyed()) { + opts.loader.load(mtrl as StrictMaterial<'image'>, opts.materialAssets || {}); + } + if (mtrl.type === 'image' && content) { + ctx.globalAlpha = getOpacity(mtrl) * parentOpacity; + const { x, y, width, height } = viewMtrl; + const radiusList: number[] = [0, 0, 0, 0]; // TODO + const attributes = mtrl; + const { scaleMode, originW = 0, originH = 0 } = attributes; + const imageW = ctx.$undoPixelRatio(originW); + const imageH = ctx.$undoPixelRatio(originH); - ctx.save(); - ctx.fillStyle = 'transparent'; - ctx.beginPath(); - ctx.moveTo(x + radiusList[0], y); - ctx.arcTo(x + w, y, x + w, y + h, radiusList[1]); - ctx.arcTo(x + w, y + h, x, y + h, radiusList[2]); - ctx.arcTo(x, y + h, x, y, radiusList[3]); - ctx.arcTo(x, y, x + w, y, radiusList[0]); - ctx.closePath(); - ctx.fill('nonzero'); - ctx.clip('nonzero'); + ctx.save(); + ctx.fillStyle = 'transparent'; + ctx.beginPath(); + ctx.moveTo(x + radiusList[0], y); + ctx.arcTo(x + width, y, x + width, y + height, radiusList[1]); + ctx.arcTo(x + width, y + height, x, y + height, radiusList[2]); + ctx.arcTo(x, y + height, x, y, radiusList[3]); + ctx.arcTo(x, y, x + width, y, radiusList[0]); + ctx.closePath(); + ctx.fill('nonzero'); + ctx.clip('nonzero'); - if (scaleMode && originH && originW) { - let sx = 0; - let sy = 0; - let sWidth = imageW; - let sHeight = imageH; - const dx = x; - const dy = y; - const dWidth = w; - const dHeight = h; + if (scaleMode && originH && originW) { + let sx = 0; + let sy = 0; + let sWidth = imageW; + let sHeight = imageH; + const dx = x; + const dy = y; + const dWidth = width; + const dHeight = height; - if (imageW > elem.w || imageH > elem.h) { - if (scaleMode === 'fill') { - const sourceScale = Math.max(elem.w / imageW, elem.h / imageH); - const newImageWidth = imageW * sourceScale; - const newImageHeight = imageH * sourceScale; - sx = (newImageWidth - elem.w) / 2 / sourceScale; - sy = (newImageHeight - elem.h) / 2 / sourceScale; - sWidth = elem.w / sourceScale; - sHeight = elem.h / sourceScale; - } else if (scaleMode === 'tile') { - sx = 0; - sy = 0; - sWidth = elem.w; - sHeight = elem.h; - } else if (scaleMode === 'fit') { - const sourceScale = Math.min(elem.w / imageW, elem.h / imageH); - sx = (imageW - elem.w / sourceScale) / 2; - sy = (imageH - elem.h / sourceScale) / 2; - sWidth = elem.w / sourceScale; - sHeight = elem.h / sourceScale; - } - } - - ctx.drawImage(content, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); - } else { - ctx.drawImage(content, x, y, w, h); - - // const sx = 0; - // const sy = 0; - // const sWidth = imageW; - // const sHeight = imageH; - // const dx = x; - // const dy = y; - // const dWidth = w; - // const dHeight = h; - // ctx.drawImage(content, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); - } - // content = null; - - ctx.globalAlpha = parentOpacity; - ctx.restore(); - } + if (imageW > mtrl.width || imageH > mtrl.height) { + if (scaleMode === 'fill') { + const sourceScale = Math.max(mtrl.width / imageW, mtrl.height / imageH); + const newImageWidth = imageW * sourceScale; + const newImageHeight = imageH * sourceScale; + sx = (newImageWidth - mtrl.width) / 2 / sourceScale; + sy = (newImageHeight - mtrl.height) / 2 / sourceScale; + sWidth = mtrl.width / sourceScale; + sHeight = mtrl.height / sourceScale; + } else if (scaleMode === 'tile') { + sx = 0; + sy = 0; + sWidth = mtrl.width; + sHeight = mtrl.height; + } else if (scaleMode === 'fit') { + const sourceScale = Math.min(mtrl.width / imageW, mtrl.height / imageH); + sx = (imageW - mtrl.width / sourceScale) / 2; + sy = (imageH - mtrl.height / sourceScale) / 2; + sWidth = mtrl.width / sourceScale; + sHeight = mtrl.height / sourceScale; } - }); + } + + ctx.drawImage(content, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); + } else { + ctx.drawImage(content, x, y, width, height); + + // const sx = 0; + // const sy = 0; + // const sWidth = imageW; + // const sHeight = imageH; + // const dx = x; + // const dy = y; + // const dWidth = w; + // const dHeight = h; + // ctx.drawImage(content, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); } - }); + // content = null; + + ctx.globalAlpha = parentOpacity; + ctx.restore(); + } }); } diff --git a/packages/renderer/src/draw/index.ts b/packages/renderer/src/draw/index.ts index a15b709..a6f1b59 100644 --- a/packages/renderer/src/draw/index.ts +++ b/packages/renderer/src/draw/index.ts @@ -1,10 +1,12 @@ -export { drawCircle } from './circle'; export { drawRect } from './rect'; +export { drawCircle } from './circle'; +export { drawEllipse } from './ellipse'; + export { drawImage } from './image'; -export { drawSVG } from './svg'; -export { drawHTML } from './html'; +export { drawSVGCode } from './svg-code'; +export { drawForeignObject } from './foreign-object'; export { drawText } from './text'; -export { drawGroup, drawElement } from './group'; -export { drawElementList } from './elements'; +export { drawGroup, drawMaterial } from './group'; +export { drawMaterialList } from './materials'; export { drawLayout } from './layout'; export { drawGlobalBackground } from './global'; diff --git a/packages/renderer/src/draw/layout.ts b/packages/renderer/src/draw/layout.ts index 73d763c..8848e53 100644 --- a/packages/renderer/src/draw/layout.ts +++ b/packages/renderer/src/draw/layout.ts @@ -1,56 +1,48 @@ -import type { RendererDrawElementOptions, ViewContext2D, DataLayout, Element } from '@idraw/types'; -import { calcViewElementSize, calcViewBoxSize } from '@idraw/util'; -import { drawBoxShadow, drawBoxBackground, drawBoxBorder } from './box'; +import type { RendererDrawMaterialOptions, ViewContext2D, DataLayout, StrictMaterial } from '@idraw/types'; +import { calcViewMaterialSize } from '@idraw/util'; +import { createColor } from './color'; export function drawLayout( ctx: ViewContext2D, layout: DataLayout, - opts: RendererDrawElementOptions, + opts: RendererDrawMaterialOptions, renderContent: (ctx: ViewContext2D) => void ) { - const { viewScaleInfo, viewSizeInfo, parentOpacity } = opts; - const elem: Element = { uuid: 'layout', type: 'group', ...layout } as Element; - const { x, y, w, h } = calcViewElementSize(elem, { viewScaleInfo }) || elem; - const angle = 0; - const viewElem: Element = { ...elem, ...{ x, y, w, h, angle } } as Element; + const { parentOpacity } = opts; + ctx.globalAlpha = 1; - drawBoxShadow(ctx, viewElem, { - viewScaleInfo, - viewSizeInfo, - renderContent: () => { - drawBoxBackground(ctx, viewElem, { viewScaleInfo, viewSizeInfo }); - } - }); - if (layout.detail.overflow === 'hidden') { - const { viewScaleInfo, viewSizeInfo } = opts; - const elem: Element<'group'> = { uuid: 'layout', type: 'group', ...layout } as Element<'group'>; - const viewElemSize = calcViewElementSize(elem, { viewScaleInfo }) || elem; - const viewElem = { ...elem, ...viewElemSize }; - const { x, y, w, h, radiusList } = calcViewBoxSize(viewElem, { - viewScaleInfo, - viewSizeInfo - }); - ctx.save(); + const { viewScaleInfo } = opts; + const mtrl: StrictMaterial<'group'> = { + id: 'layout', + type: 'group', + ...layout, + } as unknown as StrictMaterial<'group'>; + const viewMtrlSize = calcViewMaterialSize(mtrl, { viewScaleInfo }) || mtrl; + const viewMtrl = { ...mtrl, ...viewMtrlSize }; + const { x, y, width, height, fill } = viewMtrl; + const radiusList: number[] = [0, 0, 0, 0]; // TODO - ctx.fillStyle = 'transparent'; - ctx.beginPath(); - ctx.moveTo(x + radiusList[0], y); - ctx.arcTo(x + w, y, x + w, y + h, radiusList[1]); - ctx.arcTo(x + w, y + h, x, y + h, radiusList[2]); - ctx.arcTo(x, y + h, x, y, radiusList[3]); - ctx.arcTo(x, y, x + w, y, radiusList[0]); - ctx.closePath(); - ctx.fill('nonzero'); + ctx.save(); + ctx.fillStyle = createColor(ctx, fill, { viewMaterialSize: viewMtrlSize, viewScaleInfo, opacity: mtrl.opacity || 1 }); + ctx.beginPath(); + ctx.moveTo(x + radiusList[0], y); + ctx.arcTo(x + width, y, x + width, y + height, radiusList[1]); + ctx.arcTo(x + width, y + height, x, y + height, radiusList[2]); + ctx.arcTo(x, y + height, x, y, radiusList[3]); + ctx.arcTo(x, y, x + width, y, radiusList[0]); + ctx.closePath(); + ctx.fill('nonzero'); + + if (layout.overflow === 'hidden') { ctx.clip('nonzero'); } renderContent(ctx); - if (layout.detail.overflow === 'hidden') { + if (layout.overflow === 'hidden') { ctx.restore(); } - drawBoxBorder(ctx, viewElem, { originElem: elem, viewScaleInfo, viewSizeInfo }); ctx.globalAlpha = parentOpacity; } diff --git a/packages/renderer/src/draw/materials.ts b/packages/renderer/src/draw/materials.ts new file mode 100644 index 0000000..e4fe0ea --- /dev/null +++ b/packages/renderer/src/draw/materials.ts @@ -0,0 +1,40 @@ +import type { Data, RendererDrawMaterialOptions, ViewContext2D } from '@idraw/types'; +import { getDefaultMaterialAttributes } from '@idraw/util'; +import { drawMaterial } from './group'; + +const defaultAttributes = getDefaultMaterialAttributes(); + +export function drawMaterialList(ctx: ViewContext2D, data: Data, opts: RendererDrawMaterialOptions) { + const { materials = [] } = data; + const { parentOpacity } = opts; + for (let i = 0; i < materials.length; i++) { + const material = materials[i]; + const mtrl = { + ...material, + ...{ + attributes: { + ...defaultAttributes, + ...material, + }, + }, + }; + + if (opts.forceDrawAll !== true) { + if (!opts.calculator?.needRender(mtrl)) { + continue; + } + } + + try { + drawMaterial(ctx, mtrl, { + ...opts, + ...{ + parentOpacity, + }, + }); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + } +} diff --git a/packages/renderer/src/draw/path.ts b/packages/renderer/src/draw/path.ts index ef11a93..93ade6c 100644 --- a/packages/renderer/src/draw/path.ts +++ b/packages/renderer/src/draw/path.ts @@ -1,108 +1,8 @@ -import type { - Element, - RendererDrawElementOptions, - ViewContext2D, - LinearGradientColor, - RadialGradientColor -} from '@idraw/types'; -import { rotateElement, generateSVGPath, calcViewElementSize } from '@idraw/util'; -import { drawBox, drawBoxShadow } from './box'; +import type { StrictMaterial, RendererDrawMaterialOptions, ViewContext2D } from '@idraw/types'; +import { rotateViewMaterial, drawBase } from './base'; -export function drawPath(ctx: ViewContext2D, elem: Element<'path'>, opts: RendererDrawElementOptions) { - const { detail } = elem; - const { originX, originY, originW, originH, fillRule } = detail; - const { viewScaleInfo, viewSizeInfo, parentOpacity } = opts; - const { x, y, w, h, angle } = calcViewElementSize(elem, { viewScaleInfo }) || elem; - const scaleW = w / originW; - const scaleH = h / originH; - const viewOriginX = originX * scaleW; - const viewOriginY = originY * scaleH; - const internalX = x - viewOriginX; - const internalY = y - viewOriginY; - const { clipPath, clipPathStrokeColor, clipPathStrokeWidth, ...restDetail } = elem.detail as any; - - const scaleNum = viewScaleInfo.scale * viewSizeInfo.devicePixelRatio; - const viewElem = { ...elem, ...{ x, y, w, h, angle } }; - - let boxViewElem = { ...viewElem }; - boxViewElem.detail = restDetail; - let boxOriginElem = { ...elem }; - boxOriginElem.detail = restDetail; - - if ( - detail.fill && - detail.fill !== 'string' && - (detail.fill as LinearGradientColor | RadialGradientColor)?.type?.includes('gradient') - ) { - boxViewElem = { - ...viewElem, - ...{ - detail: { - ...viewElem.detail, - ...{ - background: detail.fill, - clipPath: { - commands: detail.commands, - originX, - originY, - originW, - originH - } - } - } - } - }; - boxOriginElem.detail = { ...boxViewElem.detail }; - } - - rotateElement(ctx, { x, y, w, h, angle }, () => { - drawBox(ctx, boxViewElem, { - originElem: boxOriginElem, - calcElemSize: { x, y, w, h, angle }, - viewScaleInfo, - viewSizeInfo, - parentOpacity, - renderContent: () => { - drawBoxShadow(ctx, viewElem, { - viewScaleInfo, - viewSizeInfo, - renderContent: () => { - ctx.save(); - ctx.translate(internalX, internalY); - // ctx.beginPath(); - // ctx.moveTo(viewOriginX, viewOriginY); - // ctx.lineTo(viewOriginX + w, viewOriginY); - // ctx.lineTo(viewOriginX + w, viewOriginY + h); - // ctx.lineTo(viewOriginX, viewOriginY + h); - // ctx.closePath(); - // ctx.clip('nonzero'); - ctx.scale((scaleNum * scaleW) / viewScaleInfo.scale, (scaleNum * scaleH) / viewScaleInfo.scale); - const pathStr = generateSVGPath(detail.commands || []); - const path2d = new Path2D(pathStr); - - if (detail.fill) { - if (typeof detail.fill === 'string') { - ctx.fillStyle = detail.fill; - } else { - ctx.fillStyle = 'transparent'; - } - } - - if (detail.fill) { - ctx.fill(path2d, (fillRule as CanvasFillRule) || 'nonzero'); - } - - if (detail.stroke && detail.strokeWidth !== 0) { - ctx.strokeStyle = detail.stroke; - ctx.lineWidth = (detail.strokeWidth || 1) / viewSizeInfo.devicePixelRatio; - ctx.lineCap = detail.strokeLineCap || 'square'; - ctx.stroke(path2d); - } - ctx.translate(-internalX, -internalY); - ctx.restore(); - } - }); - } - }); +export function drawPath(ctx: ViewContext2D, mtrl: StrictMaterial<'path'>, opts: RendererDrawMaterialOptions) { + rotateViewMaterial(ctx, mtrl, opts, () => { + drawBase(ctx, mtrl, opts); }); } diff --git a/packages/renderer/src/draw/rect.ts b/packages/renderer/src/draw/rect.ts index d77a7dc..096cdd9 100644 --- a/packages/renderer/src/draw/rect.ts +++ b/packages/renderer/src/draw/rect.ts @@ -1,28 +1,8 @@ -import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types'; -import { rotateElement, calcViewElementSize } from '@idraw/util'; -import { drawBox, drawBoxShadow } from './box'; +import type { StrictMaterial, RendererDrawMaterialOptions, ViewContext2D } from '@idraw/types'; +import { drawBase, rotateViewMaterial } from './base'; -export function drawRect(ctx: ViewContext2D, elem: Element<'rect'>, opts: RendererDrawElementOptions) { - const { viewScaleInfo, viewSizeInfo, parentOpacity } = opts; - const { x, y, w, h, angle } = calcViewElementSize(elem, { viewScaleInfo }) || elem; - - const viewElem = { ...elem, ...{ x, y, w, h, angle } }; - rotateElement(ctx, { x, y, w, h, angle }, () => { - drawBoxShadow(ctx, viewElem, { - viewScaleInfo, - viewSizeInfo, - renderContent: () => { - drawBox(ctx, viewElem, { - originElem: elem, - calcElemSize: { x, y, w, h, angle }, - viewScaleInfo, - viewSizeInfo, - parentOpacity, - renderContent: () => { - // empty - } - }); - } - }); +export function drawRect(ctx: ViewContext2D, mtrl: StrictMaterial<'rect'>, opts: RendererDrawMaterialOptions) { + rotateViewMaterial(ctx, mtrl, opts, () => { + drawBase(ctx, mtrl, opts); }); } diff --git a/packages/renderer/src/draw/svg-code.ts b/packages/renderer/src/draw/svg-code.ts new file mode 100644 index 0000000..e6a1a30 --- /dev/null +++ b/packages/renderer/src/draw/svg-code.ts @@ -0,0 +1,17 @@ +import type { StrictMaterial, RendererDrawMaterialOptions, ViewContext2D } from '@idraw/types'; +import { calcViewMaterialSize } from '@idraw/util'; +import { rotateViewMaterial } from './base'; + +export function drawSVGCode(ctx: ViewContext2D, mtrl: StrictMaterial<'svgCode'>, opts: RendererDrawMaterialOptions) { + const content = opts.loader.getContent(mtrl); + const { viewScaleInfo } = opts; + const { x, y, width, height } = calcViewMaterialSize(mtrl, { viewScaleInfo }) || mtrl; + rotateViewMaterial(ctx, mtrl, opts, () => { + if (!content && !opts.loader.isDestroyed()) { + opts.loader.load(mtrl as StrictMaterial<'svgCode'>, opts.materialAssets || {}); + } + if (mtrl.type === 'svgCode' && content) { + ctx.drawImage(content, x, y, width, height); + } + }); +} diff --git a/packages/renderer/src/draw/svg.ts b/packages/renderer/src/draw/svg.ts deleted file mode 100644 index 31bbf0c..0000000 --- a/packages/renderer/src/draw/svg.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types'; -import { rotateElement, calcViewElementSize } from '@idraw/util'; -import { getOpacity } from './box'; - -export function drawSVG(ctx: ViewContext2D, elem: Element<'svg'>, opts: RendererDrawElementOptions) { - const content = opts.loader.getContent(elem); - const { viewScaleInfo, parentOpacity } = opts; - const { x, y, w, h, angle } = calcViewElementSize(elem, { viewScaleInfo }) || elem; - rotateElement(ctx, { x, y, w, h, angle }, () => { - if (!content && !opts.loader.isDestroyed()) { - opts.loader.load(elem as Element<'svg'>, opts.elementAssets || {}); - } - if (elem.type === 'svg' && content) { - ctx.globalAlpha = getOpacity(elem) * parentOpacity; - ctx.drawImage(content, x, y, w, h); - ctx.globalAlpha = parentOpacity; - } - }); -} diff --git a/packages/renderer/src/draw/text.ts b/packages/renderer/src/draw/text.ts index a220d98..89936c7 100644 --- a/packages/renderer/src/draw/text.ts +++ b/packages/renderer/src/draw/text.ts @@ -1,74 +1,106 @@ -import type { Element, RendererDrawElementOptions, ViewContext2D } from '@idraw/types'; -import { rotateElement, calcViewElementSize, enhanceFontFamliy } from '@idraw/util'; -import { is, isColorStr, getDefaultElementDetailConfig } from '@idraw/util'; -import { drawBox, drawBoxShadow, getOpacity } from './box'; +import type { StrictMaterial, RendererDrawMaterialOptions, ViewContext2D } from '@idraw/types'; +import { enhanceFontFamliy, calcViewMaterialSize } from '@idraw/util'; +import { getDefaultMaterialAttributes } from '@idraw/util'; +import { rotateViewMaterial, drawClipPath } from './base'; +import { createColor } from './color'; -const detailConfig = getDefaultElementDetailConfig(); +const defaultAttrs = getDefaultMaterialAttributes(); -export function drawText(ctx: ViewContext2D, elem: Element<'text'>, opts: RendererDrawElementOptions) { - const { viewScaleInfo, viewSizeInfo, parentOpacity, calculator } = opts; - const { x, y, w, h, angle } = calcViewElementSize(elem, { viewScaleInfo }) || elem; - const viewElem = { ...elem, ...{ x, y, w, h, angle } }; - rotateElement(ctx, { x, y, w, h, angle }, () => { - drawBoxShadow(ctx, viewElem, { - viewScaleInfo, - viewSizeInfo, - renderContent: () => { - drawBox(ctx, viewElem, { - originElem: elem, - calcElemSize: { x, y, w, h, angle }, - viewScaleInfo, - viewSizeInfo, - parentOpacity - }); - } - }); +export function drawText(ctx: ViewContext2D, mtrl: StrictMaterial<'text'>, opts: RendererDrawMaterialOptions) { + const { viewScaleInfo, viewSizeInfo, calculator } = opts; + const viewMaterialSize = calcViewMaterialSize(mtrl, { viewScaleInfo }) || mtrl; + + const { scale } = viewScaleInfo; + + rotateViewMaterial(ctx, mtrl, opts, ({ viewWorldSize }) => { { - const detail: Element<'text'>['detail'] = { - ...detailConfig, - ...elem.detail + const attributes = { + ...defaultAttrs, + ...mtrl, }; - const originFontSize = detail.fontSize || detailConfig.fontSize; + const { x, y } = viewWorldSize; + + const originFontSize = attributes.fontSize || defaultAttrs.fontSize; const fontSize = originFontSize * viewScaleInfo.scale; + const { + opacity, + fill, + fillOpacity, + stroke, + strokeWidth, + strokeOpacity, + strokeLinecap, + strokeLinejoin, + strokeDasharray, + strokeDashoffset, + strokeMiterlimit, + } = attributes; + if (fontSize < 2) { return; } - const { parentOpacity } = opts; - const opacity = getOpacity(elem) * parentOpacity; - ctx.globalAlpha = opacity; - ctx.fillStyle = elem.detail.color || detailConfig.color; + const originGlobalAlpha = ctx.globalAlpha; + const virtualTextAttributes = calculator.getVirtualItem(mtrl.id); ctx.textBaseline = 'top'; ctx.$setFont({ - fontWeight: detail.fontWeight, + fontWeight: attributes.fontWeight, fontSize: fontSize, - fontFamily: enhanceFontFamliy(detail.fontFamily) + fontFamily: enhanceFontFamliy(attributes.fontFamily), }); - { - const virtualTextDetail = calculator.getVirtualFlatItem(elem.uuid); - if (Array.isArray(virtualTextDetail?.textLines) && virtualTextDetail?.textLines?.length > 0) { - if (detail.textShadowColor !== undefined && isColorStr(detail.textShadowColor)) { - ctx.shadowColor = detail.textShadowColor; - } - if (detail.textShadowOffsetX !== undefined && is.number(detail.textShadowOffsetX)) { - ctx.shadowOffsetX = detail.textShadowOffsetX; - } - if (detail.textShadowOffsetY !== undefined && is.number(detail.textShadowOffsetY)) { - ctx.shadowOffsetY = detail.textShadowOffsetY; - } - if (detail.textShadowBlur !== undefined && is.number(detail.textShadowBlur)) { - ctx.shadowBlur = detail.textShadowBlur; + drawClipPath(ctx, mtrl, { + viewScaleInfo, + viewSizeInfo, + calculator, + renderContent: () => { + // fill + if (fill) { + if (typeof fillOpacity === 'number' && fillOpacity > 0) { + ctx.globalAlpha = originGlobalAlpha * fillOpacity * opacity; + } + ctx.fillStyle = createColor(ctx, fill, { viewMaterialSize, viewScaleInfo, opacity: mtrl.opacity || 1 }); + if (Array.isArray(virtualTextAttributes?.textLines) && virtualTextAttributes?.textLines?.length > 0) { + virtualTextAttributes?.textLines?.forEach((line) => { + ctx.fillText(line.text, x + line.x * viewScaleInfo.scale, y + line.y * viewScaleInfo.scale); + }); + } + ctx.globalAlpha = originGlobalAlpha; } - virtualTextDetail?.textLines?.forEach((line) => { - ctx.fillText(line.text, x + line.x * viewScaleInfo.scale, y + line.y * viewScaleInfo.scale); - }); - } - } + // stroke + if (typeof strokeWidth === 'number' && strokeWidth > 0) { + if (typeof strokeOpacity === 'number' && strokeOpacity > 0) { + ctx.globalAlpha = originGlobalAlpha * strokeOpacity * opacity; + } - ctx.globalAlpha = parentOpacity; + ctx.lineCap = strokeLinecap; + ctx.lineJoin = strokeLinejoin; + ctx.lineDashOffset = strokeDashoffset; + ctx.miterLimit = strokeMiterlimit; + + if (Array.isArray(strokeDasharray)) { + const lineDash = strokeDasharray.map((dash) => scale * dash); + ctx.setLineDash(lineDash); + } + ctx.lineWidth = strokeWidth * scale; + ctx.strokeStyle = stroke as string; // TODO + + virtualTextAttributes?.textLines?.forEach((line) => { + ctx.strokeText(line.text, x + line.x * viewScaleInfo.scale, y + line.y * viewScaleInfo.scale); + }); + ctx.setLineDash([]); + + // reset + ctx.lineCap = defaultAttrs.strokeLinecap; + ctx.lineJoin = defaultAttrs.strokeLinejoin; + ctx.lineDashOffset = defaultAttrs.strokeDashoffset; + ctx.miterLimit = defaultAttrs.strokeMiterlimit; + ctx.globalAlpha = originGlobalAlpha; + } + }, + }); } }); } diff --git a/packages/renderer/src/index.ts b/packages/renderer/src/index.ts index f3d2f52..4a8334a 100644 --- a/packages/renderer/src/index.ts +++ b/packages/renderer/src/index.ts @@ -1,6 +1,6 @@ import { EventEmitter } from '@idraw/util'; import type { DataLayout, LoadItemMap } from '@idraw/types'; -import { drawElementList, drawLayout, drawGlobalBackground } from './draw/index'; +import { drawMaterialList, drawLayout, drawGlobalBackground } from './draw/index'; import { Loader } from './loader'; import type { Data, BoardRenderer, RendererOptions, RendererEventMap, RendererDrawOptions } from '@idraw/types'; import { Calculator } from './calculator'; @@ -16,7 +16,7 @@ export class Renderer extends EventEmitter implements BoardRen super(); this.#opts = opts; this.#calculator = new Calculator({ - tempContext: opts.tempContext + tempContext: opts.tempContext, }); this.#init(); } @@ -55,45 +55,46 @@ export class Renderer extends EventEmitter implements BoardRen const { sharer } = this.#opts; const viewContext = this.#opts.viewContext; viewContext.clearRect(0, 0, viewContext.canvas.width, viewContext.canvas.height); - const parentElementSize = { + const parentMaterialSize = { x: 0, y: 0, - w: opts.viewSizeInfo.width, - h: opts.viewSizeInfo.height + width: opts.viewSizeInfo.width, + height: opts.viewSizeInfo.height, }; // if (data.underlay) { // drawUnderlay(viewContext, data.underlay, { // loader, // calculator, - // parentElementSize, + // parentMaterialSize, // parentOpacity: 1, // ...opts // }); // } if (opts.forceDrawAll === true) { - this.#calculator.resetVirtualFlatItemMap(data, { + this.#calculator.resetVirtualItemMap(data, { viewScaleInfo: opts.viewScaleInfo, - viewSizeInfo: opts.viewSizeInfo + viewSizeInfo: opts.viewSizeInfo, }); } const drawOpts = { loader, calculator, - parentElementSize, - elementAssets: data.assets, + parentMaterialSize, + materialAssets: data.assets, parentOpacity: 1, - overrideElementMap: sharer?.getActiveOverrideElemenentMap(), - ...opts + overrideMaterialMap: sharer?.getActiveOverrideMaterialMap(), + tempContext: this.#opts.tempContext, + ...opts, }; drawGlobalBackground(viewContext, data.global, drawOpts); if (data.layout) { drawLayout(viewContext, data.layout as DataLayout, drawOpts, () => { - drawElementList(viewContext, data, drawOpts); + drawMaterialList(viewContext, data, drawOpts); }); } else { - drawElementList(viewContext, data, drawOpts); + drawMaterialList(viewContext, data, drawOpts); } } @@ -113,7 +114,7 @@ export class Renderer extends EventEmitter implements BoardRen height, contextHeight, contextWidth, - devicePixelRatio + devicePixelRatio, } = sharer.getActiveStoreSnapshot(); if (data) { this.drawData(data, { @@ -122,15 +123,15 @@ export class Renderer extends EventEmitter implements BoardRen offsetTop, offsetBottom, offsetLeft, - offsetRight + offsetRight, }, viewSizeInfo: { width, height, contextHeight, contextWidth, - devicePixelRatio - } + devicePixelRatio, + }, }); } } @@ -156,12 +157,12 @@ export { drawCircle, drawRect, drawImage, - drawSVG, - drawHTML, + drawSVGCode, + drawForeignObject, drawText, drawGroup, - drawElement, - drawElementList, + drawMaterial, + drawMaterialList, drawLayout, - drawGlobalBackground + drawGlobalBackground, } from './draw'; diff --git a/packages/renderer/src/loader.ts b/packages/renderer/src/loader.ts index 620f594..33c7080 100644 --- a/packages/renderer/src/loader.ts +++ b/packages/renderer/src/loader.ts @@ -5,69 +5,80 @@ import type { LoadContent, LoadItem, LoadItemMap, - LoadElementType, - Element, - ElementAssets, - RecursivePartial + LoadMaterialType, + StrictMaterial, + MaterialAssets, + RecursivePartial, } from '@idraw/types'; -import { loadImage, loadHTML, loadSVG, EventEmitter, createAssetId, isAssetId, createUUID } from '@idraw/util'; +import { + loadImage, + loadForeignObject, + loadSVGCode, + EventEmitter, + createAssetId, + isAssetId, + createUUID, +} from '@idraw/util'; -const supportElementTypes: LoadElementType[] = ['image', 'svg', 'html']; +const supportMaterialTypes: LoadMaterialType[] = ['image', 'svgCode', 'foreignObject']; -const getAssetIdFromElement = (element: Element<'image' | 'svg' | 'html'>) => { +const getAssetIdFromMaterial = (material: StrictMaterial<'image' | 'svgCode' | 'foreignObject'>) => { let source: string | null = null; - if (element.type === 'image') { - source = (element as Element<'image'>)?.detail?.src || null; - } else if (element.type === 'svg') { - source = (element as Element<'svg'>)?.detail?.svg || null; - } else if (element.type === 'html') { - source = (element as Element<'html'>)?.detail?.html || null; + if (material.type === 'image') { + source = (material as StrictMaterial<'image'>)?.href || null; + } else if (material.type === 'svgCode') { + source = (material as StrictMaterial<'svgCode'>)?.code || null; + } else if (material.type === 'foreignObject') { + source = (material as StrictMaterial<'foreignObject'>)?.content || null; } if (typeof source === 'string' && source) { if (isAssetId(source)) { return source; } - return createAssetId(source, element.uuid); + return createAssetId(source, material.id); } - return createAssetId(`${createUUID()}-${element.uuid}-${createUUID()}-${createUUID()}`, element.uuid); + return createAssetId(`${createUUID()}-${material.id}-${createUUID()}-${createUUID()}`, material.id); }; export class Loader extends EventEmitter implements RendererLoader { - #loadFuncMap: Record> = {}; + #loadFuncMap: Record> = {}; #currentLoadItemMap: LoadItemMap = {}; #storageLoadItemMap: LoadItemMap = {}; #hasDestroyed: boolean = false; constructor() { super(); - this.#registerLoadFunc<'image'>('image', async (elem: Element<'image'>, assets: ElementAssets) => { - const src = assets[elem.detail.src]?.value || elem.detail.src; - const content = await loadImage(src); + this.#registerLoadFunc<'image'>('image', async (mtrl: StrictMaterial<'image'>, assets: MaterialAssets) => { + const href = assets[mtrl.href]?.value || mtrl.href; + const content = await loadImage(href); return { - uuid: elem.uuid, + id: mtrl.id, lastModified: Date.now(), - content + content, }; }); - this.#registerLoadFunc<'html'>('html', async (elem: Element<'html'>, assets: ElementAssets) => { - const html = assets[elem.detail.html]?.value || elem.detail.html; - const content = await loadHTML(html, { - width: elem.detail.originW || elem.w, - height: elem.detail.originH || elem.h - }); + this.#registerLoadFunc<'foreignObject'>( + 'foreignObject', + async (mtrl: StrictMaterial<'foreignObject'>, assets: MaterialAssets) => { + const html = assets[mtrl.content]?.value || mtrl.content; + const content = await loadForeignObject(html, { + width: mtrl.originW || mtrl.width, + height: mtrl.originH || mtrl.height, + }); + return { + id: mtrl.id, + lastModified: Date.now(), + content, + }; + } + ); + this.#registerLoadFunc<'svgCode'>('svgCode', async (mtrl: StrictMaterial<'svgCode'>, assets: MaterialAssets) => { + const svg = assets[mtrl.code]?.value || mtrl.code; + const content = await loadSVGCode(svg); return { - uuid: elem.uuid, + id: mtrl.id, lastModified: Date.now(), - content - }; - }); - this.#registerLoadFunc<'svg'>('svg', async (elem: Element<'svg'>, assets: ElementAssets) => { - const svg = assets[elem.detail.svg]?.value || elem.detail.svg; - const content = await loadSVG(svg); - return { - uuid: elem.uuid, - lastModified: Date.now(), - content + content, }; }); } @@ -84,23 +95,26 @@ export class Loader extends EventEmitter implements RendererLoad this.#storageLoadItemMap = {}; } - resetElementAsset(element: Element | RecursivePartial) { - if (supportElementTypes.includes((element as Element).type)) { + resetMaterialAsset(material: StrictMaterial | RecursivePartial) { + if (supportMaterialTypes.includes((material as StrictMaterial).type)) { let assetId: string | null = null; let resource: string | null = null; - if (element.type === 'image' && typeof (element as Element<'image'>)?.detail?.src === 'string') { - resource = (element as Element<'image'>).detail.src; - } else if (element.type === 'svg' && typeof (element as Element<'svg'>)?.detail?.svg === 'string') { - resource = (element as Element<'svg'>).detail.svg; - } else if (element.type === 'html' && typeof (element as Element<'html'>)?.detail?.html === 'string') { - resource = (element as Element<'html'>).detail.html; + if (material.type === 'image' && typeof (material as StrictMaterial<'image'>)?.href === 'string') { + resource = (material as StrictMaterial<'image'>).href; + } else if (material.type === 'svgCode' && typeof (material as StrictMaterial<'svgCode'>)?.code === 'string') { + resource = (material as StrictMaterial<'svgCode'>).code; + } else if ( + material.type === 'foreignObject' && + typeof (material as StrictMaterial<'foreignObject'>)?.content === 'string' + ) { + resource = (material as StrictMaterial<'foreignObject'>).content; } if (typeof resource === 'string') { - this.load(element as Element, {}); + this.load(material as StrictMaterial, {}); if (isAssetId(resource)) { assetId = resource; - } else if (element.uuid) { - assetId = createAssetId(resource, element.uuid); + } else if (material.id) { + assetId = createAssetId(resource, material.id); } } if (assetId && isAssetId(assetId)) { @@ -118,38 +132,38 @@ export class Loader extends EventEmitter implements RendererLoad this.#storageLoadItemMap = null as any; } - #registerLoadFunc(type: T, func: LoadFunc) { + #registerLoadFunc(type: T, func: LoadFunc) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.#loadFuncMap[type] = func; } - #getLoadElementSource(element: Element): null | string { + #getLoadMaterialSource(material: StrictMaterial): null | string { let source: string | null = null; - if (element.type === 'image') { - source = (element as Element<'image'>)?.detail?.src || null; - } else if (element.type === 'svg') { - source = (element as Element<'svg'>)?.detail?.svg || null; - } else if (element.type === 'html') { - source = (element as Element<'html'>)?.detail?.html || null; + if (material.type === 'image') { + source = (material as StrictMaterial<'image'>)?.href || null; + } else if (material.type === 'svgCode') { + source = (material as StrictMaterial<'svgCode'>)?.code || null; + } else if (material.type === 'foreignObject') { + source = (material as StrictMaterial<'foreignObject'>)?.content || null; } return source; } - #createLoadItem(element: Element): LoadItem { + #createLoadItem(material: StrictMaterial): LoadItem { return { - element, + material, status: 'null', content: null, error: null, startTime: -1, endTime: -1, - source: this.#getLoadElementSource(element) + source: this.#getLoadMaterialSource(material), }; } #emitLoad(item: LoadItem) { - const assetId = getAssetIdFromElement(item.element); + const assetId = getAssetIdFromMaterial(item.material); const storageItem = this.#storageLoadItemMap[assetId]; if (!this.#hasDestroyed) { if (storageItem) { @@ -165,7 +179,7 @@ export class Loader extends EventEmitter implements RendererLoad } #emitError(item: LoadItem) { - const assetId = getAssetIdFromElement(item.element); + const assetId = getAssetIdFromMaterial(item.material); const storageItem = this.#storageLoadItemMap?.[assetId]; if (!this.#hasDestroyed) { if (storageItem) { @@ -180,18 +194,18 @@ export class Loader extends EventEmitter implements RendererLoad } } - #loadResource(element: Element, assets: ElementAssets) { - const item = this.#createLoadItem(element); - const assetId = getAssetIdFromElement(element); + #loadResource(material: StrictMaterial, assets: MaterialAssets) { + const item = this.#createLoadItem(material); + const assetId = getAssetIdFromMaterial(material); if (this.#currentLoadItemMap[assetId]) { return; } this.#currentLoadItemMap[assetId] = item; - const loadFunc = this.#loadFuncMap[element.type]; + const loadFunc = this.#loadFuncMap[material.type]; if (typeof loadFunc === 'function' && !this.#hasDestroyed) { item.startTime = Date.now(); - loadFunc(element, assets) + loadFunc(material, assets) .then((result) => { if (!this.#hasDestroyed) { item.content = result.content; @@ -202,7 +216,7 @@ export class Loader extends EventEmitter implements RendererLoad }) .catch((err: Error) => { // eslint-disable-next-line no-console - console.warn(`Load element source "${item.source}" fail`, err, element); + console.warn(`Load material source "${item.source}" fail`, err, material); item.endTime = Date.now(); item.status = 'error'; item.error = err; @@ -211,35 +225,35 @@ export class Loader extends EventEmitter implements RendererLoad } } - #isExistingErrorStorage(element: Element) { - const assetId = getAssetIdFromElement(element); + #isExistingErrorStorage(material: StrictMaterial) { + const assetId = getAssetIdFromMaterial(material); const existItem = this.#currentLoadItemMap?.[assetId]; if ( existItem && existItem.status === 'error' && existItem.source && - existItem.source === this.#getLoadElementSource(element) + existItem.source === this.#getLoadMaterialSource(material) ) { return true; } return false; } - load(element: Element, assets: ElementAssets) { + load(material: StrictMaterial, assets: MaterialAssets) { if (this.#hasDestroyed === true) { return; } - if (this.#isExistingErrorStorage(element)) { + if (this.#isExistingErrorStorage(material)) { return; } - if (supportElementTypes.includes(element.type)) { - // const elem = deepClone(element); - this.#loadResource(element, assets); + if (supportMaterialTypes.includes(material.type)) { + // const mtrl = deepClone(material); + this.#loadResource(material, assets); } } - getContent(element: Element): LoadContent | null { - const assetId = getAssetIdFromElement(element); + getContent(material: StrictMaterial): LoadContent | null { + const assetId = getAssetIdFromMaterial(material); return this.#storageLoadItemMap?.[assetId]?.content || null; } diff --git a/packages/renderer/src/virtual-flat/index.ts b/packages/renderer/src/virtual-flat/index.ts deleted file mode 100644 index 66e7401..0000000 --- a/packages/renderer/src/virtual-flat/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - Element, - ElementPosition, - Elements, - ViewRectInfo, - VirtualFlatItemMap, - VirtualFlatItem, - VirtualFlatDetail, - ViewContext2D -} from '@idraw/types'; -import { - is, - getGroupQueueByElementPosition, - calcElementOriginRectInfo, - originRectInfoToRangeRectInfo -} from '@idraw/util'; - -import { calcVirtualTextDetail } from './text'; - -export function calcVirtualFlatDetail(elem: Element, opts: { tempContext: ViewContext2D }): VirtualFlatDetail { - let virtualDetail: VirtualFlatDetail = {}; - if (elem.type === 'text') { - virtualDetail = calcVirtualTextDetail(elem as Element<'text'>, opts); - } - return virtualDetail; -} - -export function elementsToVirtualFlatMap(elements: Elements, opts: { tempContext: ViewContext2D }): VirtualFlatItemMap { - const virtualFlatMap: VirtualFlatItemMap = {}; - const currentPosition: ElementPosition = []; - - const _walk = (elem: Element) => { - const baseInfo: Omit = { - type: elem.type, - isVisibleInView: true, - position: [...currentPosition] - }; - let originRectInfo: ViewRectInfo | null = null; - - const groupQueue = getGroupQueueByElementPosition(elements, currentPosition); - - originRectInfo = calcElementOriginRectInfo(elem, { - groupQueue: groupQueue || [] - }); - - const virtualItem: VirtualFlatItem = { - ...baseInfo, - ...{ - originRectInfo: originRectInfo as ViewRectInfo, - rangeRectInfo: is.angle(elem.angle) - ? originRectInfoToRangeRectInfo(originRectInfo as ViewRectInfo) - : originRectInfo - }, - ...calcVirtualFlatDetail(elem, opts) - }; - - virtualFlatMap[elem.uuid] = virtualItem; - - if (elem.type === 'group') { - (elem as Element<'group'>).detail.children.forEach((ele, i) => { - currentPosition.push(i); - _walk(ele); - currentPosition.pop(); - }); - } - }; - - elements.forEach((elem, index) => { - currentPosition.push(index); - _walk(elem); - currentPosition.pop(); - }); - - return virtualFlatMap; -} diff --git a/packages/renderer/src/virtual-flat/text.ts b/packages/renderer/src/virtual-flat/text.ts deleted file mode 100644 index d144158..0000000 --- a/packages/renderer/src/virtual-flat/text.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type { Element, CalcVirtualDetailOptions, VirtualFlatTextDetail, VirtualFlatTextLine } from '@idraw/types'; -import { enhanceFontFamliy, getDefaultElementDetailConfig } from '@idraw/util'; - -const detailConfig = getDefaultElementDetailConfig(); - -// TODO -function isTextWidthWithinErrorRange(w0: number, w1: number, scale: number): boolean { - if (scale < 0.5) { - if (w0 < w1 && (w0 - w1) / w0 > -0.15) { - return true; - } - } - return w0 >= w1; -} - -export function calcVirtualTextDetail(elem: Element<'text'>, opts: CalcVirtualDetailOptions): VirtualFlatTextDetail { - const { w, h } = elem; - const x = 0; - const y = 0; - const ctx = opts.tempContext; - - const lines: VirtualFlatTextLine[] = []; - const detail: Element<'text'>['detail'] = { - ...detailConfig, - ...elem.detail - }; - const originFontSize = detail.fontSize || detailConfig.fontSize; - const fontSize = originFontSize; - - if (fontSize < 2) { - return {}; - } - - const originLineHeight = detail.lineHeight || originFontSize; - const lineHeight = originLineHeight; - - ctx.textBaseline = 'top'; - ctx.$setFont({ - fontWeight: detail.fontWeight, - fontSize: fontSize, - fontFamily: enhanceFontFamliy(detail.fontFamily) - }); - let detailText = detail.text.replace(/\r\n/gi, '\n'); - if (detail.textTransform === 'lowercase') { - detailText = detailText.toLowerCase(); - } else if (detail.textTransform === 'uppercase') { - detailText = detailText.toUpperCase(); - } - - const fontHeight = lineHeight; - const detailTextList = detailText.split('\n'); - - let lineNum = 0; - detailTextList.forEach((itemText: string, idx: number) => { - if (detail.minInlineSize === 'maxContent') { - lines.push({ - x, - y: 0, // TODO - text: itemText, - width: ctx.$undoPixelRatio(ctx.measureText(itemText).width) - }); - } else { - let lineText = ''; - let splitStr = ''; - let tempStrList: string[] = itemText.split(splitStr); - if (detail.wordBreak === 'normal') { - splitStr = ' '; - const wordList = itemText.split(splitStr); - tempStrList = []; - wordList.forEach((word: string, idx: number) => { - tempStrList.push(word); - if (idx < wordList.length - 1) { - tempStrList.push(splitStr); - } - }); - } - - if (tempStrList.length === 1 && detail.overflow === 'visible') { - lines.push({ - x, - y: 0, // TODO - text: tempStrList[0], - width: ctx.$undoPixelRatio(ctx.measureText(tempStrList[0]).width) - }); - } else if (tempStrList.length > 0) { - for (let i = 0; i < tempStrList.length; i++) { - if (isTextWidthWithinErrorRange(ctx.$doPixelRatio(w), ctx.measureText(lineText + tempStrList[i]).width, 1)) { - lineText += tempStrList[i] || ''; - } else { - lines.push({ - x, - y: 0, // TODO - text: lineText, - width: ctx.$undoPixelRatio(ctx.measureText(lineText).width) - }); - lineText = tempStrList[i] || ''; - lineNum++; - } - if ((lineNum + 1) * fontHeight > h && detail.overflow === 'hidden') { - break; - } - if (tempStrList.length - 1 === i) { - if ((lineNum + 1) * fontHeight <= h) { - lines.push({ - x, - y: 0, // TODO - text: lineText, - width: ctx.$undoPixelRatio(ctx.measureText(lineText).width) - }); - if (idx < detailTextList.length - 1) { - lineNum++; - } - break; - } - } - } - } else { - lines.push({ - x, - y: 0, // TODO - text: '', - width: 0 - }); - } - } - }); - - let startY = 0; - let eachLineStartY = 0; - if (fontHeight > fontSize) { - eachLineStartY = (fontHeight - fontSize) / 2; - } - if (lines.length * fontHeight < h) { - if (detail.verticalAlign === 'top') { - startY = 0; - } else if (detail.verticalAlign === 'bottom') { - startY += h - lines.length * fontHeight; - } else { - // middle and default - startY += (h - lines.length * fontHeight) / 2; - } - } - - // draw text lines - { - const _y = y + startY; - lines.forEach((line, i) => { - let _x = x; - if (detail.textAlign === 'center') { - _x = x + (w - line.width) / 2; - } else if (detail.textAlign === 'right') { - _x = x + (w - line.width); - } - lines[i].x = _x; - lines[i].y = _y + fontHeight * i + eachLineStartY; - }); - } - - const virtualTextDetail: VirtualFlatTextDetail = { - textLines: lines - }; - return virtualTextDetail; -} diff --git a/packages/renderer/src/virtual/base.ts b/packages/renderer/src/virtual/base.ts new file mode 100644 index 0000000..fe4785a --- /dev/null +++ b/packages/renderer/src/virtual/base.ts @@ -0,0 +1,103 @@ +import type { Point, Material, VirtualBaseAttributes, CalcVirtualAttributesOptions } from '@idraw/types'; +import { limitAngle } from '@idraw/util'; + +interface Rect { + x: number; + y: number; + width: number; + height: number; + angle: number; // Rotation angle around center, 0-360 degrees + children?: Rect[]; +} + +/** + * Calculate the center position of a child rectangle in world coordinates + * @param targetRect The target child rectangle + * @param parents Array of parent rectangles from direct parent to root (in hierarchical order) + * @returns Center point coordinates in world coordinate system + */ +function calculateWorldPosition(targetRect: Rect, parents: Rect[]): Point { + parents = [...parents].reverse(); + // If no parents, return the center point of target rectangle directly + if (!parents || parents.length === 0) { + return { + x: targetRect.x + targetRect.width / 2, + y: targetRect.y + targetRect.height / 2, + }; + } + + // Calculate target rectangle's center in its own coordinate system + let centerX = targetRect.x + targetRect.width / 2; + let centerY = targetRect.y + targetRect.height / 2; + + // Apply transformations from direct parent to root + // parents[0] is direct parent, parents[parents.length-1] is root + for (let i = 0; i < parents.length; i++) { + const parent = parents[i]; + + // Transform the point through this parent's coordinate system + const transformed = applyRectTransformation({ x: centerX, y: centerY }, parent); + + centerX = transformed.x; + centerY = transformed.y; + } + + return { x: centerX, y: centerY }; +} + +/** + * Apply a rectangle's transformation to a point + * @param point The point to transform (in the rectangle's local coordinates) + * @param rect The rectangle defining the transformation + * @returns The transformed point in the rectangle's parent coordinate system + */ +function applyRectTransformation(point: Point, rect: Rect): Point { + // Step 1: Calculate rectangle's center + const rectCenterX = rect.width / 2; + const rectCenterY = rect.height / 2; + + // Step 2: Translate point to rectangle's center-oriented coordinate system + const centeredX = point.x - rectCenterX; + const centeredY = point.y - rectCenterY; + + // Step 3: Apply rotation (if any) + let rotatedX = centeredX; + let rotatedY = centeredY; + + if (typeof rect.angle === 'number' && rect.angle !== 0) { + const angleRad = (rect.angle * Math.PI) / 180; + const cos = Math.cos(angleRad); + const sin = Math.sin(angleRad); + + rotatedX = centeredX * cos - centeredY * sin; + rotatedY = centeredX * sin + centeredY * cos; + } + + // Step 4: Translate back to rectangle's local coordinate system + const localX = rotatedX + rectCenterX; + const localY = rotatedY + rectCenterY; + + // Step 5: Add rectangle's position in its parent's coordinate system + const worldX = localX + rect.x; + const worldY = localY + rect.y; + + return { x: worldX, y: worldY }; +} + +export function calcVirtualBaseAttributes(mtrl: Material, opts: CalcVirtualAttributesOptions): VirtualBaseAttributes { + const { groupQueue = [] } = opts; + // const center = calcMaterialCenter(mtrl); + let worldAngle = mtrl.angle || 0; + + groupQueue.forEach((group) => { + worldAngle += group.angle || 0; + }); + const worldCenter = calculateWorldPosition(mtrl as any, groupQueue as any[]); + + const attributes: VirtualBaseAttributes = { + worldCenter, + worldAngle: limitAngle(worldAngle), + }; + + return attributes; +} diff --git a/packages/renderer/src/virtual/circle.ts b/packages/renderer/src/virtual/circle.ts new file mode 100644 index 0000000..5d05fbb --- /dev/null +++ b/packages/renderer/src/virtual/circle.ts @@ -0,0 +1,66 @@ +import type { StrictMaterial, VirtualRectAttributes, PathCommand, CalcVirtualAttributesOptions } from '@idraw/types'; +import { calcVirtualBaseAttributes } from './base'; + +export function calcVirtualCircleAttributes( + mtrl: StrictMaterial<'circle'>, + opts: CalcVirtualAttributesOptions +): VirtualRectAttributes { + const { dpr } = opts; + const { width, height } = mtrl; + let { r } = mtrl; + + // cx = (cx - x) * dpr; + // cy = (cy - y) * dpr; + + const cx = (width / 2) * dpr; + const cy = (height / 2) * dpr; + + r = r * dpr; + + // Use four cubic Bezier curves to approximate a circle (one for each 90-degree segment) + // Control point distance = (4/3)*tan(π/8) * r ≈ 0.55228475 * r + const c = r * 0.55228475; + + const commands: PathCommand[] = [ + // M - Move to starting point (right side of circle) + { + type: 'M', + params: [cx + r, cy], + }, + + // C - Cubic Bezier curve to top point + { + type: 'C', + params: [cx + r, cy - c, cx + c, cy - r, cx, cy - r], + }, + + // C - Cubic Bezier curve to left point + { + type: 'C', + params: [cx - c, cy - r, cx - r, cy - c, cx - r, cy], + }, + + // C - Cubic Bezier curve to bottom point + { + type: 'C', + params: [cx - r, cy + c, cx - c, cy + r, cx, cy + r], + }, + + // C - Cubic Bezier curve back to starting point + { + type: 'C', + params: [cx + c, cy + r, cx + r, cy + c, cx + r, cy], + }, + + { + type: 'Z', + params: [], + }, + ]; + + const attributes: VirtualRectAttributes = { + ...calcVirtualBaseAttributes(mtrl, opts), + commands, + }; + return attributes; +} diff --git a/packages/renderer/src/virtual/ellipse.ts b/packages/renderer/src/virtual/ellipse.ts new file mode 100644 index 0000000..39db506 --- /dev/null +++ b/packages/renderer/src/virtual/ellipse.ts @@ -0,0 +1,70 @@ +import type { StrictMaterial, VirtualRectAttributes, PathCommand, CalcVirtualAttributesOptions } from '@idraw/types'; +import { calcVirtualBaseAttributes } from './base'; + +export function calcVirtualEllipseAttributes( + mtrl: StrictMaterial<'ellipse'>, + opts: CalcVirtualAttributesOptions +): VirtualRectAttributes { + const { dpr } = opts; + const { width, height } = mtrl; + let { rx, ry } = mtrl; + + // x = x * dpr; + // y = y * dpr; + // cx = cx * dpr; + // cy = cy * dpr; + const cx = (width / 2) * dpr; + const cy = (height / 2) * dpr; + rx = rx * dpr; + ry = ry * dpr; + // height = height * dpr; + // width = width * dpr; + + // Magic number for Bezier curve approximation of ellipses + // This is derived from the circle approximation constant adjusted for ellipse + const kx = 0.55228475 * rx; + const ky = 0.55228475 * ry; + + const commands: PathCommand[] = [ + // M - Move to right point + { + type: 'M', + params: [cx + rx, cy], + }, + + // C - Cubic Bezier curve to top point + { + type: 'C', + params: [cx + rx, cy - ky, cx + kx, cy - ry, cx, cy - ry], + }, + + // C - Cubic Bezier curve to left point + { + type: 'C', + params: [cx - kx, cy - ry, cx - rx, cy - ky, cx - rx, cy], + }, + + // C - Cubic Bezier curve to bottom point + { + type: 'C', + params: [cx - rx, cy + ky, cx - kx, cy + ry, cx, cy + ry], + }, + + // C - Cubic Bezier curve back to starting point + { + type: 'C', + params: [cx + kx, cy + ry, cx + rx, cy + ky, cx + rx, cy], + }, + + { + type: 'Z', + params: [], + }, + ]; + + const attributes: VirtualRectAttributes = { + ...calcVirtualBaseAttributes(mtrl, opts), + commands, + }; + return attributes; +} diff --git a/packages/renderer/src/virtual/index.ts b/packages/renderer/src/virtual/index.ts new file mode 100644 index 0000000..06378e1 --- /dev/null +++ b/packages/renderer/src/virtual/index.ts @@ -0,0 +1,118 @@ +import { + StrictMaterial, + MaterialPosition, + BoundingInfo, + VirtualItemMap, + VirtualItem, + VirtualAttributes, + ViewContext2D, + CalcVirtualAttributesOptions, +} from '@idraw/types'; +import { + is, + getGroupQueueByMaterialPosition, + calcMaterialBoundingInfo, + boundingInfoToRangeBoundingInfo, +} from '@idraw/util'; + +import { calcVirtualBaseAttributes } from './base'; +import { calcVirtualRectAttributes } from './rect'; +import { calcVirtualCircleAttributes } from './circle'; +import { calcVirtualEllipseAttributes } from './ellipse'; +import { calcVirtualTextAttributes } from './text'; +import { calcVirtualPathAttributes } from './path'; + +export function calcVirtualAttributes( + mtrl: StrictMaterial, + opts: CalcVirtualAttributesOptions +): VirtualAttributes | null { + let attributes: VirtualAttributes | null = null; + + switch (mtrl.type) { + case 'rect': { + attributes = calcVirtualRectAttributes(mtrl as StrictMaterial<'rect'>, opts); + break; + } + case 'circle': { + attributes = calcVirtualCircleAttributes(mtrl as StrictMaterial<'circle'>, opts); + break; + } + case 'ellipse': { + attributes = calcVirtualEllipseAttributes(mtrl as StrictMaterial<'ellipse'>, opts); + break; + } + case 'text': { + attributes = calcVirtualTextAttributes(mtrl as StrictMaterial<'text'>, opts); + break; + } + case 'group': { + attributes = calcVirtualRectAttributes(mtrl as StrictMaterial<'rect'>, opts); + break; + } + case 'path': { + attributes = calcVirtualPathAttributes(mtrl as StrictMaterial<'path'>, opts); + break; + } + default: { + attributes = calcVirtualBaseAttributes(mtrl, opts); + break; + } + } + + return attributes; +} + +export function materialsToVirtualFlatMap( + materials: StrictMaterial[], + opts: { tempContext: ViewContext2D; dpr: number } +): VirtualItemMap { + const virtualFlatMap: VirtualItemMap = {}; + const currentPosition: MaterialPosition = []; + + const _walk = (mtrl: StrictMaterial) => { + const baseInfo: Omit = { + type: mtrl.type, + isVisibleInView: true, + position: [...currentPosition], + }; + let boundingInfo: BoundingInfo | null = null; + + const groupQueue = getGroupQueueByMaterialPosition(materials, currentPosition); + + boundingInfo = calcMaterialBoundingInfo(mtrl, { + groupQueue: groupQueue || [], + }); + + const virtualItem: VirtualItem = { + ...baseInfo, + ...{ + boundingInfo: boundingInfo as BoundingInfo, + rangeBoundingInfo: is.angle(mtrl.angle) + ? boundingInfoToRangeBoundingInfo(boundingInfo as BoundingInfo) + : boundingInfo, + }, + ...calcVirtualAttributes(mtrl, { + ...opts, + groupQueue: groupQueue || [], + }), + }; + + virtualFlatMap[mtrl.id] = virtualItem; + + if (mtrl.type === 'group') { + (mtrl as StrictMaterial<'group'>).children.forEach((ele, i) => { + currentPosition.push(i); + _walk(ele); + currentPosition.pop(); + }); + } + }; + + materials.forEach((mtrl, index) => { + currentPosition.push(index); + _walk(mtrl); + currentPosition.pop(); + }); + + return virtualFlatMap; +} diff --git a/packages/renderer/src/virtual/path.ts b/packages/renderer/src/virtual/path.ts new file mode 100644 index 0000000..cb1ebde --- /dev/null +++ b/packages/renderer/src/virtual/path.ts @@ -0,0 +1,17 @@ +import { StrictMaterial, VirtualPathAttributes, CalcVirtualAttributesOptions } from '@idraw/types'; +import { convertPathCommandsToACLMZ, scalePathCommands } from '@idraw/util'; +import { calcVirtualBaseAttributes } from './base'; + +export function calcVirtualPathAttributes( + mtrl: StrictMaterial<'path'>, + opts: CalcVirtualAttributesOptions +): VirtualPathAttributes { + const { dpr } = opts; + const attributes: VirtualPathAttributes = { ...calcVirtualBaseAttributes(mtrl, opts), anchorCommands: [] }; + const anchorCommands = convertPathCommandsToACLMZ(mtrl.commands || []); + + attributes.anchorCommands = anchorCommands; + attributes.commands = scalePathCommands(mtrl.commands, dpr, dpr); + + return attributes; +} diff --git a/packages/renderer/src/virtual/rect.ts b/packages/renderer/src/virtual/rect.ts new file mode 100644 index 0000000..55a096d --- /dev/null +++ b/packages/renderer/src/virtual/rect.ts @@ -0,0 +1,180 @@ +import type { StrictMaterial, VirtualRectAttributes, PathCommand, CalcVirtualAttributesOptions } from '@idraw/types'; +import { createId } from '@idraw/util'; +import { calcVirtualBaseAttributes } from './base'; + +export function calcVirtualRectAttributes( + mtrl: StrictMaterial<'rect' | 'text'>, + opts: CalcVirtualAttributesOptions +): VirtualRectAttributes { + const { dpr } = opts; + let { + // x, + // y, + width, + height, + rx = 0, + ry = 0, + } = mtrl as StrictMaterial<'rect'>; + const { cornerRadius } = mtrl; + // x = x * dpr; + // y = y * dpr; + const x = 0; + const y = 0; + height = height * dpr; + width = width * dpr; + + const commands: PathCommand[] = []; + + // tlRX ----- trRX + // tlRY trRY + // | | + // blRY brRY + // blRX ----- brRX + + let tlRX = 0; + let trRX = 0; + let brRX = 0; + let blRX = 0; + let tlRY = 0; + let trRY = 0; + let brRY = 0; + let blRY = 0; + + if (typeof rx === 'number' && typeof ry === 'number') { + rx = rx * dpr; + ry = ry * dpr; + tlRX = rx; + trRX = rx; + brRX = rx; + blRX = rx; + tlRY = ry; + trRY = ry; + brRY = ry; + blRY = ry; + } else if (Array.isArray(cornerRadius) && cornerRadius.length === 4) { + const crs = cornerRadius.map((r) => r * dpr); + tlRX = crs[0] || 0; + trRX = crs[1] || 0; + brRX = crs[2] || 0; + blRX = crs[3] || 0; + tlRY = crs[0] || 0; + trRY = crs[1] || 0; + brRY = crs[2] || 0; + blRY = crs[3] || 0; + } else if (typeof cornerRadius === 'number') { + const cr = cornerRadius * dpr; + tlRX = cr; + trRX = cr; + brRX = cr; + blRX = cr; + tlRY = cr; + trRY = cr; + brRY = cr; + blRY = cr; + } + + // if (Array.isArray(cornerRadius)) { + const x0 = x; + const y0 = y; + const x1 = x + width; + const y1 = y + height; + + // M x+rx, y + commands.push({ + id: createId(), + type: 'M', + params: [x + tlRX, y], + }); + + // Top edge and top right elliptical corner + if (trRX > 0 || trRY > 0) { + commands.push({ + id: createId(), + type: 'L', + params: [x1 - trRX, y0], + }); + commands.push({ + id: createId(), + type: 'A', + params: [trRX, trRY, 0, 0, 1, x1, y0 + trRY], + }); + } else { + commands.push({ + id: createId(), + type: 'L', + params: [x1, y0], + }); + } + + // Right edge and bottom right elliptical corner + if (brRX > 0 || brRY > 0) { + commands.push({ + id: createId(), + type: 'L', + params: [x1, y1 - brRY], + }); + commands.push({ + id: createId(), + type: 'A', + params: [brRX, brRY, 0, 0, 1, x1 - brRX, y1], + }); + } else { + commands.push({ + id: createId(), + type: 'L', + params: [x1, y1], + }); + } + + // Bottom edge and bottom left elliptical corner + if (blRX > 0 || blRY > 0) { + commands.push({ + id: createId(), + type: 'L', + params: [x0 + blRX, y1], + }); + commands.push({ + id: createId(), + type: 'A', + params: [blRX, blRY, 0, 0, 1, x0, y1 - blRY], + }); + } else { + commands.push({ + id: createId(), + type: 'L', + params: [x0, y1], + }); + } + + // Left edge and top left elliptical corner + if (tlRX > 0 || tlRY > 0) { + commands.push({ + id: createId(), + type: 'L', + params: [x0, y0 + tlRY], + }); + commands.push({ + id: createId(), + type: 'A', + params: [tlRX, tlRY, 0, 0, 1, x0 + tlRX, y0], + }); + } else { + commands.push({ + id: createId(), + type: 'L', + params: [x0, y0], + }); + } + + commands.push({ + id: createId(), + type: 'Z', + params: [], + }); + + const attributes: VirtualRectAttributes = { + ...calcVirtualBaseAttributes(mtrl, opts), + commands, + }; + return attributes; +} diff --git a/packages/renderer/src/virtual/text.ts b/packages/renderer/src/virtual/text.ts new file mode 100644 index 0000000..93c8515 --- /dev/null +++ b/packages/renderer/src/virtual/text.ts @@ -0,0 +1,195 @@ +import type { + StrictMaterial, + CalcVirtualAttributesOptions, + VirtualTextAttributes, + VirtualTextLine, +} from '@idraw/types'; +import { enhanceFontFamliy, getDefaultMaterialAttributes } from '@idraw/util'; +import { calcVirtualRectAttributes } from './rect'; + +const attributesConfig = getDefaultMaterialAttributes(); + +// TODO +function isTextWidthWithinErrorRange(w0: number, w1: number, scale: number): boolean { + if (scale < 0.5) { + if (w0 < w1 && (w0 - w1) / w0 > -0.15) { + return true; + } + } + return w0 >= w1; +} + +export function calcVirtualTextAttributes( + mtrl: StrictMaterial<'text'>, + opts: CalcVirtualAttributesOptions +): VirtualTextAttributes { + const { width, height } = mtrl; + const x = 0; + const y = 0; + const ctx = opts.tempContext; + + const lines: VirtualTextLine[] = []; + const attributes: StrictMaterial<'text'> = { + ...attributesConfig, + ...mtrl, + }; + const originFontSize = attributes.fontSize || attributesConfig.fontSize; + const fontSize = originFontSize; + const baseAttrs = calcVirtualRectAttributes(mtrl, opts); + + if (fontSize < 2) { + return { ...baseAttrs, textLines: [] }; + } + + const originLineHeight = attributes.lineHeight || originFontSize; + const lineHeight = originLineHeight; + + ctx.textBaseline = 'top'; + ctx.$setFont({ + fontWeight: attributes.fontWeight, + fontSize: fontSize, + fontFamily: enhanceFontFamliy(attributes.fontFamily), + }); + let attributesText = attributes.text.replace(/\r\n/gi, '\n'); + if (attributes.textTransform === 'lowercase') { + attributesText = attributesText.toLowerCase(); + } else if (attributes.textTransform === 'uppercase') { + attributesText = attributesText.toUpperCase(); + } + + const fontHeight = lineHeight; + const attributesTextList = attributesText.split('\n'); + + let lineNum = 0; + attributesTextList.forEach((itemText: string, idx: number) => { + if (attributes.minInlineSize === 'maxContent') { + const measureResult = ctx.measureText(itemText); + lines.push({ + x, + y: 0, + text: itemText, + width: ctx.$undoPixelRatio(measureResult.width), + }); + } else { + let lineText = ''; + let splitStr = ''; + let tempCharList: string[] = itemText.split(splitStr); + + if (attributes.wordBreak === 'normal') { + splitStr = ' '; + const wordList = itemText.split(splitStr); + tempCharList = []; + wordList.forEach((word: string, idx: number) => { + tempCharList.push(word); + if (idx < wordList.length - 1) { + tempCharList.push(splitStr); + } + }); + } + + if (tempCharList.length === 1 && attributes.overflow !== 'hidden') { + lines.push({ + x, + y: 0, + text: tempCharList[0], + width: ctx.$undoPixelRatio(ctx.measureText(tempCharList[0]).width), + }); + } else if (tempCharList.length > 0) { + for (let i = 0; i < tempCharList.length; i++) { + if ( + isTextWidthWithinErrorRange(ctx.$doPixelRatio(width), ctx.measureText(lineText + tempCharList[i]).width, 1) + ) { + lineText += tempCharList[i] || ''; + } else { + lines.push({ + x, + y: 0, + text: lineText, + width: ctx.$undoPixelRatio(ctx.measureText(lineText).width), + }); + lineText = tempCharList[i] || ''; + lineNum++; + } + + if (lineNum * fontHeight >= height) { + if (attributes.overflow === 'hidden') { + lineText = ''; + break; + } + } + if (tempCharList.length - 1 === i) { + if ((lineNum + 1) * fontHeight <= height) { + lines.push({ + x, + y: 0, + text: lineText, + width: ctx.$undoPixelRatio(ctx.measureText(lineText).width), + }); + lineText = ''; + if (idx < attributesTextList.length - 1) { + lineNum++; + } + break; + } + } + } + + if (lineText) { + lines.push({ + x, + y: 0, + text: lineText, + width: ctx.$undoPixelRatio(ctx.measureText(lineText).width), + }); + // eslint-disable-next-line no-useless-assignment + lineText = ''; + } + } else { + lines.push({ + x, + y: 0, + text: '', + width: 0, + }); + } + } + }); + + let startY = 0; + let eachLineStartY = 0; + if (fontHeight > fontSize) { + eachLineStartY = (fontHeight - fontSize) / 2; + } + if (lines.length * fontHeight < height) { + if (attributes.verticalAlign === 'top') { + startY = 0; + } else if (attributes.verticalAlign === 'bottom') { + startY += height - lines.length * fontHeight; + } else { + // middle and default + startY += (height - lines.length * fontHeight) / 2; + } + } + + // draw text lines + { + const _y = y + startY; + lines.forEach((line, i) => { + let _x = x; + if (attributes.textAlign === 'center') { + _x = x + (width - line.width) / 2; + } else if (attributes.textAlign === 'right') { + _x = x + (width - line.width); + } + lines[i].x = _x; + lines[i].y = _y + fontHeight * i + eachLineStartY; + }); + } + + const virtualTextAttributes: VirtualTextAttributes = { + ...baseAttrs, + textLines: lines, + }; + + return virtualTextAttributes; +} diff --git a/packages/renderer/src/view-visible/index.ts b/packages/renderer/src/visible/index.ts similarity index 50% rename from packages/renderer/src/view-visible/index.ts rename to packages/renderer/src/visible/index.ts index 286f3b9..1b7760c 100644 --- a/packages/renderer/src/view-visible/index.ts +++ b/packages/renderer/src/visible/index.ts @@ -1,87 +1,93 @@ -import { Elements, ViewScaleInfo, ViewSizeInfo, ViewRectInfo, VirtualFlatItemMap, ViewContext2D } from '@idraw/types'; -import { calcElementCenter } from '@idraw/util'; -import { elementsToVirtualFlatMap } from '../virtual-flat'; +import { StrictMaterial, ViewScaleInfo, ViewSizeInfo, BoundingInfo, VirtualItemMap, ViewContext2D } from '@idraw/types'; +import { calcMaterialCenter } from '@idraw/util'; +import { materialsToVirtualFlatMap } from '../virtual'; -export function sortElementsViewVisiableInfoMap( - elements: Elements, +export function sortMaterialsViewVisiableInfoMap( + materials: StrictMaterial[], opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo; tempContext: ViewContext2D; } ): { - virtualFlatItemMap: VirtualFlatItemMap; + virtualItemMap: VirtualItemMap; visibleCount: number; invisibleCount: number; } { const { viewScaleInfo, viewSizeInfo, tempContext } = opts; - const visibleInfoMap: VirtualFlatItemMap = elementsToVirtualFlatMap(elements, { tempContext }); - return updateVirtualFlatItemMapStatus(visibleInfoMap, { viewScaleInfo, viewSizeInfo }); + const visibleInfoMap: VirtualItemMap = materialsToVirtualFlatMap(materials, { + tempContext, + dpr: viewSizeInfo.devicePixelRatio, + }); + return updateVirtualItemMapStatus(visibleInfoMap, { viewScaleInfo, viewSizeInfo }); } -function isRangeRectInfoCollide(info1: ViewRectInfo, info2: ViewRectInfo): boolean { +function isRangeBoundingBoxCollide(info1: BoundingInfo, info2: BoundingInfo): boolean { + const centerX = info1.center.x; + const centerY = info1.center.y; const rect1MinX = Math.min(info1.topLeft.x, info1.topRight.x, info1.bottomLeft.x, info1.bottomRight.x); const rect1MaxX = Math.max(info1.topLeft.x, info1.topRight.x, info1.bottomLeft.x, info1.bottomRight.x); const rect1MinY = Math.min(info1.topLeft.y, info1.topRight.y, info1.bottomLeft.y, info1.bottomRight.y); const rect1MaxY = Math.max(info1.topLeft.y, info1.topRight.y, info1.bottomLeft.y, info1.bottomRight.y); - const rect2MinX = Math.min(info2.topLeft.x, info2.topRight.x, info2.bottomLeft.x, info2.bottomRight.x); - const rect2MaxX = Math.max(info2.topLeft.x, info2.topRight.x, info2.bottomLeft.x, info2.bottomRight.x); - const rect2MinY = Math.min(info2.topLeft.y, info2.topRight.y, info2.bottomLeft.y, info2.bottomRight.y); - const rect2MaxY = Math.max(info2.topLeft.y, info2.topRight.y, info2.bottomLeft.y, info2.bottomRight.y); + const w = Math.abs(rect1MaxX - rect1MinX); + const h = Math.abs(rect1MaxY - rect1MinY); - if ( - (rect1MinX <= rect2MaxX && rect1MaxX >= rect2MinX && rect1MinY <= rect2MaxY && rect1MaxY >= rect2MinY) || - (rect2MaxX <= rect1MaxY && rect2MaxX >= rect1MaxY && rect2MaxX <= rect1MaxY && rect2MaxX >= rect1MaxY) - ) { + const rect2MinX = Math.min(info2.topLeft.x, info2.topRight.x, info2.bottomLeft.x, info2.bottomRight.x) - w; + const rect2MaxX = Math.max(info2.topLeft.x, info2.topRight.x, info2.bottomLeft.x, info2.bottomRight.x) + w; + const rect2MinY = Math.min(info2.topLeft.y, info2.topRight.y, info2.bottomLeft.y, info2.bottomRight.y) - h; + const rect2MaxY = Math.max(info2.topLeft.y, info2.topRight.y, info2.bottomLeft.y, info2.bottomRight.y) + h; + + if (centerX >= rect2MinX && centerX <= rect2MaxX && centerY >= rect2MinY && centerY <= rect2MaxY) { return true; } return false; } -// function logVirtualFlatItemMapStatus(virtualFlatItemMap: VirtualFlatItemMap) { +// function logVirtualItemMapStatus(virtualItemMap: VirtualItemMap) { // console.log('------------------------------------------------'); -// Object.keys(virtualFlatItemMap).forEach((uuid) => { -// const item = virtualFlatItemMap[uuid]; -// const info = item.originRectInfo; +// Object.keys(virtualItemMap).forEach((id) => { +// const item = virtualItemMap[id]; +// const info = item.boundingInfo; // const rect = { // x: info.topLeft.x, // y: info.topRight.y, // w: info.bottomRight.x - info.topLeft.x, -// h: info.bottomRight.y - info.topLeft.y +// h: info.bottomRight.y - info.topLeft.y, // }; -// console.log('view: ', uuid, item.isVisibleInView, rect); +// console.log('view: ', id, item.isVisibleInView, rect); // }); // } -export function updateVirtualFlatItemMapStatus( - virtualFlatItemMap: VirtualFlatItemMap, +export function updateVirtualItemMapStatus( + virtualItemMap: VirtualItemMap, opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo } ): { - virtualFlatItemMap: VirtualFlatItemMap; + virtualItemMap: VirtualItemMap; visibleCount: number; invisibleCount: number; } { - const canvasRectInfo = calcVisibleOriginCanvasRectInfo(opts); + const canvasBoundingBox = calcVisibleOriginCanvasBoundingBox(opts); let visibleCount = 0; let invisibleCount = 0; - Object.keys(virtualFlatItemMap).forEach((uuid) => { - const info = virtualFlatItemMap[uuid]; - info.isVisibleInView = isRangeRectInfoCollide(info.rangeRectInfo, canvasRectInfo); + Object.keys(virtualItemMap).forEach((id) => { + const info = virtualItemMap[id]; + info.isVisibleInView = isRangeBoundingBoxCollide(info.rangeBoundingInfo, canvasBoundingBox); // eslint-disable-next-line @typescript-eslint/no-unused-expressions info.isVisibleInView ? visibleCount++ : invisibleCount++; }); - // logVirtualFlatItemMapStatus(virtualFlatItemMap); + // TODO + // logVirtualItemMapStatus(virtualItemMap); - return { virtualFlatItemMap, visibleCount, invisibleCount }; + return { virtualItemMap, visibleCount, invisibleCount }; } -export function calcVisibleOriginCanvasRectInfo(opts: { +export function calcVisibleOriginCanvasBoundingBox(opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo; -}): ViewRectInfo { +}): BoundingInfo { const { viewScaleInfo, viewSizeInfo } = opts; const { scale, offsetTop, offsetLeft } = viewScaleInfo; const { width, height } = viewSizeInfo; @@ -91,7 +97,7 @@ export function calcVisibleOriginCanvasRectInfo(opts: { const w = width / scale; const h = height / scale; - const center = calcElementCenter({ x, y, w, h }); + const center = calcMaterialCenter({ x, y, width, height }); const topLeft = { x, y }; const topRight = { x: x + w, y }; const bottomLeft = { x, y: y + h }; @@ -100,7 +106,7 @@ export function calcVisibleOriginCanvasRectInfo(opts: { const top = { x: center.x, y }; const right = { x: x + w, y: center.y }; const bottom = { x: center.x, y: y + h }; - const rectInfo: ViewRectInfo = { + const boundingBox: BoundingInfo = { center, topLeft, topRight, @@ -109,9 +115,9 @@ export function calcVisibleOriginCanvasRectInfo(opts: { left, top, right, - bottom + bottom, }; - return rectInfo; + return boundingBox; } -// export function isInVisiableView(rangeRectInfo: ViewRectInfo) {} +// export function isInVisiableView(rangeBoundingInfo: BoundingInfo) {} diff --git a/packages/types/package.json b/packages/types/package.json index 43738e9..d0e85bb 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@idraw/types", - "version": "0.4.0", + "version": "1.0.0", "description": "", "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/types/src/lib/board.ts b/packages/types/src/board.ts similarity index 88% rename from packages/types/src/lib/board.ts rename to packages/types/src/board.ts index 3b61e2b..ed7cf43 100644 --- a/packages/types/src/lib/board.ts +++ b/packages/types/src/board.ts @@ -1,9 +1,10 @@ -import type { Point, PointSize } from './point'; +import type { Point } from './point'; import type { BoardContent, ViewCalculator, ViewScaleInfo, ViewSizeInfo } from './view'; import type { UtilEventEmitter } from './util'; import type { ActiveStore, StoreSharer } from './store'; import type { RendererEventMap, RendererOptions, RendererDrawOptions, RendererLoader } from './renderer'; import type { Data } from './data'; +import type { PointWatcherEvent } from './watcher'; export type BoardBaseEventMap = { loadSource: void; @@ -11,23 +12,17 @@ export type BoardBaseEventMap = { export type BoardExtendEventMap = BoardBaseEventMap & Record; -export interface BoardWatcherPointEvent { - point: Point; -} - -export interface BoardWatherWheelXEvent { +export type BoardWatcherPointEvent = PointWatcherEvent; +export type BoardWatherWheelXEvent = PointWatcherEvent & { deltaX: number; - point: Point; -} -export interface BoardWatherWheelYEvent { +}; +export type BoardWatherWheelYEvent = PointWatcherEvent & { deltaY: number; - point: Point; -} -export interface BoardWatherWheelEvent { +}; +export type BoardWatherWheelEvent = PointWatcherEvent & { deltaX: number; deltaY: number; - point: Point; -} +}; export type BoardWatherWheelScaleEvent = BoardWatherWheelXEvent & BoardWatherWheelYEvent; export interface BoardWatherDrawFrameEvent> { @@ -48,6 +43,7 @@ export interface BoardWatcherEventMap = any> pointMove: BoardWatcherPointEvent; pointEnd: BoardWatcherPointEvent; pointLeave: BoardWatcherPointEvent; + click: BoardWatcherPointEvent; doubleClick: BoardWatcherPointEvent; contextMenu: BoardWatcherPointEvent; wheel: BoardWatherWheelEvent; @@ -72,6 +68,7 @@ export interface BoardMiddlewareObject = any pointMove?: (e: BoardWatcherEventMap['pointMove']) => void | boolean; pointEnd?: (e: BoardWatcherEventMap['pointEnd']) => void | boolean; pointLeave?: (e: BoardWatcherEventMap['pointLeave']) => void | boolean; + click?: (e: BoardWatcherEventMap['click']) => void | boolean; doubleClick?: (e: BoardWatcherEventMap['doubleClick']) => void | boolean; contextMenu?: (e: BoardWatcherEventMap['contextMenu']) => void | boolean; wheel?: (e: BoardWatcherEventMap['wheel']) => void | boolean; @@ -90,7 +87,7 @@ export interface BoardMiddlewareObject = any export interface BoardMiddlewareOptions< S extends Record = Record, - E extends BoardExtendEventMap = BoardExtendEventMap + E extends BoardExtendEventMap = BoardExtendEventMap, > { boardContent: BoardContent; sharer: StoreSharer; @@ -104,7 +101,7 @@ export interface BoardMiddlewareOptions< export type BoardMiddleware< S extends Record = any, E extends BoardExtendEventMap = BoardExtendEventMap, - C extends any = undefined + C extends any = undefined, > = (opts: BoardMiddlewareOptions, config?: C) => BoardMiddlewareObject; export interface BoardOptions { @@ -132,12 +129,12 @@ export interface BoardViewerOptions { } // export interface BoardViewerStorage { -// virtualFlatItemMap: VirtualFlatItemMap; +// virtualItemMap: VirtualItemMap; // } export interface BoardViewer extends UtilEventEmitter { drawFrame(): void; - scale(opts: { scale: number; point: PointSize; ignoreUpdateVisibleStatus?: boolean }): { + scale(opts: { scale: number; point: Point; ignoreUpdateVisibleStatus?: boolean }): { moveX: number; moveY: number; }; @@ -159,10 +156,10 @@ export interface BoardWatcherOptions { disabled?: boolean; boardContent: BoardContent; sharer: StoreSharer>; + container?: HTMLDivElement; } export interface BoardWatcherStore { hasPointDown: boolean; inCanvas: boolean; - prevClickPoint: Point | null; } diff --git a/packages/types/src/bounding.ts b/packages/types/src/bounding.ts new file mode 100644 index 0000000..6ba169c --- /dev/null +++ b/packages/types/src/bounding.ts @@ -0,0 +1,27 @@ +import type { Point } from '@idraw/types'; + +/** + * Represents the bounding box of an SVG path + */ +export interface BoundingBox { + minX: number; // Minimum X coordinate (left edge) + minY: number; // Minimum Y coordinate (top edge) + maxX: number; // Maximum X coordinate (right edge) + maxY: number; // Maximum Y coordinate (bottom edge) + width: number; // Width of the bounding box + height: number; // Height of the bounding box + start: Point; // Top-left corner of the bounding box + end: Point; // Bottom-right corner of the bounding box +} + +export type BoundingInfo = { + topLeft: Point; + topRight: Point; + bottomRight: Point; + bottomLeft: Point; + top: Point; + right: Point; + bottom: Point; + left: Point; + center: Point; +}; diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts new file mode 100644 index 0000000..069e587 --- /dev/null +++ b/packages/types/src/config.ts @@ -0,0 +1,26 @@ +import type { MaterialBaseAttributes, MaterialTextAttributes, MaterialImageAttributes } from './material'; + +export type DefaultMaterialAttributes = Required< + Omit< + MaterialBaseAttributes, + 'name' | 'transform' | 'cornerRadius' + // | 'shadowOffsetX' + // | 'shadowOffsetY' + // | 'shadowBlur' + // | 'shadowColor' + // | 'opacity' + // | 'boxSizing' + // | 'color' + // | 'textAlign' + // | 'verticalAlign' + // | 'fontSize' + // | 'lineHeight' + // | 'fontFamily' + // | 'fontWeight' + // | 'minInlineSize' + // | 'wordBreak' + // | 'overflow' + > +> & + Pick, 'text'> & + Pick, 'href'>; diff --git a/packages/types/src/lib/context2d.ts b/packages/types/src/context2d.ts similarity index 71% rename from packages/types/src/lib/context2d.ts rename to packages/types/src/context2d.ts index b543584..a99fcea 100644 --- a/packages/types/src/lib/context2d.ts +++ b/packages/types/src/context2d.ts @@ -33,6 +33,16 @@ export interface ViewContext2D { arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void; bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void; quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void; + ellipse( + x: number, + y: number, + radiusX: number, + radiusY: number, + rotation: number, + startAngle: number, + endAngle: number, + counterclockwise?: boolean + ): void; lineWidth: number; getLineDash(): number[]; setLineDash(segments: number[]): void; @@ -83,9 +93,59 @@ export interface ViewContext2D { clip(fillRule?: CanvasFillRule): void; clip(path: Path2D, fillRule?: CanvasFillRule): void; lineCap: CanvasLineCap; + lineJoin: CanvasLineJoin; + lineDashOffset: number; + miterLimit: number; setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void; getTransform(): DOMMatrix2DInit; createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient; createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient; createConicGradient(startAngle: number, x: number, y: number): CanvasGradient; } + +export type Context2DMoveToParams = { + x: number; + y: number; +}; + +export type Context2DBezierCurveToParams = { + cp1x: number; + cp1y: number; + cp2x: number; + cp2y: number; + x: number; + y: number; +}; + +export type Context2DEllipseParams = { + centerX: number; + centerY: number; + radiusX: number; + radiusY: number; + rotation: number; // Radian + startRadian: number; // Radian + endRadian: number; // Radian + anticlockwise: boolean; +}; + +export type Context2DBezierCurveCommand = { + id: string; + name: 'bezierCurveTo'; + params: Context2DBezierCurveToParams; +}; + +export type Context2DMoveToCommand = { id: string; name: 'moveTo'; params: Context2DMoveToParams }; + +export type Context2DBeginPathCommand = { id: string; name: 'beginPath'; params: null }; + +export type Context2DClosePathCommand = { id: string; name: 'closePath'; params: null }; + +export type Context2DEllipseCommand = { id: string; name: 'ellipse'; params: Context2DEllipseParams }; + +// TODO +export type Context2DCommand = + | Context2DBeginPathCommand + | Context2DClosePathCommand + | Context2DBezierCurveCommand + | Context2DMoveToCommand + | Context2DEllipseCommand; diff --git a/packages/types/src/controller.ts b/packages/types/src/controller.ts new file mode 100644 index 0000000..89e84cf --- /dev/null +++ b/packages/types/src/controller.ts @@ -0,0 +1,49 @@ +import { ViewRectVertexes } from './view'; +import { Point } from './point'; +import { MaterialSize } from './material'; + +export type MaterialSizeControllerType = + | 'left' + | 'right' + | 'top' + | 'bottom' + | 'top-left' + | 'top-right' + | 'bottom-left' + | 'bottom-right' + | 'left-middle' + | 'right-middle' + | 'top-middle' + | 'bottom-middle' + | 'rotate'; + +export interface MaterialSizeControllerItem { + type: MaterialSizeControllerType; + vertexes: ViewRectVertexes; + center: Point; + size: number; +} + +export interface MaterialSizeController { + originalMaterialCenter: Point; + originalMaterialSize: MaterialSize; + materialWrapper: ViewRectVertexes; + top: MaterialSizeControllerItem; + bottom: MaterialSizeControllerItem; + left: MaterialSizeControllerItem; + right: MaterialSizeControllerItem; + topLeft: MaterialSizeControllerItem; + topRight: MaterialSizeControllerItem; + bottomLeft: MaterialSizeControllerItem; + bottomRight: MaterialSizeControllerItem; + topMiddle: MaterialSizeControllerItem; + bottomMiddle: MaterialSizeControllerItem; + leftMiddle: MaterialSizeControllerItem; + rightMiddle: MaterialSizeControllerItem; + rotate: MaterialSizeControllerItem; +} + +export type LayoutSizeController = Omit< + MaterialSizeController, + 'rotate' | 'materialWrapper' | 'originalMaterialCenter' | 'originalMaterialSize' +>; diff --git a/packages/types/src/lib/core.ts b/packages/types/src/core.ts similarity index 52% rename from packages/types/src/lib/core.ts rename to packages/types/src/core.ts index 62c3038..2494eac 100644 --- a/packages/types/src/lib/core.ts +++ b/packages/types/src/core.ts @@ -1,8 +1,8 @@ -import type { Element, ElementSize, ElementType, ElementPosition } from './element'; -import type { ViewScaleInfo } from './view'; +import type { StrictMaterial, MaterialSize, MaterialType, MaterialPosition } from './material'; import type { Data } from './data'; import type { BoardBaseEventMap } from './board'; import type { ModifyType, ModifyRecord } from './modify'; +import type { IDrawMode } from './mode'; export interface CoreOptions { width: number; @@ -26,69 +26,86 @@ export type CursorType = export interface CoreEventCursor { type?: CursorType | string | null; - groupQueue?: Element<'group'>[]; - element?: Element | ElementSize | null; + groupQueue?: StrictMaterial<'group'>[]; + material?: StrictMaterial | MaterialSize | null; } // export interface CoreEventSelect { -// uuids: string[]; +// ids: string[]; // positions?: Array>; // } export interface CoreEventChange { data: Data; - type: T | 'setData' | 'other' | string; - selectedElements?: Element[] | null; - hoverElement?: Element | null; + type: T | 'setData' | 'updatingMaterial' | 'other' | string; + selectedMaterials?: StrictMaterial[] | null; + hoverMaterial?: StrictMaterial | null; modifyRecord?: ModifyRecord | null; } export interface CoreEventScale { scale: number; } -type CoreEventTextEdit = { - element: Element<'text'>; - position: ElementPosition; - groupQueue: Element<'group'>[]; - viewScaleInfo: ViewScaleInfo; +export type CoreEventTextEdit = { + id: string; +}; + +export type CoreEventPathEdit = { + id?: string; }; type CoreEventTextChange = { - element: { - uuid: string; - detail: { + material: { + id: string; + attributes: { text: string; }; }; - position: ElementPosition; + position: MaterialPosition; }; type CoreEventContextMenu = { pointerContainer: HTMLDivElement; - selectedElements: Element[]; + selectedMaterials: StrictMaterial[]; }; export type CoreEventMap = BoardBaseEventMap & { + // basic cursor: CoreEventCursor; change: CoreEventChange; + changing: CoreEventChange; ruler: { show: boolean; showGrid: boolean }; scale: { scale: number }; + modeChange: { mode: IDrawMode }; + + // create middleware + create: { type: Exclude }; + clearCreate: void; + + // select middleware select: { - uuids?: string[]; - positions?: ElementPosition[]; + ids?: string[]; + positions?: MaterialPosition[]; type?: | 'clickCanvas' - | 'selectElement' - | 'selectElements' - | 'selectElementByPosition' - | 'selectElementsByPositions' + | 'selectMaterial' + | 'selectMaterials' + | 'selectMaterialByPosition' + | 'selectMaterialsByPositions' | 'other' | string; }; - selectLayout: void; // TODO - clearSelect: { uuids?: string[] } | void; + selectLayout: void; + clearSelect: { ids?: string[] } | void; + selectInGroup: { enable: boolean }; + contextMenu: CoreEventContextMenu; + snapToGrid: { enable: boolean }; + // text middleware textEdit: CoreEventTextEdit; textChange: CoreEventTextChange; - contextMenu: CoreEventContextMenu; - selectInGroup: { enable: boolean }; - snapToGrid: { enable: boolean }; + // path editor middleware + pathEdit: CoreEventPathEdit; + clearPathEdit: void; + // path creator middleware + pathCreate: void; + clearPathCreate: void; }; diff --git a/packages/types/src/data.ts b/packages/types/src/data.ts new file mode 100644 index 0000000..6073e0a --- /dev/null +++ b/packages/types/src/data.ts @@ -0,0 +1,45 @@ +import type { Material, MaterialAssets, MaterialSize, MaterialGroupAttributes } from './material'; + +export type DataLayout = Pick & + Pick & { + operations?: { + position?: 'absolute' | 'relative'; + }; + }; + +export interface DataGlobal { + fill?: string; +} + +export type Data = { + name?: string; + materials: Material[]; + assets?: MaterialAssets; + layout?: DataLayout; + global?: DataGlobal; +}; + +export type Matrix = [ + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, + number, +]; + +export type ColorMatrix = Matrix; diff --git a/packages/types/src/dom.ts b/packages/types/src/dom.ts new file mode 100644 index 0000000..e432aee --- /dev/null +++ b/packages/types/src/dom.ts @@ -0,0 +1,124 @@ +export interface HTMLCSSProps { + // color + color?: string; + backgroundColor?: string; + stroke?: string; + borderTopColor?: string; + borderRightColor?: string; + borderBottomColor?: string; + borderLeftColor?: string; + outlineColor?: string; + textShadow?: string; + + // size + width?: string | number; + height?: string | number; + minWidth?: string | number; + maxWidth?: string | number; + minHeight?: string | number; + maxHeight?: string | number; + padding?: string | number; + margin?: string | number; + cornerRadius?: string | number; + fontSize?: string | number; + lineHeight?: string | number; + letterSpacing?: string | number; + + // layout + display?: 'block' | 'inline' | 'inline-module' | 'flex' | 'inline-flex' | 'grid' | 'none'; + position?: 'static' | 'relative' | 'absolute' | 'fixed' | 'sticky'; + top?: string | number; + right?: string | number; + bottom?: string | number; + left?: string | number; + zIndex?: string | number; + flex?: string | number; + flexDirection?: 'row' | 'row-reverse' | 'column' | 'column-reverse'; + justifyContent?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly'; + alignItems?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline'; + alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline'; + gridTemplateColumns?: string; + gridTemplateRows?: string; + gridColumn?: string; + gridRow?: string; + + // border + border?: string; + borderTop?: string; + borderRight?: string; + borderBottom?: string; + borderLeft?: string; + strokeWidth?: string | number; + borderStyle?: string; + + // bg + background?: string; + backgroundImage?: string; + backgroundPosition?: string; + backgroundSize?: string; + backgroundRepeat?: 'repeat' | 'no-repeat' | 'repeat-x' | 'repeat-y'; + + // text + fontFamily?: string; + fontWeight?: string | number; + fontStyle?: 'normal' | 'italic' | 'oblique'; + textAlign?: 'left' | 'right' | 'center' | 'justify'; + textTransform?: 'uppercase' | 'lowercase' | 'capitalize' | 'none'; + textDecoration?: 'none' | 'underline' | 'line-through' | 'overline'; + textStroke?: string; + '-webkit-text-stroke'?: string; + whiteSpace?: 'normal' | 'nowrap' | 'pre' | 'pre-wrap' | 'pre-line'; + + // animation + transition?: string; + transitionDuration?: string; + transitionTimingFunction?: string; + transitionDelay?: string; + animation?: string; + + // other + opacity?: string | number; + cursor?: string; + boxSizing?: 'content-box' | 'border-box' | 'inherit'; + overflow?: 'visible' | 'hidden' | 'scroll' | 'auto'; + visibility?: 'visible' | 'hidden'; + clip?: string; + boxShadow?: string; + outline?: string; + transform?: string; + transformOrigin?: string; + userSelect?: 'auto' | 'none' | 'text' | 'contain' | 'all'; + + wordBreak?: string; +} + +export type HTMLProps = { + // Global attributes (applicable to all HTML materials) + id?: string; // Specifies a unique id for the material + className?: string; // Specifies one or more class names + style?: HTMLCSSProps; // Inline CSS styles + title?: string; // Tooltip text displayed when hovering over the material + hidden?: boolean; // Specifies whether the material is hidden + tabindex?: number; // Specifies the tab order of the material + draggable?: boolean; // Specifies whether the material is draggable + contenteditable?: boolean | 'true' | 'false'; // Specifies whether the content is editable + spellcheck?: boolean | 'true' | 'false'; // Specifies whether spellchecking is enabled + lang?: string; // Specifies the language of the material's content + dir?: 'ltr' | 'rtl' | 'auto'; // Specifies the text direction + accesskey?: string; // Specifies a shortcut key to activate or focus the material + + // Attributes specific to certain HTML materials + href?: string; // URL for , , + src?: string; // URL for ,