代码迁移

This commit is contained in:
Yuan 2023-06-06 14:33:25 +08:00
parent b3da1703b5
commit 6848ea4b2e
50 changed files with 6704 additions and 5 deletions

@ -1 +1 @@
Subproject commit 0a0cb0a77afd9783081c2dc6ba19687b0b3aa0f7 Subproject commit 528873959ed9ed3d62c659347174ba6e55bec58f

View File

@ -39,6 +39,7 @@
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.1.0",
"eslint-plugin-vue": "^9.0.0", "eslint-plugin-vue": "^9.0.0",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"protoc-gen-ts": "^0.8.6",
"ts-md5": "^1.3.1", "ts-md5": "^1.3.1",
"typescript": "^4.5.4" "typescript": "^4.5.4"
}, },

View File

@ -21,7 +21,7 @@ module.exports = configure(function (/* ctx */) {
// rawOptions: {}, // rawOptions: {},
warnings: true, warnings: true,
errors: true, errors: true,
exclude: ['src/proto/*'], exclude: ['src/protos/*'],
}, },
// https://v2.quasar.dev/quasar-cli-vite/prefetch-feature // https://v2.quasar.dev/quasar-cli-vite/prefetch-feature

View File

@ -9,7 +9,7 @@ const { exec } = require('child_process');
const messageDir = resolve(__dirname, '../xian-ncc-da-message'); const messageDir = resolve(__dirname, '../xian-ncc-da-message');
const protoDir = resolve(messageDir, 'protos'); const protoDir = resolve(messageDir, 'protos');
const destDir = resolve(__dirname, '../src/proto'); const destDir = resolve(__dirname, '../src/protos');
/** /**
* 递归处理所有proto文件生成 * 递归处理所有proto文件生成
@ -47,7 +47,7 @@ function buildGenerateCmd(name, path = []) {
} }
function main() { function main() {
const protocDir = resolve(messageDir, 'protoc-22.2'); const protocDir = resolve(messageDir, 'protoc-23.2');
const protocBin = resolve( const protocBin = resolve(
protocDir, protocDir,
`bin/${isLinux ? 'linux-x86_64' : 'win64'}` `bin/${isLinux ? 'linux-x86_64' : 'win64'}`

View File

@ -0,0 +1,87 @@
<template>
<!-- 绘制图形模板属性 -->
<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>
<template v-if="drawStore.drawGraphicType === Rect.Type">
<rect-template></rect-template>
</template>
<template v-if="drawStore.drawGraphicType === Platform.Type">
<platform-template></platform-template>
</template>
<template v-if="drawStore.drawGraphicType === Station.Type">
<station-template></station-template>
</template>
<template v-if="drawStore.drawGraphicType === Train.Type">
<TrainProperty></TrainProperty>
</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>
<link-property
v-if="drawStore.selectedGraphicType === Link.Type"
></link-property>
<rect-property
v-if="drawStore.selectedGraphicType === Rect.Type"
></rect-property>
<platform-property
v-if="drawStore.selectedGraphicType === Platform.Type"
></platform-property>
<station-property
v-if="drawStore.selectedGraphicType === Station.Type"
></station-property>
<train-property
v-if="drawStore.selectedGraphicType === Train.Type"
></train-property>
<iscs-fan-property
v-else-if="drawStore.selectedGraphicType === IscsFan.Type"
></iscs-fan-property>
</q-card-section>
</template>
</q-card>
</div>
</template>
<script setup lang="ts">
import LinkTemplate from './templates/LinkTemplate.vue';
import RectTemplate from './templates/RectTemplate.vue';
import PlatformTemplate from './templates/PlatformTemplate.vue';
import StationTemplate from './templates/StationTemplate.vue';
import CanvasProperty from './properties/CanvasProperty.vue';
import LinkProperty from './properties/LinkProperty.vue';
import RectProperty from './properties/RectProperty.vue';
import PlatformProperty from './properties/PlatformProperty.vue';
import StationProperty from './properties/StationProperty.vue';
import TrainProperty from './properties/TrainProperty.vue';
import IscsFanProperty from './properties/IscsFanProperty.vue';
import { Link } from 'src/graphics/link/Link';
import { Rect } from 'src/graphics/rect/Rect';
import { Platform } from 'src/graphics/platform/Platform';
import { Station } from 'src/graphics/station/Station';
import { Train } from 'src/graphics/train/Train';
import { useDrawStore } from 'src/stores/draw-store';
import { IscsFan } from 'src/graphics/iscs-fan/IscsFan';
const drawStore = useDrawStore();
</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

@ -0,0 +1,42 @@
<template>
<q-form>
<q-input outlined readonly v-model="model.id" label="id" :rules="[]" />
</q-form>
</template>
<script setup lang="ts">
import { IscsFanData } from 'src/drawApp/graphics/IscsFanInteraction';
import { IscsFan } from 'src/graphics/iscs-fan/IscsFan';
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, reactive, watch } from 'vue';
const drawStore = useDrawStore();
const model = reactive(new IscsFanData());
drawStore.$subscribe;
watch(
() => drawStore.selectedGraphic,
(val) => {
if (val && val.type == IscsFan.Type) {
// console.log('Iscs');
model.copyFrom(val.saveData() as IscsFanData);
}
}
);
onMounted(() => {
// console.log('Iscs mounted');
const g = drawStore.selectedGraphic as IscsFan;
if (g) {
model.copyFrom(g.saveData());
}
});
function onUpdate() {
console.log('Iscs风机 属性更新');
const g = drawStore.selectedGraphic as IscsFan;
if (g) {
drawStore.getDrawApp().updateGraphicAndRecord(g, model);
}
}
</script>

View File

@ -0,0 +1,96 @@
<template>
<q-form>
<q-input outlined readonly v-model="linkModel.id" label="id" hint="" />
<q-input
outlined
v-model.number="linkModel.lineWidth"
type="number"
@blur="onUpdate"
label="线宽"
lazy-rules
:rules="[(val) => (val && val > 0) || '画布宽必须大于0']"
/>
<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>
<!-- <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 { LinkData } from 'src/drawApp/graphics/LinkInteraction';
import { Link } from 'src/graphics/link/Link';
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, 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);
}
}
);
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

@ -0,0 +1,112 @@
<template>
<q-form>
<q-input outlined readonly v-model="platformModel.id" label="id" hint="" />
<q-input
outlined
v-model.number="platformModel.lineWidth"
type="number"
@blur="onUpdate"
label="线宽"
lazy-rules
:rules="[(val) => (val && val > 0) || '画布宽必须大于0']"
/>
<q-input
outlined
v-model="platformModel.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="platformModel.lineColor"
@change="
(val) => {
platformModel.lineColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<q-select
outlined
@blur="onUpdate"
v-model="hasDoor"
:options="optionsDoor"
label="是否有屏蔽门"
/>
<q-select
outlined
@blur="onUpdate"
v-model="trainDirection"
:options="optionsDirection"
label="行驶方向"
/>
</q-form>
</template>
<script setup lang="ts">
import { PlatformData } from 'src/drawApp/graphics/PlatformInteraction';
import { Platform } from 'src/graphics/platform/Platform';
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, reactive, ref, watch } from 'vue';
const drawStore = useDrawStore();
const platformModel = reactive(new PlatformData());
const hasDoor = ref('是');
const optionsDoor = ['是', '否'];
const trainDirection = ref('向左');
const optionsDirection = ['向左', '向右'];
enum showSelect {
= 'true',
= 'false',
向左 = 'left',
向右 = 'right',
}
enum showSelectData {
true = '是',
false = '否',
left = '向左',
right = '向右',
}
drawStore.$subscribe;
watch(
() => drawStore.selectedGraphic,
(val) => {
if (val && val.type == Platform.Type) {
platformModel.copyFrom(val.saveData() as PlatformData);
hasDoor.value = (showSelectData as never)[platformModel.hasdoor + ''];
trainDirection.value = (showSelectData as never)[
platformModel.trainDirection
];
}
}
);
onMounted(() => {
const platform = drawStore.selectedGraphic as Platform;
if (platform) {
platformModel.copyFrom(platform.saveData());
hasDoor.value = (showSelectData as never)[platformModel.hasdoor + ''];
trainDirection.value = (showSelectData as never)[
platformModel.trainDirection
];
}
});
function onUpdate() {
platformModel.hasdoor = JSON.parse((showSelect as never)[hasDoor.value]);
platformModel.trainDirection = (showSelect as never)[trainDirection.value];
const platform = drawStore.selectedGraphic as Platform;
if (platform) {
drawStore.getDrawApp().updateGraphicAndRecord(platform, platformModel);
}
}
</script>

View File

@ -0,0 +1,74 @@
<template>
<q-form>
<q-input outlined readonly v-model="stationModel.id" label="id" hint="" />
<q-input
outlined
v-model.number="stationModel.lineWidth"
type="number"
@blur="onUpdate"
label="线宽"
lazy-rules
:rules="[(val) => (val && val > 0) || '画布宽必须大于0']"
/>
<q-input
outlined
v-model="stationModel.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="stationModel.lineColor"
@change="
(val) => {
stationModel.lineColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</q-form>
</template>
<script setup lang="ts">
import { RectData } from 'src/drawApp/graphics/RectInteraction';
import { Rect } from 'src/graphics/rect/Rect';
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, reactive, watch } from 'vue';
const drawStore = useDrawStore();
const stationModel = reactive(new RectData());
drawStore.$subscribe;
watch(
() => drawStore.selectedGraphic,
(val) => {
if (val && val.type == Rect.Type) {
stationModel.copyFrom(val.saveData() as RectData);
}
}
);
onMounted(() => {
const station = drawStore.selectedGraphic as Rect;
if (station) {
stationModel.copyFrom(station.saveData());
}
});
function onUpdate() {
const station = drawStore.selectedGraphic as Rect;
if (station) {
drawStore.getDrawApp().updateGraphicAndRecord(station, stationModel);
}
}
</script>

View File

@ -0,0 +1,165 @@
<template>
<q-form>
<q-input outlined readonly v-model="stationModel.id" label="id" hint="" />
<q-input
outlined
label="车站名称"
@blur="onUpdate"
v-model="stationModel.code"
lazy-rules
/>
<q-input
outlined
v-model.number="stationModel.codeFontSize"
type="number"
@blur="onUpdate"
label="字体大小"
lazy-rules
:rules="[(val) => (val && val > 0) || '字体大小必须大于0']"
/>
<q-input
outlined
v-model="stationModel.codeColor"
@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="stationModel.codeColor"
@change="
(val) => {
stationModel.codeColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<q-select
outlined
@blur="onUpdate"
v-model="hasCircle"
:options="optionsCircle"
label="是否有圆圈"
/>
<q-input
outlined
v-model.number="stationModel.radius"
type="number"
@blur="onUpdate"
label="半径"
lazy-rules
:rules="[(val) => (val && val > 0) || '半径大小必须大于0']"
/>
<q-input
outlined
v-model="stationModel.fillColor"
@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="stationModel.fillColor"
@change="
(val) => {
stationModel.fillColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<q-input
outlined
v-model.number="stationModel.borderWidth"
type="number"
@blur="onUpdate"
label="边框宽度"
lazy-rules
:rules="[(val) => (val && val > 0) || '画布宽必须大于0']"
/>
<q-input
outlined
v-model="stationModel.borderColor"
@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="stationModel.borderColor"
@change="
(val) => {
stationModel.borderColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</q-form>
</template>
<script setup lang="ts">
import { StationData } from 'src/drawApp/graphics/StationInteraction';
import { Station } from 'src/graphics/station/Station';
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, reactive, ref, watch } from 'vue';
const drawStore = useDrawStore();
const stationModel = reactive(new StationData());
const hasCircle = ref('是');
const optionsCircle = ['是', '否'];
enum showSelect {
= 'true',
= 'false',
}
enum showSelectData {
true = '是',
false = '否',
}
drawStore.$subscribe;
watch(
() => drawStore.selectedGraphic,
(val) => {
if (val && val.type == Station.Type) {
stationModel.copyFrom(val.saveData() as StationData);
hasCircle.value = (showSelectData as never)[stationModel.hasCircle + ''];
}
}
);
onMounted(() => {
const station = drawStore.selectedGraphic as Station;
if (station) {
stationModel.copyFrom(station.saveData());
hasCircle.value = (showSelectData as never)[stationModel.hasCircle + ''];
}
});
function onUpdate() {
stationModel.hasCircle = JSON.parse((showSelect as never)[hasCircle.value]);
const station = drawStore.selectedGraphic as Station;
if (station) {
drawStore.getDrawApp().updateGraphicAndRecord(station, stationModel);
}
}
</script>

View File

@ -0,0 +1,198 @@
<template>
<q-form>
<q-input outlined readonly v-model="trainModel.id" label="id" hint="" />
<q-input
outlined
v-model="trainModel.code"
label="车号"
hint=""
@blur="onUpdate"
/>
<q-input
outlined
v-model="trainModel.codeColor"
@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="trainModel.codeColor"
@change="
(val) => {
trainModel.codeColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<q-input
outlined
v-model="trainModel.headColor"
@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="trainModel.headColor"
@change="
(val) => {
trainModel.headColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<q-input
outlined
v-model.number="trainModel.codeFontSize"
type="number"
@blur="onUpdate"
label="字体大小"
lazy-rules
:rules="[(val) => (val && val > 0) || '字体大小必须大于0']"
/>
<q-select
outlined
@blur="onUpdate"
v-model="hasBorder"
:options="optionsDoor"
label="是否有边框"
/>
<q-input
outlined
v-model.number="trainModel.borderWidth"
type="number"
@blur="onUpdate"
label="边框线宽"
lazy-rules
:rules="[(val) => (val && val > 0) || '边框线宽必须大于0']"
/>
<q-input
outlined
v-model="trainModel.borderColor"
@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="trainModel.borderColor"
@change="
(val) => {
trainModel.borderColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<q-input
outlined
v-model="trainModel.bodyColor"
@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="trainModel.bodyColor"
@change="
(val) => {
trainModel.bodyColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
<q-select
outlined
@blur="onUpdate"
v-model="trainDirection"
:options="optionsDirection"
label="行驶方向"
/>
</q-form>
</template>
<script setup lang="ts">
import { TrainData } from 'src/drawApp/graphics/TrainInteraction';
import { Train } from 'src/graphics/train/Train';
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, reactive, ref, watch } from 'vue';
const drawStore = useDrawStore();
const trainModel = reactive(new TrainData());
const hasBorder = ref('是');
const optionsDoor = ['是', '否'];
const trainDirection = ref('向左');
const optionsDirection = ['向左', '向右'];
enum showSelect {
= 'true',
= 'false',
向左 = 'left',
向右 = 'right',
}
enum showSelectData {
true = '是',
false = '否',
left = '向左',
right = '向右',
}
drawStore.$subscribe;
watch(
() => drawStore.selectedGraphic,
(val) => {
if (val && val.type == Train.Type) {
trainModel.copyFrom(val.saveData() as TrainData);
hasBorder.value = (showSelectData as never)[trainModel.hasBorder + ''];
trainDirection.value = (showSelectData as never)[
trainModel.trainDirection
];
}
}
);
onMounted(() => {
const train = drawStore.selectedGraphic as Train;
if (train) {
trainModel.copyFrom(train.saveData());
hasBorder.value = (showSelectData as never)[trainModel.hasBorder + ''];
trainDirection.value = (showSelectData as never)[trainModel.trainDirection];
}
});
function onUpdate() {
trainModel.hasBorder = JSON.parse((showSelect as never)[hasBorder.value]);
trainModel.trainDirection = (showSelect as never)[trainDirection.value];
const train = drawStore.selectedGraphic as Train;
if (train) {
drawStore.getDrawApp().updateGraphicAndRecord(train, trainModel);
}
}
</script>

View File

@ -0,0 +1,96 @@
<template>
<q-form>
<q-input
outlined
v-model.number="template.lineWidth"
type="number"
@blur="onUpdate"
label="线宽 *"
lazy-rules
:rules="[(val) => (val && val > 0) || '线宽必须大于0']"
/>
<q-input
outlined
v-model="template.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="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 { LinkTemplate } from 'src/graphics/link/Link';
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, reactive } from 'vue';
const drawStore = useDrawStore();
const template = reactive({
lineWidth: 1,
lineColor: '#0000ff',
curve: false,
segmentsCount: 10,
});
onMounted(() => {
const type = drawStore.drawGraphicType;
if (type) {
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

@ -0,0 +1,76 @@
<template>
<q-form>
<q-input
outlined
v-model.number="template.lineWidth"
type="number"
@blur="onUpdate"
label="线宽 *"
lazy-rules
:rules="[(val) => (val && val > 0) || '线宽必须大于0']"
/>
<q-input
outlined
v-model="template.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="template.lineColor"
@change="
(val) => {
template.lineColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</q-form>
</template>
<script setup lang="ts">
import { LinkTemplate } from 'src/graphics/link/Link';
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, reactive } from 'vue';
const drawStore = useDrawStore();
const template = reactive({
lineWidth: 1,
lineColor: '#0000ff',
curve: false,
segmentsCount: 10,
});
onMounted(() => {
const type = drawStore.drawGraphicType;
if (type) {
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

@ -0,0 +1,76 @@
<template>
<q-form>
<q-input
outlined
v-model.number="template.lineWidth"
type="number"
@blur="onUpdate"
label="线宽 *"
lazy-rules
:rules="[(val) => (val && val > 0) || '线宽必须大于0']"
/>
<q-input
outlined
v-model="template.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="template.lineColor"
@change="
(val) => {
template.lineColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</q-form>
</template>
<script setup lang="ts">
import { LinkTemplate } from 'src/graphics/link/Link';
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, reactive } from 'vue';
const drawStore = useDrawStore();
const template = reactive({
lineWidth: 1,
lineColor: '#0000ff',
curve: false,
segmentsCount: 10,
});
onMounted(() => {
const type = drawStore.drawGraphicType;
if (type) {
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

@ -0,0 +1,70 @@
<template>
<q-form>
<q-input
outlined
v-model.number="template.lineWidth"
type="number"
@blur="onUpdate"
label="字体大小 *"
lazy-rules
:rules="[(val) => (val && val > 0) || '线宽必须大于0']"
/>
<q-input
outlined
v-model="template.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="template.lineColor"
@change="
(val) => {
template.lineColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</q-form>
</template>
<script setup lang="ts">
import { LinkTemplate } from 'src/graphics/link/Link';
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, reactive } from 'vue';
const drawStore = useDrawStore();
const template = reactive({
lineWidth: 1,
lineColor: '#0000ff',
});
onMounted(() => {
const type = drawStore.drawGraphicType;
if (type) {
const gt = drawStore.drawGraphicTemplate;
if (gt) {
const lt = gt as LinkTemplate;
template.lineWidth = lt.lineWidth;
template.lineColor = lt.lineColor;
}
}
});
function onUpdate() {
const gt = drawStore.drawGraphicTemplate as LinkTemplate;
if (gt) {
gt.lineWidth = template.lineWidth;
gt.lineColor = template.lineColor;
}
}
</script>

View File

@ -0,0 +1,69 @@
<template>
<q-form>
<q-input
outlined
v-model.number="template.codeFontSize"
type="number"
@blur="onUpdate"
label="字体大小 *"
lazy-rules
:rules="[(val) => (val && val > 0) || '字体大小必须大于0']"
/>
<q-input
outlined
v-model="template.headColor"
@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="template.headColor"
@change="
(val) => {
template.headColor = val;
onUpdate();
}
"
/>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</q-form>
</template>
<script setup lang="ts">
import { TrainTemplate } from 'src/graphics/train/Train';
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, reactive } from 'vue';
const drawStore = useDrawStore();
const template = reactive({
codeFontSize: 22,
headColor: '#00FF00',
});
onMounted(() => {
const type = drawStore.drawGraphicType;
if (type) {
const gt = drawStore.drawGraphicTemplate;
if (gt) {
const lt = gt as TrainTemplate;
template.codeFontSize = lt.codeFontSize;
template.headColor = lt.headColor;
}
}
});
function onUpdate() {
const gt = drawStore.drawGraphicTemplate as TrainTemplate;
if (gt) {
gt.codeFontSize = template.codeFontSize;
gt.headColor = template.headColor;
}
}
</script>

View File

@ -0,0 +1,101 @@
import * as pb_1 from 'google-protobuf';
import {
ChildTransform,
GraphicData,
GraphicTransform,
IChildTransform,
IGraphicTransform,
} from 'src/jl-graphic';
import { toStorageTransform } from '..';
import { graphicData } from 'src/protos/stationLayoutGraphics';
export interface ICommonInfo {
id: string;
graphicType: string;
transform: IGraphicTransform;
childTransforms: IChildTransform[];
}
export interface IProtoGraphicData extends pb_1.Message {
common: ICommonInfo;
code: string;
}
export abstract class GraphicDataBase implements GraphicData {
_data: IProtoGraphicData;
constructor(data: IProtoGraphicData) {
this._data = data;
}
static defaultCommonInfo(): graphicData.CommonInfo {
return new graphicData.CommonInfo({
id: '',
graphicType: '',
transform: new graphicData.Transform({
position: new graphicData.Point({ x: 0, y: 0 }),
scale: new graphicData.Point({ x: 1, y: 1 }),
rotation: 0,
skew: new graphicData.Point({ x: 0, y: 0 }),
}),
childTransforms: [],
});
}
getData<D extends IProtoGraphicData>(): D {
return this._data as D;
}
get id(): string {
return this._data.common.id;
}
set id(v: string) {
this._data.common.id = v;
}
get graphicType(): string {
return this._data.common.graphicType;
}
set graphicType(v: string) {
this._data.common.graphicType = v;
}
get transform(): GraphicTransform {
return GraphicTransform.from(this._data.common.transform);
}
set transform(v: GraphicTransform) {
this._data.common.transform = toStorageTransform(v);
}
get childTransforms(): ChildTransform[] | undefined {
const cts: ChildTransform[] = [];
if (this._data.common.childTransforms) {
this._data.common.childTransforms.forEach((ct) => {
cts.push(ChildTransform.from(ct));
});
}
return cts;
}
set childTransforms(v: ChildTransform[] | undefined) {
if (v) {
const cts: graphicData.ChildTransform[] = [];
v.forEach((ct) =>
cts.push(
new graphicData.ChildTransform({
...ct,
transform: toStorageTransform(ct.transform),
})
)
);
this._data.common.childTransforms = cts;
} else {
this._data.common.childTransforms = [];
}
}
clone(): GraphicData {
throw new Error('Method not implemented.');
}
copyFrom(gd: GraphicDataBase): void {
pb_1.Message.copyInto(gd._data, this._data);
}
eq(other: GraphicDataBase): boolean {
return pb_1.Message.equals(this._data, other._data);
}
}

View File

@ -0,0 +1,38 @@
import * as pb_1 from 'google-protobuf';
import { IIscsFanData } from 'src/graphics/iscs-fan/IscsFan';
import { graphicData } from 'src/protos/stationLayoutGraphics';
import { GraphicDataBase } from './GraphicDataBase';
export class IscsFanData extends GraphicDataBase implements IIscsFanData {
constructor(data?: graphicData.IscsFan) {
let fan;
if (data) {
fan = data;
} else {
fan = new graphicData.IscsFan({
common: GraphicDataBase.defaultCommonInfo(),
});
}
super(fan);
}
public get data(): graphicData.IscsFan {
return this.getData<graphicData.IscsFan>();
}
get code(): string {
return this.data.code;
}
set code(v: string) {
this.data.code = v;
}
clone(): IscsFanData {
return new IscsFanData(this.data.cloneMessage());
}
copyFrom(data: IscsFanData): void {
pb_1.Message.copyInto(data.data, this.data);
}
eq(other: IscsFanData): boolean {
return pb_1.Message.equals(this.data, other.data);
}
}

View File

@ -0,0 +1,77 @@
import * as pb_1 from 'google-protobuf';
import { IPointData } from 'pixi.js';
import { ILinkData } from 'src/graphics/link/Link';
import { graphicData } from 'src/protos/stationLayoutGraphics';
import { GraphicDataBase } from './GraphicDataBase';
export class LinkData extends GraphicDataBase implements ILinkData {
constructor(data?: graphicData.Link) {
let link;
if (!data) {
link = new graphicData.Link({
common: GraphicDataBase.defaultCommonInfo(),
});
} else {
link = data;
}
super(link);
}
public get data(): graphicData.Link {
return this.getData<graphicData.Link>();
}
get code(): string {
return this.data.code;
}
set code(v: string) {
this.data.code = v;
}
get curve(): boolean {
return this.data.curve;
}
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;
}
set segmentsCount(v: number) {
this.data.segmentsCount = v;
}
get points(): IPointData[] {
return this.data.points;
}
set points(points: IPointData[]) {
this.data.points = points.map(
(p) => new graphicData.Point({ x: p.x, y: p.y })
);
}
get lineWidth(): number {
return this.data.lineWidth;
}
set lineWidth(v: number) {
this.data.lineWidth = v;
}
get lineColor(): string {
return this.data.lineColor;
}
set lineColor(v: string) {
this.data.lineColor = v;
}
clone(): LinkData {
return new LinkData(this.data.cloneMessage());
}
copyFrom(data: LinkData): void {
pb_1.Message.copyInto(data.data, this.data);
}
eq(other: LinkData): boolean {
return pb_1.Message.equals(this.data, other.data);
}
}

View File

@ -0,0 +1,88 @@
import * as pb_1 from 'google-protobuf';
import { IPointData } from 'pixi.js';
import { IPlatformData } from 'src/graphics/platform/Platform';
import { graphicData } from 'src/protos/stationLayoutGraphics';
import { GraphicDataBase } from './GraphicDataBase';
export class PlatformData extends GraphicDataBase implements IPlatformData {
constructor(data?: graphicData.Platform) {
let platform;
if (!data) {
platform = new graphicData.Platform({
common: GraphicDataBase.defaultCommonInfo(),
});
} else {
platform = data;
}
super(platform);
}
public get data(): graphicData.Platform {
return this.getData<graphicData.Platform>();
}
get code(): string {
return this.data.code;
}
set code(v: string) {
this.data.code = v;
}
get hasdoor(): boolean {
return this.data.hasdoor;
}
set hasdoor(v: boolean) {
this.data.hasdoor = v;
}
get trainDirection(): string {
return this.data.trainDirection;
}
set trainDirection(v: string) {
this.data.trainDirection = v;
}
get lineWidth(): number {
return this.data.lineWidth;
}
set lineWidth(v: number) {
this.data.lineWidth = v;
}
get lineColor(): string {
return this.data.lineColor;
}
set lineColor(v: string) {
this.data.lineColor = v;
}
get lineColorDoor(): string {
return this.data.lineColorDoor;
}
set lineColorDoor(v: string) {
this.data.lineColorDoor = v;
}
get point(): IPointData {
return this.data.point;
}
set point(point: IPointData) {
this.data.point = new graphicData.Point({ x: point.x, y: point.y });
}
get width(): number {
return this.data.width;
}
set width(v: number) {
this.data.width = v;
}
get height(): number {
return this.data.height;
}
set height(v: number) {
this.data.height = v;
}
clone(): PlatformData {
return new PlatformData(this.data.cloneMessage());
}
copyFrom(data: PlatformData): void {
pb_1.Message.copyInto(data.data, this.data);
}
eq(other: PlatformData): boolean {
return pb_1.Message.equals(this.data, other.data);
}
}

View File

@ -0,0 +1,77 @@
import * as pb_1 from 'google-protobuf';
import { IPointData } from 'pixi.js';
import { IRectData } from 'src/graphics/rect/Rect';
import { graphicData } from 'src/protos/stationLayoutGraphics';
import { GraphicDataBase } from './GraphicDataBase';
export class RectData extends GraphicDataBase implements IRectData {
constructor(data?: graphicData.Rect) {
let rect;
if (!data) {
rect = new graphicData.Rect({
common: GraphicDataBase.defaultCommonInfo(),
});
} else {
rect = data;
}
super(rect);
}
public get data(): graphicData.Rect {
return this.getData<graphicData.Rect>();
}
get code(): string {
return this.data.code;
}
set code(v: string) {
this.data.code = v;
}
get lineWidth(): number {
return this.data.lineWidth;
}
set lineWidth(v: number) {
this.data.lineWidth = v;
}
get lineColor(): string {
return this.data.lineColor;
}
set lineColor(v: string) {
this.data.lineColor = v;
}
get point(): IPointData {
return this.data.point;
}
set point(point: IPointData) {
this.data.point = new graphicData.Point({ x: point.x, y: point.y });
}
get width(): number {
return this.data.width;
}
set width(v: number) {
this.data.width = v;
}
get height(): number {
return this.data.height;
}
set height(v: number) {
this.data.height = v;
}
get points(): IPointData[] {
return this.data.points;
}
set points(points: IPointData[]) {
this.data.points = points.map(
(p) => new graphicData.Point({ x: p.x, y: p.y })
);
}
clone(): RectData {
return new RectData(this.data.cloneMessage());
}
copyFrom(data: RectData): void {
pb_1.Message.copyInto(data.data, this.data);
}
eq(other: RectData): boolean {
return pb_1.Message.equals(this.data, other.data);
}
}

View File

@ -0,0 +1,92 @@
import * as pb_1 from 'google-protobuf';
import { IPointData } from 'pixi.js';
import { IStationData } from 'src/graphics/station/Station';
import { graphicData } from 'src/protos/stationLayoutGraphics';
import { GraphicDataBase } from './GraphicDataBase';
export class StationData extends GraphicDataBase implements IStationData {
constructor(data?: graphicData.Station) {
let station;
if (!data) {
station = new graphicData.Station({
common: GraphicDataBase.defaultCommonInfo(),
});
} else {
station = data;
}
super(station);
}
public get data(): graphicData.Station {
return this.getData<graphicData.Station>();
}
get code(): string {
return this.data.code;
}
set code(v: string) {
this.data.code = v;
}
get hasCircle(): boolean {
return this.data.hasCircle;
}
set hasCircle(v: boolean) {
this.data.hasCircle = v;
}
get radius(): number {
return this.data.radius;
}
set radius(v: number) {
this.data.radius = v;
}
get borderWidth(): number {
return this.data.borderWidth;
}
set borderWidth(v: number) {
this.data.borderWidth = v;
}
get borderColor(): string {
return this.data.borderColor;
}
set borderColor(v: string) {
this.data.borderColor = v;
}
get fillColor(): string {
return this.data.fillColor;
}
set fillColor(v: string) {
this.data.fillColor = v;
}
get codeColor(): string {
return this.data.codeColor;
}
set codeColor(v: string) {
this.data.codeColor = v;
}
get codeFontSize(): number {
return this.data.codeFontSize;
}
set codeFontSize(v: number) {
this.data.codeFontSize = v;
}
get point(): IPointData {
return this.data.point;
}
set point(point: IPointData) {
this.data.point = new graphicData.Point({ x: point.x, y: point.y });
}
get circlePoint(): IPointData {
return this.data.circlePoint;
}
set circlePoint(point: IPointData) {
this.data.circlePoint = new graphicData.Point({ x: point.x, y: point.y });
}
clone(): StationData {
return new StationData(this.data.cloneMessage());
}
copyFrom(data: StationData): void {
pb_1.Message.copyInto(data.data, this.data);
}
eq(other: StationData): boolean {
return pb_1.Message.equals(this.data, other.data);
}
}

View File

@ -0,0 +1,92 @@
import * as pb_1 from 'google-protobuf';
import { IPointData } from 'pixi.js';
import { ITrainData } from 'src/graphics/train/Train';
import { graphicData } from 'src/protos/stationLayoutGraphics';
import { GraphicDataBase } from './GraphicDataBase';
export class TrainData extends GraphicDataBase implements ITrainData {
constructor(data?: graphicData.Train) {
let train;
if (!data) {
train = new graphicData.Train({
common: GraphicDataBase.defaultCommonInfo(),
});
} else {
train = data;
}
super(train);
}
public get data(): graphicData.Train {
return this.getData<graphicData.Train>();
}
get code(): string {
return this.data.code;
}
set code(v: string) {
this.data.code = v;
}
get codeColor(): string {
return this.data.codeColor;
}
set codeColor(v: string) {
this.data.codeColor = v;
}
get codeFontSize(): number {
return this.data.codeFontSize;
}
set codeFontSize(v: number) {
this.data.codeFontSize = v;
}
get trainDirection(): string {
return this.data.trainDirection;
}
set trainDirection(v: string) {
this.data.trainDirection = v;
}
get hasBorder(): boolean {
return this.data.hasBorder;
}
set hasBorder(v: boolean) {
this.data.hasBorder = v;
}
get borderWidth(): number {
return this.data.borderWidth;
}
set borderWidth(v: number) {
this.data.borderWidth = v;
}
get borderColor(): string {
return this.data.borderColor;
}
set borderColor(v: string) {
this.data.borderColor = v;
}
get headColor(): string {
return this.data.headColor;
}
set headColor(v: string) {
this.data.headColor = v;
}
get bodyColor(): string {
return this.data.bodyColor;
}
set bodyColor(v: string) {
this.data.bodyColor = v;
}
get point(): IPointData {
return this.data.point;
}
set point(point: IPointData) {
this.data.point = new graphicData.Point({ x: point.x, y: point.y });
}
clone(): TrainData {
return new TrainData(this.data.cloneMessage());
}
copyFrom(data: TrainData): void {
pb_1.Message.copyInto(data.data, this.data);
}
eq(other: TrainData): boolean {
return pb_1.Message.equals(this.data, other.data);
}
}

223
src/drawApp/index.ts Normal file
View File

@ -0,0 +1,223 @@
import { fromUint8Array, toUint8Array } from 'js-base64';
import { IPointData, Point } from 'pixi.js';
import { IscsFan } from 'src/graphics/iscs-fan/IscsFan';
import { IscsFanDraw } from 'src/graphics/iscs-fan/IscsFanDrawAssistant';
import { Link } from 'src/graphics/link/Link';
import { LinkDraw } from 'src/graphics/link/LinkDrawAssistant';
import { Platform } from 'src/graphics/platform/Platform';
import { PlatformDraw } from 'src/graphics/platform/PlatformDrawAssistant';
import {
CombinationKey,
GraphicApp,
GraphicData,
GraphicTransform,
JlDrawApp,
KeyListener,
} from 'src/jl-graphic';
import { ContextMenu } from 'src/jl-graphic/ui/ContextMenu';
import { MenuItemOptions } from 'src/jl-graphic/ui/Menu';
import { IscsFanData } from './graphics/IscsFanInteraction';
import { LinkData } from './graphics/LinkInteraction';
import { PlatformData } from './graphics/PlatformInteraction';
import { graphicData } from 'src/protos/stationLayoutGraphics';
export function fromStoragePoint(p: graphicData.Point): Point {
return new Point(p.x, p.y);
}
export function toStoragePoint(p: IPointData): graphicData.Point {
return new graphicData.Point({ x: p.x, y: p.y });
}
export function fromStorageTransfrom(
transfrom: graphicData.Transform
): GraphicTransform {
return new GraphicTransform(
fromStoragePoint(transfrom.position),
fromStoragePoint(transfrom.scale),
transfrom.rotation,
fromStoragePoint(transfrom.skew)
);
}
export function toStorageTransform(
transform: GraphicTransform
): graphicData.Transform {
return new graphicData.Transform({
position: toStoragePoint(transform.position),
scale: toStoragePoint(transform.scale),
rotation: transform.rotation,
skew: toStoragePoint(transform.skew),
});
}
const UndoOptions: MenuItemOptions = {
name: '撤销',
};
const RedoOptions: MenuItemOptions = {
name: '重做',
};
const SelectAllOptions: MenuItemOptions = {
name: '全选',
};
export const DefaultCanvasMenu = new ContextMenu({
name: '绘制-画布菜单',
groups: [
{
items: [UndoOptions, RedoOptions],
},
{
items: [SelectAllOptions],
},
],
});
let drawApp: JlDrawApp | null = null;
export function getDrawApp(): JlDrawApp | null {
return drawApp;
}
export function destroyDrawApp(): void {
if (drawApp) {
drawApp.destroy();
drawApp = null;
}
}
export function initDrawApp(dom: HTMLElement): JlDrawApp {
drawApp = new JlDrawApp(dom);
const app = drawApp;
app.setOptions({
drawAssistants: [
new LinkDraw(app, () => {
return new LinkData();
}),
new IscsFanDraw(app, () => {
return new IscsFanData();
}),
new PlatformDraw(app, () => {
return new PlatformData();
}),
],
});
// 画布右键菜单
app.registerMenu(DefaultCanvasMenu);
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.addKeyboardListener(
new KeyListener({
value: 'KeyL',
onPress: () => {
app.interactionPlugin(Link.Type).resume();
},
})
);
app.addKeyboardListener(
new KeyListener({
value: 'KeyF',
onPress: () => {
app.interactionPlugin(IscsFan.Type).resume();
},
})
);
app.addKeyboardListener(
new KeyListener({
value: 'KeyP',
onPress: () => {
app.interactionPlugin(Platform.Type).resume();
},
})
);
app.addKeyboardListener(
new KeyListener({
value: '1',
onPress: () => {
app.queryStore.queryByType<IscsFan>(IscsFan.Type).forEach((fan) => {
fan.__state = fan.__state + 1;
fan.__state = fan.__state % 5;
fan.repaint();
});
},
})
);
app.addKeyboardListener(
new KeyListener({
value: 'KeyS',
global: true,
combinations: [CombinationKey.Ctrl],
onPress: () => {
saveDrawDatas(app);
},
})
);
return drawApp;
}
const StorageKey = 'graphic-storage';
export function saveDrawDatas(app: JlDrawApp) {
const storage = new graphicData.RtssGraphicStorage();
const canvasData = app.canvas.saveData();
storage.canvas = new graphicData.Canvas({
...canvasData,
viewportTransform: toStorageTransform(canvasData.viewportTransform),
});
const graphics = app.queryStore.getAllGraphics();
graphics.forEach((g) => {
if (Link.Type === g.type) {
const linkData = (g as Link).saveData();
storage.links.push((linkData as LinkData).data);
} else if (IscsFan.Type === g.type) {
const IscsFanData = (g as IscsFan).saveData();
storage.iscsFans.push((IscsFanData as IscsFanData).data);
} else if (Platform.Type === g.type) {
const platformData = (g as Platform).saveData();
storage.Platforms.push((platformData as PlatformData).data);
}
});
const base64 = fromUint8Array(storage.serialize());
console.log('保存数据', storage);
localStorage.setItem(StorageKey, base64);
}
export function loadDrawDatas(app: GraphicApp) {
// localStorage.removeItem(StorageKey);
const base64 = localStorage.getItem(StorageKey);
// console.log('加载数据', base64);
if (base64) {
const storage = graphicData.RtssGraphicStorage.deserialize(
toUint8Array(base64)
);
console.log('加载数据', storage);
app.updateCanvas(storage.canvas);
const datas: GraphicData[] = [];
storage.links.forEach((link) => {
datas.push(new LinkData(link));
});
storage.iscsFans.forEach((fan) => {
datas.push(new IscsFanData(fan));
});
storage.Platforms.forEach((platform) => {
datas.push(new PlatformData(platform));
});
app.loadGraphic(datas);
} else {
app.loadGraphic([]);
}
}

View File

@ -0,0 +1,125 @@
import {
GraphicAnimation,
GraphicData,
JlGraphic,
JlGraphicTemplate,
} from 'src/jl-graphic';
import ISCS_FAN_Assets from './iscs-fan-spritesheet.png';
import ISCS_FAN_JSON from './iscs-fan-data.json';
import { Assets, Sprite, Spritesheet, Texture } from 'pixi.js';
interface FanTextures {
border: Texture;
blue: Texture;
gray: Texture;
green: Texture;
red: Texture;
yellow: Texture;
}
export interface IIscsFanData extends GraphicData {
get code(): string;
set code(v: string);
}
export class IscsFan extends JlGraphic {
static Type = 'IscsFan';
_border: Sprite;
_fan: Sprite;
fanTextures: FanTextures;
__state = 0;
constructor(fanTextures: FanTextures) {
super(IscsFan.Type);
this.fanTextures = fanTextures;
this._border = new Sprite();
this._border.texture = this.fanTextures.border;
this._border.anchor.set(0.5);
this._fan = new Sprite();
this._fan.texture = this.fanTextures.gray;
this._fan.anchor.set(0.5);
this.addChild(this._border);
this.addChild(this._fan);
}
doRepaint(): void {
if (this.__state === 0) {
// 停止
this.stopFanRun();
this._fan.rotation = 0;
this._fan.texture = this.fanTextures.gray;
} else if (this.__state === 1) {
// 正常运行
this._fan.texture = this.fanTextures.green;
// 动画
this.initFanRun();
} else if (this.__state === 2) {
// 报警运行
this._fan.texture = this.fanTextures.yellow;
// 动画
this.initFanRun();
} else if (this.__state === 3) {
// 故障
this.stopFanRun();
this._fan.rotation = 0;
this._fan.texture = this.fanTextures.red;
} else if (this.__state === 4) {
// 通信故障
// 停止
this.stopFanRun();
this._fan.rotation = 0;
this._fan.texture = this.fanTextures.blue;
}
}
initFanRun() {
// 动画
const name = 'fan_run';
let fanRun = this.animation(name);
if (!fanRun) {
fanRun = GraphicAnimation.init({
name: 'fan_run',
run: (dt: number) => {
this._fan.angle = (this._fan.angle + dt) % 360;
},
});
this.addAnimation(fanRun);
}
const speed = Math.round(Math.random() * 10) + 1;
fanRun.xSpeed = speed;
fanRun.resume();
}
stopFanRun() {
const name = 'fan_run';
const fanRun = this.animation(name);
if (fanRun) {
fanRun.pause();
}
}
}
export class IscsFanTemplate extends JlGraphicTemplate<IscsFan> {
fanTextures?: FanTextures;
constructor() {
super(IscsFan.Type);
}
new(): IscsFan {
if (this.fanTextures) {
return new IscsFan(this.fanTextures);
}
throw new Error('资源未加载/加载失败');
}
async loadAssets(): Promise<FanTextures> {
const texture = await Assets.load(ISCS_FAN_Assets);
const iscsFanSheet = new Spritesheet(texture, ISCS_FAN_JSON);
const result = await iscsFanSheet.parse();
this.fanTextures = {
border: result['fan-border.png'],
blue: result['fan-blue.png'],
gray: result['fan-gray.png'],
green: result['fan-green.png'],
red: result['fan-red.png'],
yellow: result['fan-yellow.png'],
};
return this.fanTextures as FanTextures;
}
}

View File

@ -0,0 +1,77 @@
import { FederatedMouseEvent, Point } from 'pixi.js';
import {
GraphicDrawAssistant,
GraphicInteractionPlugin,
JlDrawApp,
JlGraphic,
} from 'src/jl-graphic';
import { IIscsFanData, IscsFan, IscsFanTemplate } from './IscsFan';
export class IscsFanDraw extends GraphicDrawAssistant<
IscsFanTemplate,
IIscsFanData
> {
_iscsFan: IscsFan | null = null;
constructor(app: JlDrawApp, createData: () => IIscsFanData) {
const template = new IscsFanTemplate();
super(app, template, createData, IscsFan.Type, '风机');
IscsFanInteraction.init(app);
}
bind(): void {
super.bind();
if (!this._iscsFan) {
this._iscsFan = this.graphicTemplate.new();
this.container.addChild(this._iscsFan);
}
}
public get iscsFan(): IscsFan {
if (!this._iscsFan) {
throw new Error('风机绘制逻辑异常');
}
return this._iscsFan;
}
redraw(cp: Point): void {
this.iscsFan.position.copyFrom(cp);
}
onLeftUp(e: FederatedMouseEvent): void {
this.iscsFan.position.copyFrom(this.toCanvasCoordinates(e.global));
this.createAndStore(false);
}
prepareData(data: IIscsFanData): boolean {
data.transform = this.iscsFan.saveTransform();
return true;
}
onEsc(): void {
this.finish();
}
}
export class IscsFanInteraction extends GraphicInteractionPlugin<IscsFan> {
static Name = 'iscs_fan_transform';
constructor(app: JlDrawApp) {
super(IscsFanInteraction.Name, app);
}
static init(app: JlDrawApp) {
return new IscsFanInteraction(app);
}
filter(...grahpics: JlGraphic[]): IscsFan[] | undefined {
return grahpics
.filter((g) => g.type === IscsFan.Type)
.map((g) => g as IscsFan);
}
bind(g: IscsFan): void {
g.eventMode = 'static';
g.cursor = 'pointer';
g.scalable = true;
g.rotatable = true;
}
unbind(g: IscsFan): void {
g.eventMode = 'none';
g.scalable = false;
g.rotatable = false;
}
}

View File

@ -0,0 +1,66 @@
{"frames": {
"fan-blue.png":
{
"frame": {"x":0,"y":0,"w":41,"h":41},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":41,"h":41},
"sourceSize": {"w":41,"h":41},
"anchor": {"x":0.5,"y":0.5}
},
"fan-border.png":
{
"frame": {"x":41,"y":0,"w":41,"h":41},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":41,"h":41},
"sourceSize": {"w":41,"h":41},
"anchor": {"x":0.5,"y":0.5}
},
"fan-gray.png":
{
"frame": {"x":82,"y":0,"w":41,"h":41},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":41,"h":41},
"sourceSize": {"w":41,"h":41},
"anchor": {"x":0.5,"y":0.5}
},
"fan-green.png":
{
"frame": {"x":123,"y":0,"w":41,"h":41},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":41,"h":41},
"sourceSize": {"w":41,"h":41},
"anchor": {"x":0.5,"y":0.5}
},
"fan-red.png":
{
"frame": {"x":164,"y":0,"w":41,"h":41},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":41,"h":41},
"sourceSize": {"w":41,"h":41},
"anchor": {"x":0.5,"y":0.5}
},
"fan-yellow.png":
{
"frame": {"x":205,"y":0,"w":41,"h":41},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":41,"h":41},
"sourceSize": {"w":41,"h":41},
"anchor": {"x":0.5,"y":0.5}
}},
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "1.1",
"image": "test-iscs-fan.png",
"format": "RGBA8888",
"size": {"w":246,"h":41},
"scale": "1",
"smartupdate": "$TexturePacker:SmartUpdate:e7620bd2d73cc0b3e2deea9704e7eefc:f129a1d9e4b9ba57720b3861c22b155b:eb2d421f7759984b7713aa4aa5354134$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

102
src/graphics/link/Link.ts Normal file
View File

@ -0,0 +1,102 @@
import { Color, Graphics, IPointData } from 'pixi.js';
import {
GraphicData,
JlGraphic,
JlGraphicTemplate,
convertToBezierParams,
} from 'src/jl-graphic';
import { ILineGraphic } from 'src/jl-graphic/plugins/GraphicEditPlugin';
export interface ILinkData extends GraphicData {
get code(): string; // 编号
set code(v: string);
get curve(): boolean; // 是否曲线
set curve(v: boolean);
get segmentsCount(): number; // 曲线分段数
set segmentsCount(v: number);
get points(): IPointData[]; // 线坐标点
set points(points: IPointData[]);
get lineWidth(): number; // 线宽
set lineWidth(v: number);
get lineColor(): string; // 线色
set lineColor(v: string);
clone(): ILinkData;
copyFrom(data: ILinkData): void;
eq(other: ILinkData): boolean;
}
export class Link extends JlGraphic implements ILineGraphic {
static Type = 'Link';
lineGraphic: Graphics;
constructor() {
super(Link.Type);
this.lineGraphic = new Graphics();
this.addChild(this.lineGraphic);
}
get datas(): ILinkData {
return this.getDatas<ILinkData>();
}
doRepaint(): void {
if (this.datas.points.length < 2) {
throw new Error('Link坐标数据异常');
}
this.lineGraphic.clear();
this.lineGraphic.lineStyle(
this.datas.lineWidth,
new Color(this.datas.lineColor)
);
if (this.datas.curve) {
// 曲线
const bps = convertToBezierParams(this.datas.points);
bps.forEach((bp) => {
this.lineGraphic.drawBezierCurve(
bp.p1,
bp.p2,
bp.cp1,
bp.cp2,
this.datas.segmentsCount
);
});
} else {
// 直线
const start = this.getStartPoint();
this.lineGraphic.moveTo(start.x, start.y);
for (let i = 0; i < this.datas.points.length; i++) {
const p = this.datas.points[i];
this.lineGraphic.lineTo(p.x, p.y);
}
}
}
get linePoints(): IPointData[] {
return this.datas.points;
}
set linePoints(points: IPointData[]) {
const old = this.datas.clone();
old.points = points;
this.updateData(old);
}
getStartPoint(): IPointData {
return this.datas.points[0];
}
getEndPoint(): IPointData {
return this.datas.points[this.datas.points.length - 1];
}
}
export class LinkTemplate extends JlGraphicTemplate<Link> {
curve: boolean;
lineWidth: number;
lineColor: string;
segmentsCount: number;
constructor() {
super(Link.Type);
this.lineWidth = 2;
this.lineColor = '#000000';
this.curve = false;
this.segmentsCount = 10;
}
new(): Link {
return new Link();
}
}

View File

@ -0,0 +1,375 @@
import {
Color,
DisplayObject,
FederatedMouseEvent,
FederatedPointerEvent,
Graphics,
IHitArea,
Point,
} from 'pixi.js';
import {
AbsorbablePosition,
DraggablePoint,
GraphicApp,
GraphicDrawAssistant,
GraphicInteractionPlugin,
GraphicTransformEvent,
JlDrawApp,
JlGraphic,
KeyListener,
calculateMirrorPoint,
convertToBezierParams,
linePoint,
pointPolygon,
} from 'src/jl-graphic';
import AbsorbablePoint, {
AbsorbableCircle,
} from 'src/jl-graphic/graphic/AbsorbablePosition';
import {
BezierCurveEditPlugin,
ILineGraphic,
PolylineEditPlugin,
addWayPoint,
addWaypointConfig,
clearWayPoint,
clearWaypointsConfig,
getWaypointRangeIndex,
} from 'src/jl-graphic/plugins/GraphicEditPlugin';
import { ContextMenu } from 'src/jl-graphic/ui/ContextMenu';
import { ILinkData, Link, LinkTemplate } from './Link';
export interface ILinkDrawOptions {
newData: () => ILinkData;
}
export class LinkDraw extends GraphicDrawAssistant<LinkTemplate, ILinkData> {
points: Point[] = [];
graphic: Graphics = new Graphics();
// 快捷切曲线绘制
keyqListener: KeyListener = new KeyListener({
value: 'KeyQ',
global: true,
onPress: () => {
if (this.points.length == 0) {
this.graphicTemplate.curve = true;
}
},
});
// 快捷切直线绘制
keyzListener: KeyListener = new KeyListener({
value: 'KeyZ',
global: true,
onPress: () => {
if (this.points.length == 0) {
this.graphicTemplate.curve = false;
}
},
});
constructor(app: JlDrawApp, createData: () => ILinkData) {
super(app, new LinkTemplate(), createData, Link.Type, '轨道Link');
this.container.addChild(this.graphic);
this.graphicTemplate.curve = true;
LinkPointsEditPlugin.init(app);
}
bind(): void {
super.bind();
this.app.addKeyboardListener(this.keyqListener, this.keyzListener);
}
unbind(): void {
super.unbind();
this.app.removeKeyboardListener(this.keyqListener, this.keyzListener);
}
clearCache(): void {
this.points = [];
this.graphic.clear();
}
onRightClick(): void {
this.createAndStore(true);
}
onLeftDown(e: FederatedPointerEvent): void {
const { x, y } = this.toCanvasCoordinates(e.global);
const p = new Point(x, y);
if (this.graphicTemplate.curve) {
if (this.points.length == 0) {
this.points.push(p);
} else {
this.points.push(p, p.clone());
}
} else {
this.points.push(p);
}
}
onLeftUp(e: FederatedMouseEvent): void {
const template = this.graphicTemplate;
if (template.curve) {
const mp = this.toCanvasCoordinates(e.global);
if (this.points.length == 1) {
this.points.push(new Point(mp.x, mp.y));
} else if (this.points.length > 1) {
const cp2 = this.points[this.points.length - 2];
const p = this.points[this.points.length - 1];
cp2.copyFrom(calculateMirrorPoint(p, mp));
this.points.push(mp);
}
}
}
redraw(p: Point): void {
if (this.points.length < 1) return;
this.graphic.clear();
const template = this.graphicTemplate;
this.graphic.lineStyle(template.lineWidth, new Color(template.lineColor));
const ps = [...this.points];
if (template.curve) {
// 曲线
if (ps.length == 1) {
this.graphic.moveTo(ps[0].x, ps[0].y);
this.graphic.lineTo(p.x, p.y);
} else {
if ((ps.length + 1) % 3 == 0) {
ps.push(p.clone(), p.clone());
} else {
const cp = ps[ps.length - 2];
const p1 = ps[ps.length - 1];
const mp = calculateMirrorPoint(p1, p);
cp.copyFrom(mp);
}
const bps = convertToBezierParams(ps);
bps.forEach((bp) =>
this.graphic.drawBezierCurve(
bp.p1,
bp.p2,
bp.cp1,
bp.cp2,
template.segmentsCount
)
);
}
} else {
ps.push(p);
// 直线
this.graphic.moveTo(ps[0].x, ps[0].y);
for (let i = 1; i < ps.length; i++) {
const p = ps[i];
this.graphic.lineTo(p.x, p.y);
}
}
}
prepareData(data: ILinkData): boolean {
const template = this.graphicTemplate;
if (
(!template.curve && this.points.length < 2) ||
(template.curve && this.points.length < 4)
) {
console.log('Link绘制因点不够取消绘制');
return false;
}
if (template.curve) {
this.points.pop();
}
data.curve = template.curve;
data.segmentsCount = template.segmentsCount;
data.points = this.points;
data.lineWidth = template.lineWidth;
data.lineColor = template.lineColor;
data.segmentsCount = template.segmentsCount;
return true;
}
}
export class LinkGraphicHitArea implements IHitArea {
link: Link;
constructor(link: Link) {
this.link = link;
}
contains(x: number, y: number): boolean {
const p = new Point(x, y);
if (this.link.datas.curve) {
// 曲线
const bps = convertToBezierParams(this.link.datas.points);
for (let i = 0; i < bps.length; i++) {
const bp = bps[i];
if (
pointPolygon(
p,
[bp.p1, bp.cp1, bp.cp2, bp.p2],
this.link.datas.lineWidth
)
) {
return true;
}
}
} else {
// 直线
for (let i = 1; i < this.link.datas.points.length; i++) {
const p1 = this.link.datas.points[i - 1];
const p2 = this.link.datas.points[i];
if (linePoint(p1, p2, p, this.link.datas.lineWidth)) {
return true;
}
}
}
return false;
}
}
/**
*
* @param link
* @returns
*/
function buildAbsorbablePositions(link: Link): AbsorbablePosition[] {
const aps: AbsorbablePosition[] = [];
const links = link.queryStore.queryByType<Link>(Link.Type);
links.forEach((other) => {
if (other.id == link.id) {
return;
}
const apa = new AbsorbablePoint(
other.localToCanvasPoint(other.getStartPoint())
);
const apb = new AbsorbablePoint(
other.localToCanvasPoint(other.getEndPoint())
);
aps.push(apa, apb);
});
aps.push(new AbsorbableCircle(new Point(450, 410), 30));
return aps;
}
/**
*
* @param g
* @param dp
* @param index
*/
function onEditPointCreate(
g: ILineGraphic,
dp: DraggablePoint,
index: number
): void {
const link = g as Link;
if (index === 0 || index == link.datas.points.length - 1) {
// 端点
dp.on('transformstart', (e: GraphicTransformEvent) => {
if (e.isShift()) {
link.getGraphicApp().setOptions({
absorbablePositions: buildAbsorbablePositions(link),
});
}
});
}
}
const LinkEditMenu: ContextMenu = ContextMenu.init({
name: '轨道编辑菜单',
groups: [
{
items: [addWaypointConfig, clearWaypointsConfig],
},
],
});
/**
* link路径编辑
*/
export class LinkPointsEditPlugin extends GraphicInteractionPlugin<Link> {
static Name = 'LinkPointsDrag';
constructor(app: GraphicApp) {
super(LinkPointsEditPlugin.Name, app);
app.registerMenu(LinkEditMenu);
}
static init(app: GraphicApp): LinkPointsEditPlugin {
return new LinkPointsEditPlugin(app);
}
filter(...grahpics: JlGraphic[]): Link[] | undefined {
return grahpics.filter((g) => g.type == Link.Type) as Link[];
}
bind(g: Link): void {
g.lineGraphic.eventMode = 'static';
g.lineGraphic.cursor = 'pointer';
g.lineGraphic.hitArea = new LinkGraphicHitArea(g);
g.on('_rightclick', this.onContextMenu, this);
g.on('selected', this.onSelected, this);
g.on('unselected', this.onUnselected, this);
}
unbind(g: Link): void {
g.off('_rightclick', this.onContextMenu, this);
g.off('selected', this.onSelected, this);
g.off('unselected', this.onUnselected, this);
}
onContextMenu(e: FederatedMouseEvent) {
const target = e.target as DisplayObject;
const link = target.getGraphic() as Link;
this.app.updateSelected(link);
addWaypointConfig.handler = () => {
const linePoints = link.linePoints;
const p = link.screenToLocalPoint(e.global);
const { start, end } = getWaypointRangeIndex(
linePoints,
link.datas.curve,
p
);
addWayPoint(link, link.datas.curve, start, end, p);
};
clearWaypointsConfig.handler = () => {
clearWayPoint(link, link.datas.curve);
};
LinkEditMenu.open(e.global);
}
onSelected(g: DisplayObject): void {
const link = g as Link;
let lep;
if (link.datas.curve) {
// 曲线
lep = link.getAssistantAppend<BezierCurveEditPlugin>(
BezierCurveEditPlugin.Name
);
if (!lep) {
lep = new BezierCurveEditPlugin(link, {
onEditPointCreate,
});
link.addAssistantAppend(lep);
}
} else {
// 直线
lep = link.getAssistantAppend<PolylineEditPlugin>(
PolylineEditPlugin.Name
);
if (!lep) {
lep = new PolylineEditPlugin(link, { onEditPointCreate });
link.addAssistantAppend(lep);
}
}
lep.showAll();
}
onUnselected(g: DisplayObject): void {
const link = g as Link;
let lep;
if (link.datas.curve) {
// 曲线
lep = link.getAssistantAppend<BezierCurveEditPlugin>(
BezierCurveEditPlugin.Name
);
} else {
// 直线
lep = link.getAssistantAppend<PolylineEditPlugin>(
PolylineEditPlugin.Name
);
}
if (lep) {
lep.hideAll();
}
}
}

View File

@ -0,0 +1,203 @@
import { Color, Graphics, IPointData, Rectangle } from 'pixi.js';
import {
GraphicData,
JlGraphic,
JlGraphicTemplate,
VectorText,
getRectangleCenter,
} from 'src/jl-graphic';
export interface IPlatformData extends GraphicData {
get code(): string; // 编号
set code(v: string);
get hasdoor(): boolean; // 是否有屏蔽门
set hasdoor(v: boolean);
get trainDirection(): string; // 行驶方向--屏蔽门上下
set trainDirection(v: string);
get lineWidth(): number; // 线宽
set lineWidth(v: number);
get lineColor(): string; // 站台线色
set lineColor(v: string);
get lineColorDoor(): string; // 屏蔽门线色
set lineColorDoor(v: string);
get point(): IPointData; // 位置坐标
set point(point: IPointData);
get width(): number; // 宽度
set width(v: number);
get height(): number; // 高度
set height(v: number);
clone(): IPlatformData;
copyFrom(data: IPlatformData): void;
eq(other: IPlatformData): boolean;
}
//站台颜色
export enum PlatformColorEnum {
blue = '0x0fe81f', //站台的颜色
lightBlue = '0x55d15d',
yellow = '0xfbff00',
white = '0xffffff',
lozengeRed = '0xff0000', //站台旁的菱形图标
whiteNumbers = '0xffffff', //站台旁白色数字
HCharYellow = '0xfbff00', //站台旁的H字符
HCharWhite = '0xffffff',
HCharRed = '0xff0000',
doorBlue = '0x008000', //屏蔽门的颜色
doorRed = '0xff0000',
}
const platformConsts = {
width: 60,
height: 20,
lineWidth: 3,
besideFontSize: 12,
doorOpenSpacing: 5,
doorPlatformSpacing: 10,
besideSpacing: 10,
};
export class Platform extends JlGraphic {
static Type = 'Platform';
platformGraphic: Graphics;
doorGraphic: Graphics;
doorCloseGraphic: Graphics;
besideGraphic: Graphics;
codeGraph: VectorText = new VectorText(''); //站台旁数字、字符
constructor() {
super(Platform.Type);
this.platformGraphic = new Graphics();
this.doorGraphic = new Graphics();
this.doorCloseGraphic = new Graphics();
this.besideGraphic = new Graphics();
this.addChild(this.platformGraphic);
this.addChild(this.doorGraphic);
this.addChild(this.doorCloseGraphic);
this.addChild(this.besideGraphic);
this.addChild(this.codeGraph);
this.codeGraph.setVectorFontSize(platformConsts.besideFontSize);
}
get datas(): IPlatformData {
return this.getDatas<IPlatformData>();
}
doRepaint(): void {
const width = this.datas.width;
const height = this.datas.height;
//屏蔽门
const doorGraphic = this.doorGraphic;
const doorCloseGraphic = this.doorCloseGraphic;
doorGraphic.clear();
doorCloseGraphic.clear();
if (this.datas.hasdoor) {
doorGraphic.lineStyle(
this.datas.lineWidth,
new Color(this.datas.lineColorDoor)
);
doorGraphic.moveTo(-width / 2 - this.datas.lineWidth / 2, 0);
doorGraphic.lineTo(-platformConsts.doorOpenSpacing, 0);
doorGraphic.moveTo(platformConsts.doorOpenSpacing, 0);
doorGraphic.lineTo(width / 2 + this.datas.lineWidth / 2, 0);
//屏蔽门闭合
doorCloseGraphic.lineStyle(
this.datas.lineWidth,
new Color(this.datas.lineColorDoor)
);
doorCloseGraphic.moveTo(-platformConsts.doorOpenSpacing, 0);
doorCloseGraphic.lineTo(platformConsts.doorOpenSpacing, 0);
doorGraphic.position.set(
0,
-height / 2 - platformConsts.doorPlatformSpacing
);
doorCloseGraphic.position.set(
0,
-height / 2 - platformConsts.doorPlatformSpacing
);
}
//站台
const platformGraphic = this.platformGraphic;
platformGraphic.clear();
platformGraphic.lineStyle(
this.datas.lineWidth,
new Color(this.datas.lineColor)
);
platformGraphic.beginFill(this.datas.lineColor, 1);
platformGraphic.drawRect(0, 0, this.datas.width, this.datas.height);
platformGraphic.endFill;
const rectP = new Rectangle(0, 0, this.datas.width, this.datas.height);
platformGraphic.pivot = getRectangleCenter(rectP);
this.position.set(this.datas.point.x, this.datas.point.y);
//站台旁菱形图标
const besideGraphic = this.besideGraphic;
besideGraphic.clear();
besideGraphic.lineStyle(1, new Color(PlatformColorEnum.lozengeRed));
besideGraphic.drawRect(0, 0, this.datas.height / 4, this.datas.height / 4);
const rect = new Rectangle(
0,
0,
this.datas.height / 4,
this.datas.height / 4
);
besideGraphic.pivot = getRectangleCenter(rect);
besideGraphic.rotation = Math.PI / 4;
besideGraphic.position.set(
-width / 2 - this.datas.lineWidth / 2 - platformConsts.besideSpacing,
0
);
//站台旁的数字、字符
const codeGraph = this.codeGraph;
codeGraph.text = 'H';
codeGraph.anchor.set(0.5);
codeGraph.position.set(
-width / 2 - this.datas.lineWidth / 2 - platformConsts.besideSpacing,
0
);
codeGraph.style.fill = PlatformColorEnum.HCharYellow;
//站台方向
if (this.datas.trainDirection == 'right') {
doorGraphic.position.set(
0,
height / 2 + platformConsts.doorPlatformSpacing
);
doorCloseGraphic.position.set(
0,
height / 2 + platformConsts.doorPlatformSpacing
);
besideGraphic.position.set(
width / 2 + this.datas.lineWidth / 2 + platformConsts.besideSpacing,
0
);
codeGraph.position.set(
width / 2 + this.datas.lineWidth / 2 + platformConsts.besideSpacing,
0
);
}
//子元素显隐
doorCloseGraphic.visible = false;
/* besideGraphic.visible = false;
codeGraph.visible = false; */
}
}
export class PlatformTemplate extends JlGraphicTemplate<Platform> {
hasdoor: boolean;
trainDirection: string;
lineWidth: number;
lineColor: string;
lineColorDoor: string;
width: number;
height: number;
constructor() {
super(Platform.Type);
this.hasdoor = true;
this.trainDirection = 'left';
this.lineWidth = platformConsts.lineWidth;
this.lineColor = PlatformColorEnum.yellow;
this.lineColorDoor = PlatformColorEnum.doorBlue;
this.width = platformConsts.width;
this.height = platformConsts.height;
}
new(): Platform {
return new Platform();
}
}

View File

@ -0,0 +1,132 @@
import {
Color,
FederatedPointerEvent,
Graphics,
Point,
Rectangle,
} from 'pixi.js';
import {
GraphicDrawAssistant,
GraphicInteractionPlugin,
JlDrawApp,
JlGraphic,
getRectangleCenter,
} from 'src/jl-graphic';
import { IPlatformData, Platform, PlatformTemplate } from './Platform';
export interface IPlatformDrawOptions {
newData: () => IPlatformData;
}
export class PlatformDraw extends GraphicDrawAssistant<
PlatformTemplate,
IPlatformData
> {
point: Point = new Point(0, 0);
platformGraphic: Graphics = new Graphics();
doorGraphic: Graphics = new Graphics();
constructor(app: JlDrawApp, createData: () => IPlatformData) {
super(
app,
new PlatformTemplate(),
createData,
Platform.Type,
'站台Platform'
);
this.container.addChild(this.platformGraphic);
this.container.addChild(this.doorGraphic);
this.graphicTemplate.hasdoor = true;
platformInteraction.init(app);
}
bind(): void {
super.bind();
}
unbind(): void {
super.unbind();
}
clearCache(): void {
this.platformGraphic.clear();
this.doorGraphic.clear();
}
onRightClick(): void {
this.createAndStore(true);
}
onLeftDown(e: FederatedPointerEvent): void {
const { x, y } = this.toCanvasCoordinates(e.global);
const p = new Point(x, y);
this.point = p;
this.createAndStore(true);
}
redraw(p: Point): void {
const template = this.graphicTemplate;
//屏蔽门
if (template.hasdoor) {
const doorGraphic = this.doorGraphic;
doorGraphic.clear();
doorGraphic.lineStyle(
template.lineWidth,
new Color(template.lineColorDoor)
);
const width = template.width;
const height = template.height;
doorGraphic.moveTo(-width / 2 - template.lineWidth / 2, -height / 2 - 10);
doorGraphic.lineTo(width / 2 + template.lineWidth / 2, -height / 2 - 10);
doorGraphic.position.set(p.x, p.y);
}
//站台
const platformGraphic = this.platformGraphic;
platformGraphic.clear();
this.point.set(p.x, p.y);
const rect = new Rectangle(0, 0, template.width, template.height);
platformGraphic.pivot = getRectangleCenter(rect);
platformGraphic.lineStyle(template.lineWidth, template.lineColor);
platformGraphic.beginFill(template.lineColor, 1);
platformGraphic.drawRect(0, 0, template.width, template.height);
platformGraphic.endFill;
platformGraphic.position.set(this.point.x, this.point.y);
}
prepareData(data: IPlatformData): boolean {
const template = this.graphicTemplate;
data.hasdoor = template.hasdoor;
data.trainDirection = template.trainDirection;
data.point = this.point;
data.lineWidth = template.lineWidth;
data.lineColor = template.lineColor;
data.lineColorDoor = template.lineColorDoor;
data.width = template.width;
data.height = template.height;
return true;
}
}
export class platformInteraction extends GraphicInteractionPlugin<Platform> {
static Name = 'platform_transform';
constructor(app: JlDrawApp) {
super(platformInteraction.Name, app);
}
static init(app: JlDrawApp) {
return new platformInteraction(app);
}
filter(...grahpics: JlGraphic[]): Platform[] | undefined {
return grahpics
.filter((g) => g.type === Platform.Type)
.map((g) => g as Platform);
}
bind(g: Platform): void {
g.eventMode = 'static';
g.cursor = 'pointer';
g.scalable = true;
g.rotatable = true;
}
unbind(g: Platform): void {
g.eventMode = 'none';
g.scalable = false;
g.rotatable = false;
}
}

86
src/graphics/rect/Rect.ts Normal file
View File

@ -0,0 +1,86 @@
import { Color, Graphics, IPointData, Point } from 'pixi.js';
import { GraphicData, JlGraphic, JlGraphicTemplate } from 'src/jl-graphic';
export interface IRectData extends GraphicData {
get code(): string; // 编号
set code(v: string);
get lineWidth(): number; // 线宽
set lineWidth(v: number);
get lineColor(): string; // 线色
set lineColor(v: string);
get point(): IPointData; // 位置坐标
set point(point: IPointData);
get width(): number; // 宽度
set width(v: number);
get height(): number; // 高度
set height(v: number);
get points(): IPointData[]; // 线坐标点
set points(points: IPointData[]);
clone(): IRectData;
copyFrom(data: IRectData): void;
eq(other: IRectData): boolean;
}
const rectConsts = {
lineWidth: 2,
lineColor: '0xff0000',
width: 60,
height: 20,
};
export class Rect extends JlGraphic {
static Type = 'Rect';
rectGraphic: Graphics;
constructor() {
super(Rect.Type);
this.rectGraphic = new Graphics();
this.addChild(this.rectGraphic);
}
get datas(): IRectData {
return this.getDatas<IRectData>();
}
doRepaint(): void {
const width = this.datas.width;
const height = this.datas.height;
if (this.linePoints.length == 0) {
const r1 = new Point(this.datas.point.x, this.datas.point.y);
const r2 = new Point(r1.x + width, r1.y);
const r3 = new Point(r1.x + width, r1.y + height);
const r4 = new Point(r1.x, r1.y + height);
this.datas.points = [r1, r2, r3, r4, r1];
}
const rectGraphic = this.rectGraphic;
rectGraphic.clear();
rectGraphic.lineStyle(
this.datas.lineWidth,
new Color(this.datas.lineColor)
);
rectGraphic.drawPolygon(this.datas.points);
}
get linePoints(): IPointData[] {
return this.datas.points;
}
set linePoints(points: IPointData[]) {
const old = this.datas.clone();
old.points = points;
this.updateData(old);
}
}
export class RectTemplate extends JlGraphicTemplate<Rect> {
lineWidth: number;
lineColor: string;
width: number;
height: number;
constructor() {
super(Rect.Type);
this.lineWidth = rectConsts.lineWidth;
this.lineColor = rectConsts.lineColor;
this.width = rectConsts.width;
this.height = rectConsts.height;
}
new(): Rect {
return new Rect();
}
}

View File

@ -0,0 +1,262 @@
import {
FederatedPointerEvent,
Graphics,
Point,
IHitArea,
DisplayObject,
FederatedMouseEvent,
} from 'pixi.js';
import {
DraggablePoint,
GraphicApp,
GraphicDrawAssistant,
GraphicInteractionPlugin,
GraphicTransformEvent,
JlDrawApp,
JlGraphic,
linePoint,
} from 'src/jl-graphic';
import AbsorbablePoint, {
AbsorbablePosition,
} from 'src/jl-graphic/graphic/AbsorbablePosition';
import {
ILineGraphic,
PolylineEditPlugin,
addWaySegmentingConfig,
addPolygonSegmentingPoint,
clearWayPoint,
clearWaypointsConfig,
getWayLineIndex,
} from 'src/jl-graphic/plugins/GraphicEditPlugin';
import { ContextMenu } from 'src/jl-graphic/ui/ContextMenu';
import { IRectData, Rect, RectTemplate } from './Rect';
import { Link } from '../link/Link';
export interface IRectDrawOptions {
newData: () => IRectData;
}
export class RectDraw extends GraphicDrawAssistant<RectTemplate, IRectData> {
point1: Point | null = null;
point2: Point | null = null;
rectGraphic: Graphics = new Graphics();
constructor(app: JlDrawApp, createData: () => IRectData) {
super(app, new RectTemplate(), createData, Rect.Type, '站台Rect');
this.container.addChild(this.rectGraphic);
RectPointsEditPlugin.init(app);
}
bind(): void {
super.bind();
}
unbind(): void {
super.unbind();
}
clearCache(): void {
this.rectGraphic.clear();
}
onRightClick(): void {
this.createAndStore(true);
}
onLeftDown(e: FederatedPointerEvent): void {
const { x, y } = this.toCanvasCoordinates(e.global);
const p = new Point(x, y);
if (this.point1 === null) {
this.point1 = p;
} else {
this.point2 = p;
this.createAndStore(true);
this.point1 = null;
this.point2 = null;
}
}
redraw(p: Point): void {
const template = this.graphicTemplate;
if (this.point1 === null) return;
const rectGraphic = this.rectGraphic;
rectGraphic.clear();
rectGraphic.lineStyle(template.lineWidth, template.lineColor);
rectGraphic.drawRect(...this.normalize(this.point1, p));
}
//根据画的两个点确定左上角的点的坐标和矩形宽高
private normalize(p1: Point, p2: Point): [number, number, number, number] {
const { abs } = Math;
const x = p1.x < p2.x ? p1.x : p2.x;
const y = p1.y < p2.y ? p1.y : p2.y;
const w = abs(p1.x - p2.x);
const h = abs(p1.y - p2.y);
return [x, y, w, h];
}
prepareData(data: IRectData): boolean {
if (this.point1 == null) {
console.log('Rect绘制因点不够取消绘制');
return false;
}
const p1 = this.point1 as Point;
const p2 = this.point2 as Point;
const [x, y, width, height] = this.normalize(p1, p2);
const template = this.graphicTemplate;
data.point = new Point(x, y);
data.lineWidth = template.lineWidth;
data.lineColor = template.lineColor;
data.width = width;
data.height = height;
return true;
}
}
//碰撞检测
export class RectGraphicHitArea implements IHitArea {
rect: Rect;
constructor(rect: Rect) {
this.rect = rect;
}
contains(x: number, y: number): boolean {
let contains = false;
const p = new Point(x, y);
const rectData = this.rect.datas;
//contains = pointPolygon(p, rectData.points, rectData.lineWidth);是否包含多边形内部
const tolerance = rectData.lineWidth;
for (let i = 0; i < rectData.points.length - 1; i++) {
const p1 = rectData.points[i];
const p2 = rectData.points[i + 1];
contains = contains || linePoint(p1, p2, p, tolerance);
if (contains) {
break;
}
}
return contains;
}
}
/**
*
* @param rect
* @returns
*/
function buildAbsorbablePositions(rect: Rect): AbsorbablePosition[] {
const aps: AbsorbablePosition[] = [];
const rects = rect.queryStore.queryByType<Rect>(Rect.Type);
const links = rect.queryStore.queryByType<Link>(Link.Type);
links.forEach((other) => {
const apa = new AbsorbablePoint(
other.localToCanvasPoint(other.getStartPoint())
);
const apb = new AbsorbablePoint(
other.localToCanvasPoint(other.getEndPoint())
);
aps.push(apa, apb);
});
rects.forEach((other) => {
if (other.id == rect.id) {
return;
}
other.linePoints.forEach((point) => {
const absorbablePoint = new AbsorbablePoint(point);
aps.push(absorbablePoint);
});
});
return aps;
}
/**
*
* @param g
* @param dp
* @param index
*/
function onEditPointCreate(g: ILineGraphic, dp: DraggablePoint): void {
const rect = g as Rect;
// 端点
dp.on('transformstart', (e: GraphicTransformEvent) => {
if (e.isShift()) {
rect.getGraphicApp().setOptions({
absorbablePositions: buildAbsorbablePositions(rect),
});
}
});
}
const RectEditMenu: ContextMenu = ContextMenu.init({
name: '矩形编辑菜单',
groups: [
{
items: [addWaySegmentingConfig, clearWaypointsConfig],
},
],
});
/**
* rect路径编辑
*/
export class RectPointsEditPlugin extends GraphicInteractionPlugin<Rect> {
static Name = 'RectPointsDrag';
constructor(app: GraphicApp) {
super(RectPointsEditPlugin.Name, app);
app.registerMenu(RectEditMenu);
}
static init(app: GraphicApp): RectPointsEditPlugin {
return new RectPointsEditPlugin(app);
}
filter(...grahpics: JlGraphic[]): Rect[] | undefined {
return grahpics.filter((g) => g.type == Rect.Type) as Rect[];
}
bind(g: Rect): void {
g.rectGraphic.eventMode = 'static';
g.rectGraphic.cursor = 'pointer';
g.rectGraphic.hitArea = new RectGraphicHitArea(g);
g.on('_rightclick', this.onContextMenu, this);
g.on('selected', this.onSelected, this);
g.on('unselected', this.onUnselected, this);
}
unbind(g: Rect): void {
g.off('_rightclick', this.onContextMenu, this);
g.off('selected', this.onSelected, this);
g.off('unselected', this.onUnselected, this);
}
onContextMenu(e: FederatedMouseEvent) {
const target = e.target as DisplayObject;
const rect = target.getGraphic() as Rect;
this.app.updateSelected(rect);
addWaySegmentingConfig.handler = () => {
const linePoints = rect.linePoints;
const p = rect.screenToLocalPoint(e.global);
const { start, end } = getWayLineIndex(linePoints, p);
addPolygonSegmentingPoint(rect, start, end);
};
clearWaypointsConfig.handler = () => {
clearWayPoint(rect, false);
};
RectEditMenu.open(e.global);
}
onSelected(g: DisplayObject): void {
const rect = g as Rect;
let lep = rect.getAssistantAppend<PolylineEditPlugin>(
PolylineEditPlugin.Name
);
if (!lep) {
lep = new PolylineEditPlugin(rect, { onEditPointCreate });
rect.addAssistantAppend(lep);
}
lep.showAll();
}
onUnselected(g: DisplayObject): void {
const rect = g as Rect;
const lep = rect.getAssistantAppend<PolylineEditPlugin>(
PolylineEditPlugin.Name
);
if (lep) {
lep.hideAll();
}
}
}

View File

View File

@ -0,0 +1,101 @@
import { Color, Graphics, IPointData, Point } from 'pixi.js';
import {
GraphicData,
JlGraphic,
JlGraphicTemplate,
VectorText,
} from 'src/jl-graphic';
export interface IStationData extends GraphicData {
get code(): string; // 编号
set code(v: string);
get hasCircle(): boolean; // 是否有圆圈--线网图
set hasCircle(v: boolean);
get radius(): number; // 半径
set radius(v: number);
get borderWidth(): number; // 线宽
set borderWidth(v: number);
get borderColor(): string; // 圆边框线色
set borderColor(v: string);
get fillColor(): string; // 圆填充颜色
set fillColor(v: string);
get codeColor(): string; // 车站字体颜色
set codeColor(v: string);
get codeFontSize(): number; // 车站字体大小
set codeFontSize(v: number);
get point(): IPointData; // 位置坐标
set point(point: IPointData);
get circlePoint(): IPointData; // 位置坐标
set circlePoint(point: IPointData);
clone(): IStationData;
copyFrom(data: IStationData): void;
eq(other: IStationData): boolean;
}
export class Station extends JlGraphic {
static Type = 'station';
codeGraph: VectorText = new VectorText(''); //站台旁数字、字符
circleGraphic: Graphics;
constructor() {
super(Station.Type);
this.circleGraphic = new Graphics();
this.addChild(this.codeGraph);
this.addChild(this.circleGraphic);
}
get datas(): IStationData {
return this.getDatas<IStationData>();
}
doRepaint(): void {
this.circleGraphic.clear();
this.position.set(this.datas.point.x, this.datas.point.y);
const codeGraph = this.codeGraph;
if (this.datas.code == '') {
codeGraph.text = '车站Station';
} else {
codeGraph.text = this.datas.code;
}
codeGraph.style.fill = this.datas.codeColor;
codeGraph.setVectorFontSize(this.datas.codeFontSize);
codeGraph.anchor.set(0.5);
if (this.datas.hasCircle) {
const circleGraphic = this.circleGraphic;
circleGraphic.lineStyle(
this.datas.borderWidth,
new Color(this.datas.borderColor)
);
circleGraphic.beginFill(this.datas.fillColor, 1);
circleGraphic.drawCircle(0, 0, this.datas.radius);
circleGraphic.endFill;
circleGraphic.position.set(
this.datas.circlePoint.x,
this.datas.circlePoint.y
);
}
}
}
export class StationTemplate extends JlGraphicTemplate<Station> {
hasCircle: boolean;
radius: number;
borderWidth: number;
borderColor: string;
fillColor: string;
codeColor: string;
codeFontSize: number;
circlePoint: IPointData;
constructor() {
super(Station.Type);
this.hasCircle = false;
this.radius = 5;
this.borderWidth = 1;
this.borderColor = '0xff0000';
this.fillColor = '0xff0000';
this.codeColor = '0xF48815';
this.codeFontSize = 22;
this.circlePoint = new Point(0, -20);
}
new(): Station {
return new Station();
}
}

View File

@ -0,0 +1,96 @@
import { FederatedPointerEvent, Point } from 'pixi.js';
import {
GraphicDrawAssistant,
GraphicInteractionPlugin,
JlDrawApp,
JlGraphic,
VectorText,
} from 'src/jl-graphic';
import { IStationData, Station, StationTemplate } from './Station';
export interface IStationDrawOptions {
newData: () => IStationData;
}
export class StationDraw extends GraphicDrawAssistant<
StationTemplate,
IStationData
> {
point: Point = new Point(0, 0);
codeGraph: VectorText = new VectorText('');
constructor(app: JlDrawApp, createData: () => IStationData) {
super(app, new StationTemplate(), createData, Station.Type, '车站Station');
this.container.addChild(this.codeGraph);
stationInteraction.init(app);
}
bind(): void {
super.bind();
}
unbind(): void {
super.unbind();
}
clearCache(): void {
//this.codeGraph.clear();
}
onRightClick(): void {
this.createAndStore(true);
}
onLeftDown(e: FederatedPointerEvent): void {
const { x, y } = this.toCanvasCoordinates(e.global);
const p = new Point(x, y);
this.point = p;
this.createAndStore(true);
}
redraw(p: Point): void {
const codeGraph = this.codeGraph;
codeGraph.text = '车站Station';
codeGraph.anchor.set(0.5);
codeGraph.style.fill = '0xf48815';
codeGraph.position.set(p.x, p.y);
codeGraph.setVectorFontSize(22);
}
prepareData(data: IStationData): boolean {
const template = this.graphicTemplate;
data.point = this.point;
data.hasCircle = template.hasCircle;
data.radius = template.radius;
data.borderWidth = template.borderWidth;
data.borderColor = template.borderColor;
data.fillColor = template.fillColor;
data.codeColor = template.codeColor;
data.codeFontSize = template.codeFontSize;
data.circlePoint = template.circlePoint;
return true;
}
}
export class stationInteraction extends GraphicInteractionPlugin<Station> {
static Name = 'station_transform';
constructor(app: JlDrawApp) {
super(stationInteraction.Name, app);
}
static init(app: JlDrawApp) {
return new stationInteraction(app);
}
filter(...grahpics: JlGraphic[]): Station[] | undefined {
return grahpics
.filter((g) => g.type === Station.Type)
.map((g) => g as Station);
}
bind(g: Station): void {
g.eventMode = 'static';
g.cursor = 'pointer';
g.scalable = true;
g.rotatable = true;
}
unbind(g: Station): void {
g.eventMode = 'none';
g.scalable = false;
g.rotatable = false;
}
}

185
src/graphics/train/Train.ts Normal file
View File

@ -0,0 +1,185 @@
import { Color, Graphics, IPointData } from 'pixi.js';
import {
GraphicData,
JlGraphic,
JlGraphicTemplate,
VectorText,
} from 'src/jl-graphic';
export interface ITrainData extends GraphicData {
get code(): string; // 车号
set code(v: string);
get codeColor(): string; // 车号颜色
set codeColor(v: string);
get codeFontSize(): number; // 车号字体大小
set codeFontSize(v: number);
get trainDirection(): string; // 行驶方向
set trainDirection(v: string);
get hasBorder(): boolean; // 是否有边框
set hasBorder(v: boolean);
get borderWidth(): number; // 边框线宽
set borderWidth(v: number);
get borderColor(): string; // 边框颜色
set borderColor(v: string);
get headColor(): string; // 箭头颜色
set headColor(v: string);
get bodyColor(): string; // 背景色
set bodyColor(v: string);
get point(): IPointData; // 位置坐标
set point(point: IPointData);
clone(): ITrainData;
copyFrom(data: ITrainData): void;
eq(other: ITrainData): boolean;
}
// 列车颜色
export enum TrainColorEnum {
headColor = '0x00FF00', // 箭头颜色
bodyColor = '0xA388B1', // 背景色
codeColor = '0xffffff', // 车号颜色
borderColor = '0xA3E198', // 边框的颜色
}
export const trainConsts = {
borderWidth: 1,
codeFontSize: 22,
marginX: 2, // 图形x轴边距
pauseW: 2, // 停止框宽度
};
export class Train extends JlGraphic {
static Type = 'Train';
arrowLeft: Graphics;
pauseLeft: Graphics;
arrowRight: Graphics;
pauseRight: Graphics;
codeRact: Graphics;
codeGraph: VectorText = new VectorText(''); //车号
constructor() {
super(Train.Type);
this.arrowLeft = new Graphics();
this.pauseLeft = new Graphics();
this.arrowRight = new Graphics();
this.pauseRight = new Graphics();
this.codeRact = new Graphics();
this.addChild(this.arrowLeft);
this.addChild(this.pauseLeft);
this.addChild(this.arrowRight);
this.addChild(this.pauseRight);
this.addChild(this.codeRact);
this.addChild(this.codeGraph);
this.codeGraph.setVectorFontSize(trainConsts.codeFontSize);
}
get datas(): ITrainData {
return this.getDatas<ITrainData>();
}
doRepaint(): void {
this.position.set(this.datas.point.x, this.datas.point.y);
const codeGraph = this.codeGraph;
const codeRact = this.codeRact;
if (this.datas.code == '') {
codeGraph.text = '01110111';
} else {
codeGraph.text = this.datas.code;
}
codeGraph.setVectorFontSize(this.datas.codeFontSize);
codeGraph.anchor.set(0.5);
const style = {
fill: this.datas.codeColor,
padding: 5,
};
codeGraph.style = style;
const {
x: codeX,
y: codeY,
width: codeWidth,
height: codeHeight,
} = codeGraph.getLocalBounds();
const marginX = trainConsts.marginX;
const pauseW = trainConsts.pauseW;
const arrowLeft = this.arrowLeft;
arrowLeft.beginFill(this.datas.headColor, 1);
arrowLeft.drawPolygon([
-codeHeight * 0.4 - marginX - pauseW - marginX - codeWidth / 2,
0,
-marginX - pauseW - marginX - codeWidth / 2,
codeHeight / 2,
-marginX - pauseW - marginX - codeWidth / 2,
-codeHeight / 2,
]);
arrowLeft.endFill();
this.pauseLeft.beginFill(this.datas.headColor, 1);
this.pauseLeft.drawRect(0, 0, pauseW, codeHeight);
this.pauseLeft.endFill();
this.pauseLeft.position.set(
-marginX - pauseW - codeWidth / 2,
-codeHeight / 2
);
this.pauseRight.beginFill(this.datas.headColor, 1);
this.pauseRight.drawRect(0, 0, pauseW, codeHeight);
this.pauseRight.endFill();
this.pauseRight.position.set(marginX + codeWidth / 2, -codeHeight / 2);
const arrowRight = this.arrowRight;
arrowRight.beginFill(this.datas.headColor, 1);
arrowRight.drawPolygon([
codeWidth / 2 + marginX + pauseW + marginX + codeHeight * 0.4,
0,
codeWidth / 2 + marginX + pauseW + marginX,
codeHeight / 2,
codeWidth / 2 + marginX + pauseW + marginX,
-codeHeight / 2,
]);
arrowRight.endFill();
if (this.datas.hasBorder) {
codeRact.visible = true;
codeRact.lineStyle(
this.datas.borderWidth,
new Color(this.datas.borderColor)
);
codeRact.beginFill(new Color(this.datas.bodyColor));
codeRact.drawRect(codeX, codeY, codeWidth, codeHeight);
codeRact.endFill();
} else {
codeRact.visible = false;
}
// 运行方向控制箭头停止显隐
if (this.datas.trainDirection == 'right') {
this.arrowLeft.visible = false;
this.arrowRight.visible = true;
this.pauseLeft.visible = false;
this.pauseRight.visible = true;
} else {
this.arrowLeft.visible = true;
this.arrowRight.visible = false;
this.pauseLeft.visible = true;
this.pauseRight.visible = false;
}
}
}
export class TrainTemplate extends JlGraphicTemplate<Train> {
trainDirection: string;
codeFontSize: number;
hasBorder: boolean;
borderWidth: number;
borderColor: string;
headColor: string;
codeColor: string;
bodyColor: string;
constructor() {
super(Train.Type);
this.trainDirection = 'left';
this.codeFontSize = trainConsts.codeFontSize;
this.hasBorder = true;
this.borderWidth = trainConsts.borderWidth;
this.borderColor = TrainColorEnum.borderColor;
this.headColor = TrainColorEnum.headColor;
this.codeColor = TrainColorEnum.codeColor;
this.bodyColor = TrainColorEnum.bodyColor;
}
new(): Train {
return new Train();
}
}

View File

@ -0,0 +1,141 @@
import { Color, FederatedPointerEvent, Graphics, Point } from 'pixi.js';
import {
GraphicDrawAssistant,
GraphicInteractionPlugin,
JlDrawApp,
JlGraphic,
VectorText,
} from 'src/jl-graphic';
import { ITrainData, Train, TrainTemplate, trainConsts } from './Train';
export interface ITrainDrawOptions {
newData: () => ITrainData;
}
export class TrainDraw extends GraphicDrawAssistant<TrainTemplate, ITrainData> {
point: Point = new Point(0, 0);
arrowLeft: Graphics = new Graphics();
pauseLeft: Graphics = new Graphics();
codeRact: Graphics = new Graphics();
constructor(app: JlDrawApp, createData: () => ITrainData) {
super(app, new TrainTemplate(), createData, Train.Type, '列车Train');
this.container.addChild(this.arrowLeft);
this.container.addChild(this.pauseLeft);
this.container.addChild(this.codeRact);
this.graphicTemplate.hasBorder = true;
trainInteraction.init(app);
}
bind(): void {
super.bind();
}
unbind(): void {
super.unbind();
}
clearCache(): void {
this.arrowLeft.clear();
this.pauseLeft.clear();
this.codeRact.clear();
}
onRightClick(): void {
this.createAndStore(true);
}
onLeftDown(e: FederatedPointerEvent): void {
const { x, y } = this.toCanvasCoordinates(e.global);
const p = new Point(x, y);
this.point = p;
this.createAndStore(true);
}
redraw(p: Point): void {
const template = this.graphicTemplate;
const codeGraph = new VectorText(''); // 车号
codeGraph.setVectorFontSize(22);
codeGraph.anchor.set(0.5);
codeGraph.text = '01110111';
const style = { padding: 5 };
codeGraph.style = style;
const { width, height } = codeGraph.getLocalBounds();
codeGraph.destroy();
const marginX = trainConsts.marginX;
const pauseW = trainConsts.pauseW;
// 边框
if (template.hasBorder) {
const codeRact = this.codeRact;
codeRact.clear();
codeRact.lineStyle(template.borderWidth, new Color(template.borderColor));
codeRact.beginFill(new Color(template.bodyColor));
codeRact.drawRect(-width / 2, -height / 2, width, height);
codeRact.endFill();
codeRact.position.set(p.x, p.y);
}
// 箭头
const arrowLeft = this.arrowLeft;
arrowLeft.clear();
this.point.set(p.x, p.y);
arrowLeft.beginFill(template.headColor, 1);
arrowLeft.drawPolygon([
-height * 0.4 - marginX - pauseW - marginX - width / 2,
0,
-marginX - pauseW - marginX - width / 2,
height / 2,
-marginX - pauseW - marginX - width / 2,
-height / 2,
]);
arrowLeft.endFill();
arrowLeft.position.set(this.point.x, this.point.y);
// 停止框
const pauseLeft = this.pauseLeft;
pauseLeft.clear();
pauseLeft.beginFill(template.headColor, 1);
pauseLeft.drawRect(0, 0, pauseW, height);
pauseLeft.endFill();
pauseLeft.position.set(
this.point.x - marginX - pauseW - width / 2,
this.point.y - height / 2
);
}
prepareData(data: ITrainData): boolean {
const template = this.graphicTemplate;
data.code = '01110111';
data.codeColor = template.codeColor;
data.codeFontSize = template.codeFontSize;
data.hasBorder = template.hasBorder;
data.trainDirection = template.trainDirection;
data.point = this.point;
data.borderWidth = template.borderWidth;
data.borderColor = template.borderColor;
data.headColor = template.headColor;
data.bodyColor = template.bodyColor;
return true;
}
}
export class trainInteraction extends GraphicInteractionPlugin<Train> {
static Name = 'train_transform';
constructor(app: JlDrawApp) {
super(trainInteraction.Name, app);
}
static init(app: JlDrawApp) {
return new trainInteraction(app);
}
filter(...grahpics: JlGraphic[]): Train[] | undefined {
return grahpics.filter((g) => g.type === Train.Type).map((g) => g as Train);
}
bind(g: Train): void {
g.eventMode = 'static';
g.cursor = 'pointer';
g.scalable = true;
g.rotatable = true;
}
unbind(g: Train): void {
g.eventMode = 'none';
g.scalable = false;
g.rotatable = false;
}
}

View File

@ -0,0 +1,53 @@
import { Color, Graphics, IPointData } from 'pixi.js';
import { GraphicData, JlGraphic } from 'src/jl-graphic';
export interface ITrunoutData extends GraphicData {
get code(): string;
set code(code: string);
get lineWidth(): number;
set lineWidth(code: number);
get lineColor(): string;
set lineColor(code: string);
get pointA(): IPointData;
set pointA(code: IPointData);
get pointB(): IPointData;
set pointB(code: IPointData);
get pointC(): IPointData;
set pointC(code: IPointData);
clone(): ITrunoutData;
copyFrom(data: ITrunoutData): void;
eq(other: ITrunoutData): boolean;
}
export class Turnout extends JlGraphic {
static Type = 'Turnout';
graphics: {
forkPoint: {
AB: Graphics;
AC: Graphics;
};
};
constructor() {
super(Turnout.Type);
this.graphics = {
forkPoint: { AB: new Graphics(), AC: new Graphics() },
};
}
get datas(): ITrunoutData {
return this.getDatas<ITrunoutData>();
}
doRepaint(): void {
const { pointA, pointB, pointC } = this.datas;
const forkPoint: IPointData = {
x: pointA.x + (pointB.x - pointA.x),
y: pointA.y + (pointB.y - pointA.y),
};
// this.lineGraphic.clear();
// this.lineGraphic.lineStyle(this.datas.lineWidth, new Color());
}
}

View File

@ -19,6 +19,8 @@ import {
distance2, distance2,
linePoint, linePoint,
pointPolygon, pointPolygon,
calculateLineSegmentingPoint,
calculateDistanceFromPointToLine,
} from '../utils'; } from '../utils';
import { GraphicTransformEvent, ShiftData } from './GraphicTransformPlugin'; import { GraphicTransformEvent, ShiftData } from './GraphicTransformPlugin';
@ -57,6 +59,9 @@ export abstract class GraphicEditPlugin<
export const addWaypointConfig: MenuItemOptions = { export const addWaypointConfig: MenuItemOptions = {
name: '添加路径点', name: '添加路径点',
}; };
export const addWaySegmentingConfig: MenuItemOptions = {
name: '细分',
};
export const removeWaypointConfig: MenuItemOptions = { export const removeWaypointConfig: MenuItemOptions = {
name: '移除路径点', name: '移除路径点',
}; };
@ -103,6 +108,45 @@ export abstract class LineEditPlugin extends GraphicEditPlugin<ILineGraphic> {
abstract initEditPoints(): void; abstract initEditPoints(): void;
} }
export function getWayLineIndex(
points: IPointData[],
p: IPointData
): { start: number; end: number } {
let start = 0;
let end = 0;
let minDistance = 0;
for (let i = 1; i < points.length; i++) {
const sp = points[i - 1];
const ep = points[i];
let distance = calculateDistanceFromPointToLine(sp, ep, p);
distance = Math.round(distance * 100) / 100;
if (i == 1) {
minDistance = distance;
}
if (distance == minDistance) {
const minX = Math.min(sp.x, ep.x);
const maxX = Math.max(sp.x, ep.x);
const minY = Math.min(sp.y, ep.y);
const maxY = Math.max(sp.y, ep.y);
const point = calculateFootPointFromPointToLine(sp, ep, p);
if (
point.x >= minX &&
point.x <= maxX &&
point.y >= minY &&
point.y <= maxY
) {
start = i - 1;
}
}
if (distance < minDistance) {
minDistance = distance;
start = i - 1;
}
}
end = start + 1;
return { start, end };
}
export function getWaypointRangeIndex( export function getWaypointRangeIndex(
points: IPointData[], points: IPointData[],
curve: boolean, curve: boolean,
@ -251,6 +295,21 @@ export function addLineWayPoint(
graphic.linePoints = points; graphic.linePoints = points;
} }
export function addPolygonSegmentingPoint(
graphic: ILineGraphic,
start: number,
end: number,
knife = 2
) {
const linePoints = graphic.linePoints;
const points = linePoints.slice(0, start + 1);
points.push(
...calculateLineSegmentingPoint(linePoints[start], linePoints[end], knife)
);
points.push(...linePoints.slice(end));
graphic.linePoints = points;
}
function assertBezierWayPoint(i: number) { function assertBezierWayPoint(i: number) {
const c = i % 3; const c = i % 3;
if (c !== 0) { if (c !== 0) {

View File

@ -287,6 +287,31 @@ export function calculateLineMidpoint(p1: IPointData, p2: IPointData): Point {
return new Point(x, y); return new Point(x, y);
} }
/**
* 线--线
* @param p1
* @param p2
* @param knife
* @returns
*/
export function calculateLineSegmentingPoint(
p1: IPointData,
p2: IPointData,
knife: number
): IPointData[] {
const segmentingPoints: IPointData[] = [];
const x = p1.x < p2.x ? p1.x : p2.x;
const y = p1.y < p2.y ? p1.y : p2.y;
const w = Math.abs(p1.x - p2.x);
const h = Math.abs(p1.y - p2.y);
for (let i = 0; i < knife - 1; i++) {
const pointX = x + (w * (i + 1)) / knife;
const pointy = y + (h * (i + 1)) / knife;
segmentingPoints.push(new Point(pointX, pointy));
}
return segmentingPoints;
}
/** /**
* 线 * 线
* @param p1 * @param p1

184
src/layouts/DrawLayout.vue Normal file
View File

@ -0,0 +1,184 @@
<template>
<q-layout view="hHh LpR fFf">
<q-header reveal class="bg-primary text-white">
<q-toolbar>
<q-btn dense flat round icon="menu" @click="toggleLeftDrawer" />
<q-toolbar-title>
<q-avatar>
<img src="https://cdn.quasar.dev/logo-v2/svg/logo-mono-white.svg" />
</q-avatar>
Title
</q-toolbar-title>
<q-btn dense flat round icon="menu" @click="toggleRightDrawer" />
</q-toolbar>
<q-resize-observer @resize="onHeaderResize" />
</q-header>
<q-drawer v-model="leftDrawerOpen" bordered side="left">
<q-resize-observer @resize="onLeftResize" />
<!-- drawer content -->
<q-list bordered padding class="rounded-borders text-primary">
<q-item
clickable
v-ripple
:active="link === 'inbox'"
@click="link = 'inbox'"
active-class="my-menu-link"
>
<q-item-section avatar>
<q-icon name="inbox" />
</q-item-section>
<q-item-section>Inbox</q-item-section>
</q-item>
<q-item
clickable
v-ripple
:active="link === 'outbox'"
@click="link = 'outbox'"
active-class="my-menu-link"
>
<q-item-section avatar>
<q-icon name="send" />
</q-item-section>
<q-item-section>Outbox</q-item-section>
</q-item>
<q-item
clickable
v-ripple
:active="link === 'trash'"
@click="link = 'trash'"
active-class="my-menu-link"
>
<q-item-section avatar>
<q-icon name="delete" />
</q-item-section>
<q-item-section>Trash</q-item-section>
</q-item>
<q-separator spaced />
<q-item
clickable
v-ripple
:active="link === 'settings'"
@click="link = 'settings'"
active-class="my-menu-link"
>
<q-item-section avatar>
<q-icon name="settings" />
</q-item-section>
<q-item-section>Settings</q-item-section>
</q-item>
<q-item
clickable
v-ripple
:active="link === 'help'"
@click="link = 'help'"
active-class="my-menu-link"
>
<q-item-section avatar>
<q-icon name="help" />
</q-item-section>
<q-item-section>Help</q-item-section>
</q-item>
</q-list>
</q-drawer>
<q-drawer show-if-above bordered v-model="rightDrawerOpen" side="right">
<q-resize-observer @resize="onRightResize" />
<!-- drawer content -->
<draw-properties></draw-properties>
</q-drawer>
<q-page-container>
<div id="draw-app-container"></div>
</q-page-container>
</q-layout>
</template>
<script setup lang="ts">
import DrawProperties from 'src/components/draw-app/DrawProperties.vue';
import { getDrawApp, loadDrawDatas } from 'src/drawApp';
import { useDrawStore } from 'src/stores/draw-store';
import { onMounted, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router';
console.log(useRoute().fullPath);
const drawStore = useDrawStore();
const leftDrawerOpen = ref(false);
const rightDrawerOpen = ref(false);
function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
onResize();
}
function toggleRightDrawer() {
rightDrawerOpen.value = !rightDrawerOpen.value;
onResize();
}
const link = ref('outbox');
onMounted(() => {
console.log('绘制应用layout mounted');
const dom = document.getElementById('draw-app-container');
if (dom) {
const drawApp = drawStore.initDrawApp(dom);
loadDrawDatas(drawApp);
onResize();
}
});
const canvasWidth = ref(0);
const canvasHeight = ref(0);
const headerHeight = ref(0);
const leftWidth = ref(0);
const rightWidth = ref(0);
function onHeaderResize(size: { height: number; width: number }) {
headerHeight.value = size.height;
onResize();
}
function onLeftResize(size: { height: number; width: number }) {
leftWidth.value = size.width;
onResize();
}
function onRightResize(size: { height: number; width: number }) {
rightWidth.value = size.width;
onResize();
}
function onResize() {
const clientWidth = document.body.clientWidth;
const clientHeight = document.body.clientHeight;
canvasWidth.value =
clientWidth -
(leftDrawerOpen.value ? leftWidth.value : 0) -
(rightDrawerOpen.value ? rightWidth.value : 0);
canvasHeight.value = clientHeight - headerHeight.value;
const dom = document.getElementById('draw-app-container');
if (dom) {
dom.style.width = canvasWidth.value + 'px';
dom.style.height = canvasHeight.value + 'px';
}
const drawApp = getDrawApp();
if (drawApp) {
drawApp.onDomResize(canvasWidth.value, canvasHeight.value);
}
}
onUnmounted(() => {
drawStore.destroy();
});
</script>

File diff suppressed because it is too large Load Diff

View File

@ -46,6 +46,11 @@ const routes: RouteRecordRaw[] = [
}, },
], ],
}, },
{
path: '/painting/:id',
name: 'painting',
component: () => import('layouts/DrawLayout.vue'),
},
// Always leave this as last one, // Always leave this as last one,
// but you can also remove it // but you can also remove it

78
src/stores/draw-store.ts Normal file
View File

@ -0,0 +1,78 @@
import { defineStore } from 'pinia';
import { destroyDrawApp, getDrawApp, initDrawApp } from 'src/drawApp';
import { DrawAssistant, JlCanvas, JlDrawApp, JlGraphic } from 'src/jl-graphic';
export const useDrawStore = defineStore('draw', {
state: () => ({
drawAssistant: null as DrawAssistant | null,
selectedGraphics: null as JlGraphic[] | null,
}),
getters: {
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 {
const app = getDrawApp();
if (app == null) {
throw new Error('未初始化app');
}
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 (Object.hasOwn(plugin, '__GraphicDrawAssistant')) {
this.drawAssistant = plugin as DrawAssistant;
} else {
this.drawAssistant = null;
}
}
});
app.on('graphicselectedchange', () => {
this.selectedGraphics = app.selectedGraphics;
});
this.selectedGraphics = [];
return app;
},
destroy() {
// console.log('绘制状态清空,绘制应用销毁');
this.drawAssistant = null;
this.selectedGraphics = null;
destroyDrawApp();
},
},
});

@ -1 +1 @@
Subproject commit 9aa54608cd592ff96fc506e310fb19caff67f809 Subproject commit d3082562ac7e602c4993aea9c8c19561ef5cd449

View File

@ -2699,6 +2699,11 @@ process-nextick-args@~2.0.0:
resolved "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" resolved "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
protoc-gen-ts@^0.8.6:
version "0.8.6"
resolved "https://registry.npmmirror.com/protoc-gen-ts/-/protoc-gen-ts-0.8.6.tgz#e789a6fc3fbe09bdc119acecc349b9554ec5940e"
integrity sha512-66oeorGy4QBvYjQGd/gaeOYyFqKyRmRgTpofmnw8buMG0P7A0jQjoKSvKJz5h5tNUaVkIzvGBUTRVGakrhhwpA==
proxy-addr@~2.0.7: proxy-addr@~2.0.7:
version "2.0.7" version "2.0.7"
resolved "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" resolved "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"