实现图形模板表单、画布表单、link表单及drawStore相关功能

This commit is contained in:
walker 2023-05-21 18:04:39 +08:00
parent 7ece0182b9
commit 39d68c8fe0
12 changed files with 339 additions and 196 deletions

View File

@ -1,21 +1,45 @@
<template>
<div v-if="drawStore.drawMode" class="q-pa-md">
<!-- 绘制图形模板属性 -->
<div v-if="drawStore.drawMode">
<q-card flat>
<q-card-section>
<div class="text-h6">{{ drawStore.drawGraphicName + ' 模板' }}</div>
</q-card-section>
<q-separator inset></q-separator>
<q-card-section>
<template v-if="drawStore.drawGraphicType === Link.Type">
<link-template></link-template>
</template>
</q-card-section>
</q-card>
</div>
<!-- 画布或图形对象属性 -->
<div v-else-if="drawStore.selectedGraphics !== null">
<q-card flat>
<q-card-section>
<div class="text-h6">{{ drawStore.selectedObjName + ' 属性' }}</div>
</q-card-section>
<q-separator inset></q-separator>
<template v-if="drawStore.selectedGraphics.length === 0">
<q-card-section>
<canvas-property></canvas-property>
</q-card-section>
</template>
<template v-else-if="drawStore.selectedGraphics.length === 1">
<q-card-section v-if="drawStore.selectedGraphicType === Link.Type">
<link-property></link-property>
</q-card-section>
</template>
</q-card>
</div>
<div v-else class="q-pa-md"></div>
</template>
<script setup lang="ts">
import LinkTemplate from './templates/LinkTemplate.vue';
import CanvasProperty from './properties/CanvasProperty.vue';
import LinkProperty from './properties/LinkProperty.vue';
import { Link } from 'src/graphics/link/Link';
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, ref } from 'vue';
const drawStore = useDrawStore();
onMounted(() => {
console.log('绘制属性组件mounted');
});
</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

@ -1,65 +1,96 @@
<template>
<q-form @submit="onSubmit" @reset="onReset" class="q-gutter-md">
<q-form>
<q-input outlined readonly v-model="linkModel.id" label="id" :rules="[]" />
<q-input
filled
v-model="name"
label="Your name *"
hint="Name and surname"
lazy-rules
:rules="[(val) => (val && val.length > 0) || 'Please type something']"
/>
<q-input
filled
outlined
v-model.number="linkModel.lineWidth"
type="number"
v-model="age"
label="Your age *"
@blur="onUpdate"
label="线宽"
lazy-rules
:rules="[
(val) => (val !== null && val !== '') || 'Please type your age',
(val) => (val > 0 && val < 100) || 'Please type a real age',
]"
:rules="[(val) => (val && val > 0) || '画布宽必须大于0']"
/>
<q-toggle v-model="accept" label="I accept the license and terms" />
<q-input
outlined
v-model="linkModel.lineColor"
@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
v-model="linkModel.lineColor"
@change="
(val) => {
linkModel.lineColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<div>
<q-btn label="Submit" type="submit" color="primary" />
<q-btn label="Reset" type="reset" color="primary" flat class="q-ml-sm" />
</div>
<!-- <q-btn-toggle
disable
v-model="linkModel.curve"
:options="[
{ label: '直线', value: false },
{ label: '曲线', value: true },
]"
/> -->
<q-input
v-if="linkModel.curve"
outlined
v-model.number="linkModel.segmentsCount"
type="number"
@blur="onUpdate"
label="曲线分段数量"
lazy-rules
:rules="[(val) => (val && val > 0) || '曲线分段数量必须大于0']"
/>
</q-form>
</template>
<script setup lang="ts">
import { useQuasar } from 'quasar';
import { ref } from 'vue';
const $q = useQuasar();
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';
const name = ref(null);
const age = ref(null);
const accept = ref(false);
const drawStore = useDrawStore();
const linkModel = reactive(new LinkData());
function onSubmit() {
if (accept.value !== true) {
$q.notify({
color: 'red-5',
textColor: 'white',
icon: 'warning',
message: 'You need to accept the license and terms first',
});
} else {
$q.notify({
color: 'green-4',
textColor: 'white',
icon: 'cloud_done',
message: 'Submitted',
});
drawStore.$subscribe;
watch(
() => drawStore.selectedGraphic,
(val) => {
if (val && val.type == Link.Type) {
// console.log('link');
linkModel.copyFrom(val.saveData() as LinkData);
}
}
);
function onReset() {
name.value = null;
age.value = null;
accept.value = false;
onMounted(() => {
// console.log('link mounted');
const link = drawStore.selectedGraphic as Link;
if (link) {
linkModel.copyFrom(link.saveData());
}
});
function onUpdate() {
console.log('link 属性更新');
const link = drawStore.selectedGraphic as Link;
if (link) {
drawStore.getDrawApp().updateGraphicAndRecord(link, linkModel);
}
}
</script>

View File

@ -1,57 +1,96 @@
<template>
<q-form class="q-gutter-md">
<q-form>
<q-input
filled
outlined
v-model.number="template.lineWidth"
type="number"
@blur="onUpdate"
label="线宽 *"
lazy-rules
:rules="[(val) => (val && val.length > 0) || '线宽必须大于0']"
:rules="[(val) => (val && val > 0) || '线宽必须大于0']"
/>
<q-input
filled
outlined
v-model="template.lineColor"
@blur="onUpdate"
label="线色 *"
lazy-rules
:rules="[(val) => (val && val.length > 0) || '线色必须设置']"
: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 v-model="template.lineColor" />
<q-color
v-model="template.lineColor"
@change="
(val) => {
template.lineColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<q-btn-toggle
v-model="template.curve"
@update:model-value="onUpdate"
:options="[
{ label: '直线', value: false },
{ label: '曲线', value: true },
]"
/>
<q-input
v-if="template.curve"
outlined
v-model.number="template.segmentsCount"
type="number"
@blur="onUpdate"
label="曲线分段数量 *"
lazy-rules
:rules="[(val) => (val && val > 0) || '曲线分段数量必须大于0']"
/>
</q-form>
</template>
<script setup lang="ts">
import { useQuasar } from 'quasar';
import { LinkDraw } from 'src/graphics/link/LinkDrawAssistant';
import { LinkTemplate } from 'src/graphics/link/Link';
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, reactive, ref } from 'vue';
const $q = useQuasar();
import { onMounted, reactive } from 'vue';
const drawStore = useDrawStore();
const template = reactive({
lineWidth: 1,
lineColor: '#0000ff',
curve: 0,
curve: false,
segmentsCount: 10,
});
onMounted(() => {
const type = drawStore.drawGraphicType;
if (type) {
const app = drawStore.getDrawApp();
const plugin = app.interactionPlugin<LinkDraw>(type);
const gt = plugin.graphicTemplate;
template.lineWidth = gt.lineWidth;
template.lineColor = gt.lineColor;
template.curve = gt.curve ? 1 : 0;
template.segmentsCount = gt.segmentsCount;
const gt = drawStore.drawGraphicTemplate;
if (gt) {
const lt = gt as LinkTemplate;
template.lineWidth = lt.lineWidth;
template.lineColor = lt.lineColor;
template.curve = lt.curve;
template.segmentsCount = lt.segmentsCount;
}
}
});
function onUpdate() {
const gt = drawStore.drawGraphicTemplate as LinkTemplate;
if (gt) {
gt.lineWidth = template.lineWidth;
gt.lineColor = template.lineColor;
gt.curve = template.curve;
gt.segmentsCount = template.segmentsCount;
}
}
</script>

View File

@ -33,6 +33,12 @@ export class LinkData extends GraphicDataBase implements ILinkData {
set curve(v: boolean) {
this.data.curve = v;
}
get curveNumber(): number {
return this.data.curve ? 1 : 0;
}
set curveNumber(v: number) {
this.data.curve = v === 0 ? false : true;
}
get segmentsCount(): number {
return this.data.segmentsCount;
}

View File

@ -15,7 +15,7 @@ export class IscsFanDraw extends GraphicDrawAssistant<
constructor(app: JlDrawApp, createData: () => IIscsFanData) {
const template = new IscsFanTemplate();
super(app, template, createData, IscsFan.Type, '');
super(app, template, createData, IscsFan.Type, '风机');
IscsFanInteraction.init(app);
}
@ -41,9 +41,6 @@ export class IscsFanDraw extends GraphicDrawAssistant<
this.iscsFan.position.copyFrom(this.toCanvasCoordinates(e.global));
this.createAndStore(false);
}
onRightUp(): void {
this.finish();
}
prepareData(data: IIscsFanData): boolean {
data.transform = this.iscsFan.saveTransform();
return true;

View File

@ -88,7 +88,7 @@ export class LinkDraw extends GraphicDrawAssistant<LinkTemplate, ILinkData> {
this.points = [];
this.graphic.clear();
}
onRightUp(): void {
onRightClick(): void {
this.createAndStore(true);
}
onLeftDown(e: FederatedPointerEvent): void {

View File

@ -20,13 +20,11 @@ import {
InteractionPlugin,
KeyListener,
ShiftData,
ViewportMovePlugin,
} from '../plugins';
import { CommonMouseTool } from '../plugins/CommonMousePlugin';
import { MenuItemOptions } from '../ui/Menu';
import { DOWN, LEFT, RIGHT, UP, recursiveChildren } from '../utils';
import {
CanvasData,
GraphicApp,
GraphicAppOptions,
ICanvasProperties,
@ -40,9 +38,10 @@ export abstract class GraphicDrawAssistant<
GT extends GraphicTemplate,
GD extends GraphicData
> extends AppInteractionPlugin {
readonly __GraphicDrawAssistant = true;
app: JlDrawApp;
type: string; // 图形对象类型
description?: string; // 描述
description: string; // 描述
icon: string; // 界面显示的图标
container: Container = new Container();
graphicTemplate: GT;
@ -64,7 +63,7 @@ export abstract class GraphicDrawAssistant<
graphicTemplate: GT,
createGraphicData: () => GD,
icon: string,
description?: string
description: string
) {
super(graphicTemplate.type, graphicApp);
this.app = graphicApp;
@ -88,14 +87,16 @@ export abstract class GraphicDrawAssistant<
canvas.on('mouseup', this.onLeftUp, this);
canvas.on('rightdown', this.onRightDown, this);
canvas.on('rightup', this.onRightUp, this);
canvas.on('_rightclick', this.onRightClick, this);
this.app.viewport.wheel({
percent: 0.01,
});
this.app.addKeyboardListener(this.escListener);
this.app
.interactionPlugin<ViewportMovePlugin>(ViewportMovePlugin.Name)
.resume();
this.app.viewport.drag({
mouseButtons: 'right',
});
}
unbind(): void {
this.clearCache();
@ -111,9 +112,7 @@ export abstract class GraphicDrawAssistant<
this.app.removeKeyboardListener(this.escListener);
this.app
.interactionPlugin<ViewportMovePlugin>(ViewportMovePlugin.Name)
.pause();
this.app.viewport.plugins.remove('drag');
}
onLeftDown(e: FederatedMouseEvent) {}
@ -125,6 +124,9 @@ export abstract class GraphicDrawAssistant<
onLeftUp(e: FederatedMouseEvent) {}
onRightDown(e: FederatedMouseEvent) {}
onRightUp(e: FederatedMouseEvent) {}
onRightClick(e: FederatedMouseEvent) {
this.finish();
}
/**
* id
*/
@ -186,13 +188,6 @@ export abstract class GraphicDrawAssistant<
}
}
/**
* App事件
*/
export enum DrawAppEvent {
propertiesupdate = 'propertiesupdate', // 图形对象数据变更
}
export type DrawAssistant = GraphicDrawAssistant<GraphicTemplate, GraphicData>;
export interface IDrawAppOptions {
@ -226,16 +221,11 @@ export class JlDrawApp extends GraphicApp {
drawAssistants: DrawAssistant[] = [];
selectedData: GraphicData | ICanvasProperties | null;
constructor(dom: HTMLElement) {
super(dom);
this.appendDrawStatesDisplay();
// 数据更新表单缓存处理
this.selectedData = null;
this.initSelectedDataUpdateListen();
// 拖拽操作记录
this.appOperationRecord();
// 绑定通用键盘操作
@ -314,49 +304,6 @@ export class JlDrawApp extends GraphicApp {
});
}
/**
* Vue代理及记录需求
*/
private initSelectedDataUpdateListen() {
// 画布数据更新
this.selectedData = new CanvasData(this.canvas.properties);
this.canvas.on('dataupdate', () => {
if (this.selectedGraphics.length === 0) {
this.selectedData = this.canvas.saveData();
this.emit('propertiesupdate', this.selectedData);
}
});
this.viewport.on('moved-end', () => {
if (this.selectedGraphics.length === 0) {
this.selectedData = this.canvas.saveData();
this.emit('propertiesupdate', this.selectedData);
}
});
// 图形对象数据更新
const graphicPropertiesUpdate = (g: DisplayObject) => {
if (this.selectedGraphics.length === 1) {
this.selectedData = (g as JlGraphic).saveData();
this.emit(DrawAppEvent.propertiesupdate, this.selectedData);
}
};
this.on('graphicselectedchange', (g: DisplayObject, selected) => {
if (selected) {
g.on('repaint', graphicPropertiesUpdate, this);
} else {
g.off('repaint', graphicPropertiesUpdate, this);
}
if (this.selectedGraphics.length === 0) {
this.selectedData = this.canvas.properties.clone();
} else if (this.selectedGraphics.length === 1) {
const g = this.selectedGraphics[0];
this.selectedData = g.saveData();
} else {
this.selectedData = null;
}
this.emit(DrawAppEvent.propertiesupdate, this.selectedData);
});
}
/**
*
*/
@ -516,34 +463,22 @@ export class JlDrawApp extends GraphicApp {
this.opRecord.record(new GraphicDeleteOperation(this, deletes));
}
onSubmit(): void {
if (this.selectedData == null) return;
if (this.selectedGraphics.length === 0) {
const cp = this.selectedData as ICanvasProperties;
updateCanvasAndRecord(data: ICanvasProperties) {
const old = this.canvas.properties.clone();
this.canvas.update(data);
const newVal = this.canvas.properties.clone();
this.opRecord.record(
new UpdateCanvasOperation(
this,
this.canvas,
cp,
this.canvas.properties.clone()
)
);
this.canvas.update(this.selectedData as ICanvasProperties);
} else if (this.selectedGraphics.length === 1) {
const g = this.selectedGraphics[0];
if (this.selectedData != null) {
const data = g.saveData();
g.updateData(this.selectedData as GraphicData);
this.opRecord.record(
new GraphicDataUpdateOperation(
this,
[g],
[data],
[this.selectedData as GraphicData]
)
new UpdateCanvasOperation(this, this.canvas, old, newVal)
);
}
}
updateGraphicAndRecord(g: JlGraphic, data: GraphicData) {
const old = g.saveData();
g.updateData(data);
const newVal = g.saveData();
this.opRecord.record(
new GraphicDataUpdateOperation(this, [g], [old], [newVal])
);
}
}
@ -561,21 +496,10 @@ function handleArrowKeyMoveGraphics(
if (child.selected && child.draggable) {
handler(child);
}
// if (child.selected && child.draggable) {
// child.emit(
// 'transformstart',
// GraphicTransformEvent.init(child, 'shift')
// );
// child.position.x += dp.x;
// child.position.y += dp.y;
// }
});
} else {
app.selectedGraphics.forEach((g) => {
handler(g);
// g.emit('transformstart', GraphicTransformEvent.init(g, 'shift'));
// g.position.x += dp.x;
// g.position.y += dp.y;
});
}
}

View File

@ -380,8 +380,8 @@ export class GraphicApp extends EventEmitter<GraphicAppEvents> {
this.interactiveTypeOptions = { interactiveGraphicTypeIncludes: [] };
// 创建pixi渲染app
this.app = new Application({
// width: dom.clientWidth,
// height: dom.clientHeight,
width: dom.clientWidth,
height: dom.clientHeight,
antialias: true,
resizeTo: dom,
});
@ -829,6 +829,7 @@ export class GraphicApp extends EventEmitter<GraphicAppEvents> {
}
const worldWidth = this.canvas._properties.width;
const worldHeight = this.canvas._properties.height;
this.app.resize();
this.viewport.resize(screenWidth, screenHeight, worldWidth, worldHeight);
if (this.viewport.OOB().right) {
this.viewport.right = this.viewport.right + 1;

View File

@ -33,7 +33,7 @@ declare namespace GlobalMixins {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface GraphicAppEvents {
propertiesupdate: [selectedData: GraphicData | CanvasProperties | null];
// propertiesupdate: [selectedData: GraphicData | CanvasProperties | null];
}
interface DisplayObject {

View File

@ -133,6 +133,7 @@ onMounted(() => {
if (dom) {
const drawApp = drawStore.initDrawApp(dom);
loadDrawDatas(drawApp);
onResize();
}
});

View File

@ -1,13 +1,44 @@
import { defineStore } from 'pinia';
import { destroyDrawApp, getDrawApp, initDrawApp } from 'src/examples/app';
import { GraphicDrawAssistant, JlDrawApp } from 'src/jlgraphic';
import { DrawAssistant, JlCanvas, JlDrawApp, JlGraphic } from 'src/jlgraphic';
export const useDrawStore = defineStore('draw', {
state: () => ({
drawGraphicType: null as string | null,
drawAssistant: null as DrawAssistant | null,
selectedGraphics: null as JlGraphic[] | null,
}),
getters: {
drawMode: (state) => state.drawGraphicType != null,
drawMode: (state) => state.drawAssistant != null,
drawGraphicType: (state) => state.drawAssistant?.type,
drawGraphicName: (state) => state.drawAssistant?.description,
drawGraphicTemplate: (state) => state.drawAssistant?.graphicTemplate,
selectedGraphicType: (state) => {
if (state.selectedGraphics) {
if (state.selectedGraphics.length === 1) {
return state.selectedGraphics[0].type;
}
}
},
selectedObjName: (state) => {
if (state.selectedGraphics) {
if (state.selectedGraphics.length == 0) {
return '画布';
} else if (state.selectedGraphics.length == 1) {
return state.selectedGraphics[0].type;
}
return '多选';
}
return '';
},
selectedGraphic: (state) => {
if (state.selectedGraphics) {
if (state.selectedGraphics.length === 1) {
return state.selectedGraphics[0];
}
}
return null;
},
},
actions: {
getDrawApp(): JlDrawApp {
@ -17,21 +48,30 @@ export const useDrawStore = defineStore('draw', {
}
return app;
},
getJlCanvas(): JlCanvas {
return this.getDrawApp().canvas;
},
initDrawApp(dom: HTMLElement) {
const app = initDrawApp(dom);
app.on('interaction-plugin-resume', (plugin) => {
if (plugin.isAppPlugin()) {
if (plugin instanceof GraphicDrawAssistant) {
this.drawGraphicType = plugin.name;
if (Object.hasOwn(plugin, '__GraphicDrawAssistant')) {
this.drawAssistant = plugin as DrawAssistant;
} else {
this.drawGraphicType = null;
this.drawAssistant = null;
}
}
});
app.on('graphicselectedchange', () => {
this.selectedGraphics = app.selectedGraphics;
});
this.selectedGraphics = [];
return app;
},
destroy() {
this.drawGraphicType = null;
// console.log('绘制状态清空,绘制应用销毁');
this.drawAssistant = null;
this.selectedGraphics = null;
destroyDrawApp();
},
},