diff --git a/src/components/draw-app/properties/LinkProperty.vue b/src/components/draw-app/properties/LinkProperty.vue index bb555ac..0aa8412 100644 --- a/src/components/draw-app/properties/LinkProperty.vue +++ b/src/components/draw-app/properties/LinkProperty.vue @@ -5,7 +5,7 @@ outlined v-model.number="linkModel.lineWidth" type="number" - @blur="onUpdate" + @blur="onFormUpdate" label="线宽" lazy-rules :rules="[(val) => (val && val > 0) || '画布宽必须大于0']" @@ -14,7 +14,7 @@ @@ -50,7 +50,7 @@ outlined v-model.number="linkModel.segmentsCount" type="number" - @blur="onUpdate" + @blur="onFormUpdate" label="曲线分段数量" lazy-rules :rules="[(val) => (val && val > 0) || '曲线分段数量必须大于0']" @@ -62,34 +62,41 @@ import { LinkData } from 'src/examples/app/graphics/LinkInteraction'; import { Link } from 'src/graphics/link/Link'; import { useDrawStore } from 'src/stores/draw-store'; -import { onMounted, reactive, watch } from 'vue'; +import { onMounted, onUnmounted, reactive, watch } from 'vue'; const drawStore = useDrawStore(); const linkModel = reactive(new LinkData()); drawStore.$subscribe; -watch( - () => drawStore.selectedGraphic, - (val) => { - if (val && val.type == Link.Type) { - // console.log('link变更'); - linkModel.copyFrom(val.saveData() as LinkData); - } - } -); +// watch( +// () => drawStore.selectedGraphic, +// (val) => { +// console.log('监控'); +// if (val && val.type == Link.Type) { +// console.log('link变更'); +// linkModel.copyFrom(val.saveData() as LinkData); +// } +// } +// ); onMounted(() => { // console.log('link 属性表单 mounted'); - const link = drawStore.selectedGraphic as Link; - if (link) { - linkModel.copyFrom(link.saveData()); - } + drawStore.bindFormData(linkModel); + // const link = drawStore.selectedGraphic as Link; + // if (link) { + // linkModel.copyFrom(link.saveData()); + // } }); -function onUpdate() { - console.log('link 属性更新'); +onUnmounted(() => { + console.log('link 属性表单 unmounted'); + drawStore.unbindFormData(linkModel); +}); + +function onFormUpdate() { const link = drawStore.selectedGraphic as Link; if (link) { + console.log('link 属性更新', link); drawStore.getDrawApp().updateGraphicAndRecord(link, linkModel); } } diff --git a/src/components/draw-app/properties/SignalProperty.vue b/src/components/draw-app/properties/SignalProperty.vue index f2688bd..0046733 100644 --- a/src/components/draw-app/properties/SignalProperty.vue +++ b/src/components/draw-app/properties/SignalProperty.vue @@ -15,7 +15,7 @@ import { SignalData } from 'src/examples/app/graphics/SignalInteraction'; import { Signal } from 'src/graphics/signal/Signal'; import { useDrawStore } from 'src/stores/draw-store'; -import { onMounted, reactive, ref, watch } from 'vue'; +import { onMounted, onUnmounted, reactive, ref, watch } from 'vue'; const drawStore = useDrawStore(); const signalModel = reactive(new SignalData()); @@ -40,6 +40,7 @@ watch( ); onMounted(() => { + drawStore.bindFormData(signalModel); const signal = drawStore.selectedGraphic as Signal; if (signal) { signalModel.copyFrom(signal.saveData()); @@ -47,6 +48,10 @@ onMounted(() => { } }); +onUnmounted(() => { + drawStore.unbindFormData(signalModel); +}); + function onUpdate() { signalModel.direction = (directionSelect as never)[signalDirection.value]; const signal = drawStore.selectedGraphic as Signal; diff --git a/src/jlgraphic/app/JlDrawApp.ts b/src/jlgraphic/app/JlDrawApp.ts index 1fe6eab..25ef784 100644 --- a/src/jlgraphic/app/JlDrawApp.ts +++ b/src/jlgraphic/app/JlDrawApp.ts @@ -22,7 +22,15 @@ import { } from '../plugins'; import { CommonMouseTool } from '../plugins/CommonMousePlugin'; import { MenuItemOptions } from '../ui/Menu'; -import { DOWN, LEFT, RIGHT, UP, recursiveChildren } from '../utils'; +import { + DOWN, + DebouncedFunction, + LEFT, + RIGHT, + UP, + debounce, + recursiveChildren, +} from '../utils'; import { GraphicDataUpdateOperation, UpdateCanvasOperation, @@ -233,6 +241,16 @@ export interface IDrawApp extends IGraphicApp { * @param data */ updateGraphicAndRecord(g: JlGraphic, data: GraphicData): void; + /** + * 绑定form表单对象 + * @param form + */ + bindFormData(form: GraphicData): void; + /** + * 解绑form表单对象 + * @param form + */ + unbindFormData(form: GraphicData): void; } /** @@ -258,6 +276,8 @@ export class JlDrawApp extends GraphicApp implements IDrawApp { drawAssistants: DrawAssistant[] = []; _drawing = false; + private debouncedFormDataUpdator: DebouncedFunction<(g: JlGraphic) => void>; + get drawing(): boolean { return this._drawing; } @@ -274,6 +294,9 @@ export class JlDrawApp extends GraphicApp implements IDrawApp { this.appOperationRecord(); // 绑定通用键盘操作 this.bindKeyboardOperation(); + this.formDataSyncListen(); + + this.debouncedFormDataUpdator = debounce(this.doFormDataUpdate, 60); } setOptions(options: DrawAppOptions): void { @@ -484,6 +507,57 @@ export class JlDrawApp extends GraphicApp implements IDrawApp { graphic.eventMode = 'static'; graphic.selectable = true; graphic.draggable = true; + graphic.on('repaint', () => { + this.handleFormDataUpdate(graphic); + }); + graphic.on('transformend', () => { + this.handleFormDataUpdate(graphic); + }); + } + + formData: GraphicData | undefined = undefined; + + /** + * 绑定form表单对象 + * @param form + */ + bindFormData(form: GraphicData): void { + this.formData = form; + if (this.selectedGraphics.length == 1) { + this.formData.copyFrom(this.selectedGraphics[0].saveData()); + } + } + + /** + * 移除form绑定 + * @param form + */ + unbindFormData(form: GraphicData): void { + if (this.formData == form) { + this.formData = undefined; + } + } + + private formDataSyncListen(): void { + this.on('graphicselected', () => { + if (this.selectedGraphics.length == 1) { + this.handleFormDataUpdate(this.selectedGraphics[0]); + } + }); + } + + /** + * 处理表单数据更新(使用debounce限流) + */ + private handleFormDataUpdate(g: JlGraphic): void { + this.debouncedFormDataUpdator(this, g); + } + + private doFormDataUpdate(g: JlGraphic): void { + if (this.selectedGraphics.length > 1) return; + if (this.formData && g.type === this.formData.graphicType) { + this.formData.copyFrom(g.saveData()); + } } updateCanvasAndRecord(data: ICanvasProperties) { diff --git a/src/jlgraphic/app/JlGraphicApp.ts b/src/jlgraphic/app/JlGraphicApp.ts index b47fab1..afc67d5 100644 --- a/src/jlgraphic/app/JlGraphicApp.ts +++ b/src/jlgraphic/app/JlGraphicApp.ts @@ -46,6 +46,7 @@ import { } from '../plugins/KeyboardPlugin'; import { ContextMenu, ContextMenuPlugin } from '../ui/ContextMenu'; import { MenuItemOptions } from '../ui/Menu'; +import { DebouncedFunction, debounce } from '../utils'; import { getRectangleCenter, recursiveChildren } from '../utils/GraphicUtils'; import { GraphicCreateOperation, @@ -317,6 +318,7 @@ export interface GraphicAppEvents extends GlobalMixins.GraphicAppEvents { 'options-update': [options: GraphicAppOptions]; // 配置更新 graphicselectedchange: [graphic: JlGraphic, selected: boolean]; graphicchildselectedchange: [child: DisplayObject, selected: boolean]; + graphicselected: [graphics: JlGraphic[]]; 'viewport-scaled': [vp: Viewport]; drag_op_start: [event: AppDragEvent]; drag_op_move: [event: AppDragEvent]; @@ -509,11 +511,6 @@ export interface IGraphicScene extends EventEmitter { * @param graphics */ updateSelected(...graphics: JlGraphic[]): void; - /** - * 发布选中对象改变事件 - * @param graphic - */ - fireSelectedChange(graphic: JlGraphic): void; /** * 选中所有图形 */ @@ -563,6 +560,8 @@ abstract class GraphicSceneBase menuPlugin: ContextMenuPlugin; // 菜单插件 + private debounceEmitFunc: DebouncedFunction<() => void>; + wsMsgBroker: AppWsMsgBroker; // websocket消息代理 constructor(options: GraphicAppOptions) { super(); @@ -628,6 +627,11 @@ abstract class GraphicSceneBase this.menuPlugin = new ContextMenuPlugin(this); this.wsMsgBroker = new AppWsMsgBroker(this); + + this.debounceEmitFunc = debounce(this.doEmitAppGraphicSelected, 50); + this.on('graphicselectedchange', () => { + this.debounceEmitFunc(this); + }); } abstract get app(): GraphicApp; @@ -883,12 +887,10 @@ abstract class GraphicSceneBase // graphic可能是vue的Proxy对象,会导致canvas删除时因不是同一个对象而无法从画布移除 const g = this.graphicStore.deleteGraphics(graphic); if (g) { + // 清除选中 + g.updateSelected(false); // 从画布移除 this.canvas.removeGraphic(g); - // 清除选中 - if (g.updateSelected(false)) { - this.fireSelectedChange(g); - } // 对象删除处理 g.onDelete(); this.emit('graphicdeleted', g); @@ -936,15 +938,6 @@ abstract class GraphicSceneBase this.updateSelected(...this.queryStore.getAllGraphics()); } - /** - * 发送选中变化事件 - * @param graphic - */ - fireSelectedChange(graphic: JlGraphic) { - // console.log('通知选中变化', this.selecteds) - const select = graphic.selected; - this.emit('graphicselectedchange', graphic, select); - } /** * 更新选中 */ @@ -955,16 +948,19 @@ abstract class GraphicSceneBase } if (graphic.selected) { graphic.updateSelected(false); - this.fireSelectedChange(graphic); } }); graphics.forEach((graphic) => { - if (graphic.updateSelected(true)) { - this.fireSelectedChange(graphic); - } + graphic.updateSelected(true); }); } + private doEmitAppGraphicSelected(): void { + // 场景发布图形选中 + this.emit('graphicselected', this.selectedGraphics); + // this.app.emit('graphicselected', this.selectedGraphics); + } + /** * 更新画布 * @param param diff --git a/src/jlgraphic/core/JlGraphic.ts b/src/jlgraphic/core/JlGraphic.ts index 3891196..595bf59 100644 --- a/src/jlgraphic/core/JlGraphic.ts +++ b/src/jlgraphic/core/JlGraphic.ts @@ -642,6 +642,10 @@ export abstract class JlGraphic extends Container { this.removeAllChildSelected(); this.emit('unselected', this); } + const app = this.getGraphicApp(); + if (app) { + app.emit('graphicselectedchange', this, this.selected); + } } hasSelectedChilds(): boolean { @@ -678,11 +682,16 @@ export abstract class JlGraphic extends Container { }); } fireChildSelected(child: DisplayObject) { - if (child.selected) { + const selected = child.selected; + if (selected) { this.emit('childselected', child); } else { this.emit('childunselected', child); } + const app = this.getGraphicApp(); + if (app) { + app.emit('graphicchildselectedchange', child, selected); + } } exitChildEdit() { this.childEdit = false; diff --git a/src/jlgraphic/plugins/CommonMousePlugin.ts b/src/jlgraphic/plugins/CommonMousePlugin.ts index 76993ff..7ff07e0 100644 --- a/src/jlgraphic/plugins/CommonMousePlugin.ts +++ b/src/jlgraphic/plugins/CommonMousePlugin.ts @@ -271,7 +271,6 @@ export class CommonMouseTool extends AppInteractionPlugin { graphic.invertChildSelected(target); } else { graphic.invertSelected(); - app.fireSelectedChange(graphic); } } } else { diff --git a/src/jlgraphic/plugins/GraphicTransformPlugin.ts b/src/jlgraphic/plugins/GraphicTransformPlugin.ts index fcd259e..17b3b02 100644 --- a/src/jlgraphic/plugins/GraphicTransformPlugin.ts +++ b/src/jlgraphic/plugins/GraphicTransformPlugin.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { Container, DisplayObject, @@ -17,9 +18,11 @@ import { JlGraphic } from '../core'; import { AbsorbablePosition, VectorText } from '../graphic'; import { DraggablePoint } from '../graphic/DraggablePoint'; import { + DebouncedFunction, angleToAxisx, calculateLineMidpoint, convertRectangleToPolygonPoints, + debounce, distance, recursiveChildren, } from '../utils'; @@ -298,6 +301,8 @@ export class GraphicTransformPlugin extends InteractionPluginBase { ap.tryAbsorb(...targets); } } + // const start = new Date().getTime(); + // 事件发布 targets.forEach((target) => { if (target.shiftStartPoint && target.shiftLastPoint) { @@ -314,6 +319,8 @@ export class GraphicTransformPlugin extends InteractionPluginBase { ); } }); + // const dt = new Date().getTime() - start; + // console.log('拖拽耗时', `${dt}ms`, targets); } } } @@ -847,11 +854,14 @@ export class BoundsGraphic extends Graphics { alpha: 1, }; obj: DisplayObject; + debouncedRedraw: DebouncedFunction<() => void>; constructor(graphic: DisplayObject) { super(); this.obj = graphic; this.name = BoundsGraphic.Name; this.visible = false; + + this.debouncedRedraw = debounce(this.doRedraw, 50); this.obj.on('transformstart', this.onObjTransformStart, this); this.obj.on('transformend', this.onObjTransformEnd, this); if (this.obj.children && this.obj.children.length > 0) { @@ -880,7 +890,6 @@ export class BoundsGraphic extends Graphics { onGraphicRepaint(): void { if (this.visible) { this.redraw(); - this.visible = true; } } @@ -892,8 +901,13 @@ export class BoundsGraphic extends Graphics { } redraw() { + this.debouncedRedraw(this); + } + doRedraw() { + const visible = this.visible; this.visible = false; // 屏蔽包围框本身 const bounds = new Polygon(this.obj.localBoundsToCanvasPoints()); this.clear().lineStyle(BoundsGraphic.BoundsLineStyle).drawShape(bounds); + this.visible = visible; } } diff --git a/src/jlgraphic/utils/debounce.ts b/src/jlgraphic/utils/debounce.ts new file mode 100644 index 0000000..27310c7 --- /dev/null +++ b/src/jlgraphic/utils/debounce.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export interface DebouncedFunction any> { + (context: ThisParameterType, ...args: Parameters): void; + cancel: () => void; +} + +export function debounce) => any>( + fn: F, + waitMs = 250 +): DebouncedFunction { + let timeoutId: ReturnType | undefined; + + const debouncedFunction = function ( + context: ThisParameterType, + ...args: Parameters + ) { + const invokeFunction = function () { + timeoutId = undefined; + fn.apply(context, args); + }; + + if (timeoutId !== undefined) { + console.debug('debounce clear timeout', fn); + clearTimeout(timeoutId); + } + + timeoutId = setTimeout(invokeFunction, waitMs); + }; + + debouncedFunction.cancel = function () { + if (timeoutId !== undefined) { + clearTimeout(timeoutId); + } + }; + + return debouncedFunction; +} diff --git a/src/jlgraphic/utils/index.ts b/src/jlgraphic/utils/index.ts index 57ec464..f4c9aa6 100644 --- a/src/jlgraphic/utils/index.ts +++ b/src/jlgraphic/utils/index.ts @@ -3,6 +3,8 @@ import { Point, Rectangle } from 'pixi.js'; export * from './GraphicUtils'; export * from './IntersectUtils'; +export * from './debounce'; + export const UP: Point = new Point(0, -1); export const DOWN: Point = new Point(0, 1); export const LEFT: Point = new Point(-1, 0); diff --git a/src/stores/draw-store.ts b/src/stores/draw-store.ts index 96fc96f..b4877a4 100644 --- a/src/stores/draw-store.ts +++ b/src/stores/draw-store.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia'; import { destroyDrawApp, getDrawApp, initDrawApp } from 'src/examples/app'; -import { DrawAssistant, IDrawApp, JlGraphic } from 'src/jlgraphic'; +import { DrawAssistant, GraphicData, IDrawApp, JlGraphic } from 'src/jlgraphic'; import { IJlCanvas } from 'src/jlgraphic/app/JlGraphicApp'; export const useDrawStore = defineStore('draw', { @@ -52,6 +52,22 @@ export const useDrawStore = defineStore('draw', { getJlCanvas(): IJlCanvas { return this.getDrawApp().canvas; }, + bindFormData(form: GraphicData): void { + console.log('绑定form数据', form); + const app = this.getDrawApp(); + app.bindFormData(form); + // app.app.on('graphicselectedchange', () => { + // if (app.selectedGraphics.length == 1) { + // const g = app.selectedGraphics[0]; + // if (g.type === form.graphicType) { + // form.copyFrom(g.saveData()); + // } + // } + // }); + }, + unbindFormData(form: GraphicData): void { + console.log('取消绑定form数据', form); + }, initDrawApp() { const app = initDrawApp(); app.on('interaction-plugin-resume', (plugin) => { @@ -63,7 +79,8 @@ export const useDrawStore = defineStore('draw', { } } }); - app.on('graphicselectedchange', () => { + app.on('graphicselected', () => { + console.log('批量选中事件', app.selectedGraphics.length); this.selectedGraphics = app.selectedGraphics; }); this.selectedGraphics = [];