diff --git a/src/components/draw-app/DrawProperties.vue b/src/components/draw-app/DrawProperties.vue index 7230a41..349ff48 100644 --- a/src/components/draw-app/DrawProperties.vue +++ b/src/components/draw-app/DrawProperties.vue @@ -33,6 +33,7 @@ diff --git a/src/components/draw-app/properties/CanvasProperty.vue b/src/components/draw-app/properties/CanvasProperty.vue new file mode 100644 index 0000000..839ad02 --- /dev/null +++ b/src/components/draw-app/properties/CanvasProperty.vue @@ -0,0 +1,80 @@ + + + diff --git a/src/drawApp/drawApp.ts b/src/drawApp/drawApp.ts index 8d74b20..065f0ce 100644 --- a/src/drawApp/drawApp.ts +++ b/src/drawApp/drawApp.ts @@ -13,6 +13,9 @@ import { useDrawStore } from 'src/stores/draw-store'; import { iscsGraphicData } from 'src/protos/iscs_graphic_data'; import { toStorageTransform } from './graphics/GraphicDataBase'; import { fromUint8Array } from 'js-base64'; +import { Arrow, ArrowTemplate } from 'src/graphics/arrow/Arrow'; +import { ArrowData } from './graphics/ArrowInteraction'; +import { ArrowDraw } from 'src/graphics/arrow/ArrowDrawAssistant'; // import { Notify } from 'quasar'; @@ -82,23 +85,24 @@ export function initDrawApp(): IDrawApp { isSupportDeletion: isSupportDeletion, }); const app = drawApp; - // app.canvas.on('_rightclick', (e) => { - // if (app.drawing) return; - // UndoOptions.disabled = !app.opRecord.hasUndo; - // RedoOptions.disabled = !app.opRecord.hasRedo; + new ArrowDraw(drawApp, new ArrowTemplate(new ArrowData())); + app.canvas.on('_rightclick', (e) => { + if (app.drawing) return; + UndoOptions.disabled = !app.opRecord.hasUndo; + RedoOptions.disabled = !app.opRecord.hasRedo; - // UndoOptions.handler = () => { - // app.opRecord.undo(); - // }; - // RedoOptions.handler = () => { - // app.opRecord.redo(); - // }; - // SelectAllOptions.handler = () => { - // app.selectAllGraphics(); - // }; - // DefaultCanvasMenu.open(e.global); - // }); - // app.on('destroy', async () => {}); + UndoOptions.handler = () => { + app.opRecord.undo(); + }; + RedoOptions.handler = () => { + app.opRecord.redo(); + }; + SelectAllOptions.handler = () => { + app.selectAllGraphics(); + }; + DefaultCanvasMenu.open(e.global); + }); + app.on('destroy', async () => {}); app.addKeyboardListener( new KeyListener({ value: 'KeyS', @@ -136,6 +140,10 @@ export function saveDrawDatas(app: IDrawApp) { viewportTransform: toStorageTransform(canvasData.viewportTransform), }); const graphics = app.queryStore.getAllGraphics(); + if (Arrow.Type === g.type) { + const arrowData = (g as Arrow).saveData(); + storage.arrows.push((arrowData as ArrowData).data); + } console.log(storage, '保存数据', graphics); const base64 = fromUint8Array(storage.serialize()); return base64; diff --git a/src/drawApp/graphics/ArrowInteraction.ts b/src/drawApp/graphics/ArrowInteraction.ts new file mode 100644 index 0000000..bff1dbc --- /dev/null +++ b/src/drawApp/graphics/ArrowInteraction.ts @@ -0,0 +1,48 @@ +import * as pb_1 from 'google-protobuf'; +// import { IArrowData, Arrow } from 'src/graphics/arrow/Arrow'; +import { iscsGraphicData } from 'src/protos/iscs_graphic_data'; +import { IArrowData, Arrow } from 'src/graphics/arrow/Arrow'; +import { GraphicDataBase } from './GraphicDataBase'; +import { IPointData } from 'pixi.js'; + +export class ArrowData extends GraphicDataBase implements IArrowData { + constructor(data?: iscsGraphicData.Arrow) { + let arrow; + if (data) { + arrow = data; + } else { + arrow = new iscsGraphicData.Arrow({ + common: GraphicDataBase.defaultCommonInfo(Arrow.Type), + }); + } + super(arrow); + } + + public get data(): iscsGraphicData.Arrow { + return this.getData(); + } + + get code(): string { + return this.data.code; + } + set code(v: string) { + this.data.code = v; + } + get points(): IPointData[] { + return this.data.points; + } + set points(points: IPointData[]) { + this.data.points = points.map( + (p) => new iscsGraphicData.Point({ x: p.x, y: p.y }) + ); + } + clone(): ArrowData { + return new ArrowData(this.data.cloneMessage()); + } + copyFrom(data: ArrowData): void { + pb_1.Message.copyInto(data.data, this.data); + } + eq(other: ArrowData): boolean { + return pb_1.Message.equals(this.data, other.data); + } +} diff --git a/src/graphics/arrow/Arrow.ts b/src/graphics/arrow/Arrow.ts new file mode 100644 index 0000000..155491b --- /dev/null +++ b/src/graphics/arrow/Arrow.ts @@ -0,0 +1,92 @@ +import { Graphics, IPointData } from 'pixi.js'; +import { + GraphicData, + JlGraphic, + JlGraphicTemplate, + ILineGraphic, +} from 'jl-graphic'; + +export interface IArrowData extends GraphicData { + get code(): string; // 编号 + set code(v: string); + get points(): IPointData[]; // 线坐标点 + set points(points: IPointData[]); + clone(): IArrowData; + copyFrom(data: IArrowData): void; + eq(other: IArrowData): boolean; +} + +export const ArrowConsts = { + lineColor: '#0000CD', + lineWidth: 5, +}; + +export class Arrow extends JlGraphic implements ILineGraphic { + static Type = 'Arrow'; + lineGraphic: Graphics; + arrowGraphic: Graphics; + transformSave: boolean; + + constructor() { + super(Arrow.Type); + this.lineGraphic = new Graphics(); + this.arrowGraphic = new Graphics(); + this.transformSave = true; + this.addChild(this.lineGraphic); + this.addChild(this.arrowGraphic); + } + + doRepaint() { + if (this.datas.points.length < 2) { + throw new Error('Arrow坐标数据异常'); + } + + this.lineGraphic.clear(); + this.lineGraphic.lineStyle(ArrowConsts.lineWidth, ArrowConsts.lineColor); + const p1 = this.datas.points[0]; + const p2 = this.datas.points[1]; + this.lineGraphic.moveTo(p1.x, p1.y); + this.lineGraphic.lineTo(p2.x, p2.y); + this.arrowGraphic.clear(); + this.arrowGraphic.beginFill(ArrowConsts.lineColor, 1); + if (this.arrowGraphic.drawRegularPolygon) { + let roation = Math.PI / 2; + const angle = Math.atan2(p1.y - p2.y, p1.x - p2.x); + if (angle > Math.PI / 2) { + roation = angle - Math.PI / 2; + } else if (angle <= Math.PI / 2 && angle > 0) { + roation = Math.PI / 2 - angle; + } else { + roation = angle - Math.PI / 2; + } + this.arrowGraphic.drawRegularPolygon(p2.x, p2.y, 10, 3, roation); + } + this.arrowGraphic.endFill(); + } + + get code(): string { + return this.datas.code; + } + + get datas(): IArrowData { + return this.getDatas(); + } + + get linePoints(): IPointData[] { + return this.datas.points; + } + set linePoints(points: IPointData[]) { + const old = this.datas.clone(); + old.points = points; + this.updateData(old); + } +} + +export class ArrowTemplate extends JlGraphicTemplate { + constructor(dataTemplate: IArrowData) { + super(Arrow.Type, { dataTemplate }); + } + new() { + return new Arrow(); + } +} diff --git a/src/graphics/arrow/ArrowDrawAssistant.ts b/src/graphics/arrow/ArrowDrawAssistant.ts new file mode 100644 index 0000000..845d3a4 --- /dev/null +++ b/src/graphics/arrow/ArrowDrawAssistant.ts @@ -0,0 +1,204 @@ +import { + DraggablePoint, + IGraphicApp, + GraphicDrawAssistant, + GraphicInteractionPlugin, + GraphicTransformEvent, + IDrawApp, + JlGraphic, + linePoint, + AbsorbablePosition, + AbsorbableLine, + ILineGraphic, + PolylineEditPlugin, +} from 'jl-graphic'; +import { IArrowData, Arrow, ArrowConsts, ArrowTemplate } from './Arrow'; +import { + DisplayObject, + FederatedMouseEvent, + Graphics, + IHitArea, + Point, +} from 'pixi.js'; + +export class ArrowDraw extends GraphicDrawAssistant { + points: Point[] = []; + lineGraphic = new Graphics(); + arrowGraphic = new Graphics(); + + constructor(app: IDrawApp, template: ArrowTemplate) { + super(app, template, 'call_made', '箭头Arrow'); + this.container.addChild(this.lineGraphic); + this.container.addChild(this.arrowGraphic); + + ArrowPointEditPlugin.init(app, this); + } + + bind(): void { + super.bind(); + } + unbind(): void { + super.unbind(); + } + + onLeftDown(e: FederatedMouseEvent): void { + const { x, y } = this.toCanvasCoordinates(e.global); + const p = new Point(x, y); + this.points.push(p); + if (this.points.length === 2) { + this.createAndStore(true); + } + } + + onRightClick(): void { + this.finish(); + return; + } + + onEsc(): void { + this.finish(); + return; + } + + redraw(p: Point): void { + if (this.points.length < 1) return; + const p1 = this.points[0]; + this.lineGraphic.clear(); + this.lineGraphic.lineStyle(ArrowConsts.lineWidth, ArrowConsts.lineColor); + this.lineGraphic.moveTo(p1.x, p1.y); + this.lineGraphic.lineTo(p.x, p.y); + this.arrowGraphic.clear(); + this.arrowGraphic.beginFill(ArrowConsts.lineColor, 1); + if (this.arrowGraphic.drawRegularPolygon) { + let roation = Math.PI / 2; + const angle = Math.atan2(p1.y - p.y, p1.x - p.x); + if (angle > Math.PI / 2) { + roation = angle - Math.PI / 2; + } else if (angle <= Math.PI / 2 && angle >= 0) { + roation = Math.PI / 2 - angle; + } else { + roation = angle - Math.PI / 2; + } + this.arrowGraphic.drawRegularPolygon(p.x, p.y, 10, 3, roation); + } + this.arrowGraphic.endFill(); + } + + prepareData(data: IArrowData): boolean { + if (this.points.length < 2) { + console.log('Arrow绘制因点不够取消绘制'); + return false; + } + data.points = this.points; + return true; + } + + clearCache(): void { + this.points = []; + this.lineGraphic.clear(); + this.arrowGraphic.clear(); + } +} + +export class ArrowGraphicHitArea implements IHitArea { + arrow: Arrow; + constructor(arrow: Arrow) { + this.arrow = arrow; + } + contains(x: number, y: number): boolean { + const p1 = this.arrow.datas.points[0]; + const p2 = this.arrow.datas.points[1]; + if (linePoint(p1, p2, { x, y }, ArrowConsts.lineWidth)) { + return true; + } + return false; + } +} + +function buildAbsorbablePositions(arrow: Arrow): AbsorbablePosition[] { + const aps: AbsorbablePosition[] = []; + + const arrows = arrow.queryStore.queryByType(Arrow.Type); + const canvas = arrow.getCanvas(); + arrows.forEach((other) => { + if (other.id === arrow.id) { + return; + } + const [p1, p2] = [ + other.localToCanvasPoint(other.datas.points[0]), + other.localToCanvasPoint(other.datas.points[1]), + ]; + const ala = new AbsorbableLine( + new Point(p1.x, 0), + new Point(p1.x, canvas.height) + ); + aps.push(ala); + const alb = new AbsorbableLine( + new Point(p2.x, 0), + new Point(p2.x, canvas.height) + ); + aps.push(alb); + }); + return aps; +} + +function onEditPointCreate(g: ILineGraphic, dp: DraggablePoint): void { + const arrow = g as Arrow; + dp.on('transformstart', (e: GraphicTransformEvent) => { + if (e.isShift()) { + arrow.getGraphicApp().setOptions({ + absorbablePositions: buildAbsorbablePositions(arrow), + }); + } + }); +} + +export class ArrowPointEditPlugin extends GraphicInteractionPlugin { + static Name = 'ArrowPointDrag'; + drawAssistant: ArrowDraw; + + constructor(app: IGraphicApp, da: ArrowDraw) { + super(ArrowPointEditPlugin.Name, app); + this.drawAssistant = da; + } + static init(app: IGraphicApp, da: ArrowDraw) { + return new ArrowPointEditPlugin(app, da); + } + filter(...grahpics: JlGraphic[]): Arrow[] | undefined { + return grahpics.filter((g) => g.type == Arrow.Type) as Arrow[]; + } + bind(g: Arrow): void { + g.eventMode = 'static'; + g.cursor = 'pointer'; + g.hitArea = new ArrowGraphicHitArea(g); + g.transformSave = true; + g.on('selected', this.onSelected, this); + g.on('unselected', this.onUnselected, this); + } + unbind(g: Arrow): void { + g.off('selected', this.onSelected, this); + g.off('unselected', this.onUnselected, this); + } + + onContextMenu(e: FederatedMouseEvent) { + const target = e.target as DisplayObject; + const arrow = target.getGraphic() as Arrow; + this.app.updateSelected(arrow); + } + + onSelected(g: DisplayObject): void { + const arrow = g as Arrow; + const lep = new PolylineEditPlugin(arrow, { onEditPointCreate }); + arrow.addAssistantAppend(lep); + lep.showAll(); + } + onUnselected(g: DisplayObject): void { + const arrow = g as Arrow; + const lep = arrow.getAssistantAppend( + PolylineEditPlugin.Name + ); + if (lep) { + lep.hideAll(); + } + } +} diff --git a/src/layouts/DrawLayout.vue b/src/layouts/DrawLayout.vue index 27c6fdc..da6e055 100644 --- a/src/layouts/DrawLayout.vue +++ b/src/layouts/DrawLayout.vue @@ -111,6 +111,7 @@ import { useRoute, useRouter } from 'vue-router'; import { successNotify } from 'src/utils/CommonNotify'; import { useQuasar } from 'quasar'; import { useDrawStore } from 'src/stores/draw-store'; +import { Arrow } from 'src/graphics/arrow/Arrow'; const $q = useQuasar(); const route = useRoute(); @@ -195,7 +196,7 @@ onMounted(() => { } else { drawStore.setDraftId(null); } - const drawAssistantsTypes = []; + const drawAssistantsTypes = [Arrow.Type]; drawAssistantsTypes.forEach((type) => { const drawAssistant = drawStore.getDrawApp().getDrawAssistant(type); if (drawAssistant) { diff --git a/src/stores/draw-store.ts b/src/stores/draw-store.ts index c7fb00c..a24805d 100644 --- a/src/stores/draw-store.ts +++ b/src/stores/draw-store.ts @@ -1,6 +1,6 @@ import { defineStore } from 'pinia'; import { QTable } from 'quasar'; -import { initDrawApp, destroyDrawApp } from 'src/drawApp/drawApp'; +import { initDrawApp, destroyDrawApp, getDrawApp } from 'src/drawApp/drawApp'; import { DrawAssistant, GraphicData, @@ -23,7 +23,9 @@ export const useDrawStore = defineStore('draw', { drawGraphicType: (state) => state.drawAssistant?.type, drawGraphicName: (state) => state.drawAssistant?.description, drawGraphicTemplate: (state) => state.drawAssistant?.graphicTemplate, - + getApp: () => { + return getDrawApp(); + }, selectedGraphicType: (state) => { if (state.selectedGraphics) { if (state.selectedGraphics.length === 1) { @@ -103,6 +105,5 @@ export const useDrawStore = defineStore('draw', { setDraftId(id: number | null) { this.draftId = id; }, - }, });