添加箭头

This commit is contained in:
joylink_fanyuhong 2024-09-13 16:57:58 +08:00
parent 383c97e305
commit f962070209
8 changed files with 455 additions and 20 deletions

View File

@ -33,6 +33,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDrawStore } from 'src/stores/draw-store'; import { useDrawStore } from 'src/stores/draw-store';
import CanvasProperty from './properties/CanvasProperty.vue';
const drawStore = useDrawStore(); const drawStore = useDrawStore();
</script> </script>

View File

@ -0,0 +1,80 @@
<template>
<q-form>
<q-input
outlined
v-model.number="canvas.width"
@blur="onUpdate"
label="画布宽 *"
lazy-rules
:rules="[(val) => (val && val > 0) || '画布宽必须大于0']"
/>
<q-input
outlined
type="number"
v-model.number="canvas.height"
@blur="onUpdate"
label="画布高 *"
lazy-rules
:rules="[(val) => val > 0 || '画布高必须大于0']"
/>
<q-input
outlined
v-model="canvas.backgroundColor"
@blur="onUpdate"
label="画布背景色 *"
lazy-rules
:rules="[(val) => (val && val.length > 0) || '画布背景色必须设置']"
>
<template v-slot:append>
<q-icon name="colorize" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-color
:model-value="canvas.backgroundColor"
@change="
(val) => {
canvas.backgroundColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</q-form>
</template>
<script setup lang="ts">
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, onUnmounted, reactive } from 'vue';
const drawStore = useDrawStore();
const canvas = reactive({
width: 1920,
height: 1080,
backgroundColor: '#ffffff',
});
onMounted(() => {
// console.log('mounted');
const jc = drawStore.getJlCanvas();
canvas.width = jc.properties.width;
canvas.height = jc.properties.height;
canvas.backgroundColor = jc.properties.backgroundColor;
});
onUnmounted(() => {
// console.log('unmounted');
});
function onUpdate() {
// console.log('');
const app = drawStore.getDrawApp();
app.updateCanvasAndRecord({
...canvas,
viewportTransform: app.canvas.properties.viewportTransform,
});
}
</script>

View File

@ -13,6 +13,9 @@ import { useDrawStore } from 'src/stores/draw-store';
import { iscsGraphicData } from 'src/protos/iscs_graphic_data'; import { iscsGraphicData } from 'src/protos/iscs_graphic_data';
import { toStorageTransform } from './graphics/GraphicDataBase'; import { toStorageTransform } from './graphics/GraphicDataBase';
import { fromUint8Array } from 'js-base64'; 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'; // import { Notify } from 'quasar';
@ -82,23 +85,24 @@ export function initDrawApp(): IDrawApp {
isSupportDeletion: isSupportDeletion, isSupportDeletion: isSupportDeletion,
}); });
const app = drawApp; const app = drawApp;
// app.canvas.on('_rightclick', (e) => { new ArrowDraw(drawApp, new ArrowTemplate(new ArrowData()));
// if (app.drawing) return; app.canvas.on('_rightclick', (e) => {
// UndoOptions.disabled = !app.opRecord.hasUndo; if (app.drawing) return;
// RedoOptions.disabled = !app.opRecord.hasRedo; UndoOptions.disabled = !app.opRecord.hasUndo;
RedoOptions.disabled = !app.opRecord.hasRedo;
// UndoOptions.handler = () => { UndoOptions.handler = () => {
// app.opRecord.undo(); app.opRecord.undo();
// }; };
// RedoOptions.handler = () => { RedoOptions.handler = () => {
// app.opRecord.redo(); app.opRecord.redo();
// }; };
// SelectAllOptions.handler = () => { SelectAllOptions.handler = () => {
// app.selectAllGraphics(); app.selectAllGraphics();
// }; };
// DefaultCanvasMenu.open(e.global); DefaultCanvasMenu.open(e.global);
// }); });
// app.on('destroy', async () => {}); app.on('destroy', async () => {});
app.addKeyboardListener( app.addKeyboardListener(
new KeyListener({ new KeyListener({
value: 'KeyS', value: 'KeyS',
@ -136,6 +140,10 @@ export function saveDrawDatas(app: IDrawApp) {
viewportTransform: toStorageTransform(canvasData.viewportTransform), viewportTransform: toStorageTransform(canvasData.viewportTransform),
}); });
const graphics = app.queryStore.getAllGraphics(); 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); console.log(storage, '保存数据', graphics);
const base64 = fromUint8Array(storage.serialize()); const base64 = fromUint8Array(storage.serialize());
return base64; return base64;

View File

@ -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<iscsGraphicData.Arrow>();
}
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);
}
}

View File

@ -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<IArrowData>();
}
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<Arrow> {
constructor(dataTemplate: IArrowData) {
super(Arrow.Type, { dataTemplate });
}
new() {
return new Arrow();
}
}

View File

@ -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<ArrowTemplate, IArrowData> {
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>(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<Arrow> {
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>(
PolylineEditPlugin.Name
);
if (lep) {
lep.hideAll();
}
}
}

View File

@ -111,6 +111,7 @@ import { useRoute, useRouter } from 'vue-router';
import { successNotify } from 'src/utils/CommonNotify'; import { successNotify } from 'src/utils/CommonNotify';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useDrawStore } from 'src/stores/draw-store'; import { useDrawStore } from 'src/stores/draw-store';
import { Arrow } from 'src/graphics/arrow/Arrow';
const $q = useQuasar(); const $q = useQuasar();
const route = useRoute(); const route = useRoute();
@ -195,7 +196,7 @@ onMounted(() => {
} else { } else {
drawStore.setDraftId(null); drawStore.setDraftId(null);
} }
const drawAssistantsTypes = []; const drawAssistantsTypes = [Arrow.Type];
drawAssistantsTypes.forEach((type) => { drawAssistantsTypes.forEach((type) => {
const drawAssistant = drawStore.getDrawApp().getDrawAssistant(type); const drawAssistant = drawStore.getDrawApp().getDrawAssistant(type);
if (drawAssistant) { if (drawAssistant) {

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { QTable } from 'quasar'; import { QTable } from 'quasar';
import { initDrawApp, destroyDrawApp } from 'src/drawApp/drawApp'; import { initDrawApp, destroyDrawApp, getDrawApp } from 'src/drawApp/drawApp';
import { import {
DrawAssistant, DrawAssistant,
GraphicData, GraphicData,
@ -23,7 +23,9 @@ export const useDrawStore = defineStore('draw', {
drawGraphicType: (state) => state.drawAssistant?.type, drawGraphicType: (state) => state.drawAssistant?.type,
drawGraphicName: (state) => state.drawAssistant?.description, drawGraphicName: (state) => state.drawAssistant?.description,
drawGraphicTemplate: (state) => state.drawAssistant?.graphicTemplate, drawGraphicTemplate: (state) => state.drawAssistant?.graphicTemplate,
getApp: () => {
return getDrawApp();
},
selectedGraphicType: (state) => { selectedGraphicType: (state) => {
if (state.selectedGraphics) { if (state.selectedGraphics) {
if (state.selectedGraphics.length === 1) { if (state.selectedGraphics.length === 1) {
@ -103,6 +105,5 @@ export const useDrawStore = defineStore('draw', {
setDraftId(id: number | null) { setDraftId(id: number | null) {
this.draftId = id; this.draftId = id;
}, },
}, },
}); });