diff --git a/package-lock.json b/package-lock.json index b07e727..acad1a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "0.0.0", "dependencies": { "@meta2d/core": "^1.0.78", + "@vueuse/rxjs": "^13.1.0", "ant-design-vue": "^4.2.6", "axios": "^1.8.4", "lodash-es": "^4.17.21", + "rxjs": "^7.8.2", "vue": "^3.5.13", "vue-i18n": "^11.1.3", "vue-router": "^4.5.0" @@ -1769,6 +1771,34 @@ } } }, + "node_modules/@vueuse/rxjs": { + "version": "13.1.0", + "resolved": "https://registry.npmmirror.com/@vueuse/rxjs/-/rxjs-13.1.0.tgz", + "integrity": "sha512-/ItzpO8yky6K+frthSlUbBvmy4WSrKSHze/SyYPMgGGLh+xMYFUvmXQnO/daVC4fdWJN8Phb2dfa7d9WZTDQhQ==", + "license": "MIT", + "dependencies": { + "@vueuse/shared": "13.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "rxjs": ">=6.0.0", + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/shared": { + "version": "13.1.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-13.1.0.tgz", + "integrity": "sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.1.tgz", @@ -4706,7 +4736,6 @@ "version": "7.8.2", "resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -5942,7 +5971,6 @@ "version": "2.8.1", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/type-check": { diff --git a/package.json b/package.json index 67e56a4..1a2b2ca 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,11 @@ }, "dependencies": { "@meta2d/core": "^1.0.78", + "@vueuse/rxjs": "^13.1.0", "ant-design-vue": "^4.2.6", "axios": "^1.8.4", "lodash-es": "^4.17.21", + "rxjs": "^7.8.2", "vue": "^3.5.13", "vue-i18n": "^11.1.3", "vue-router": "^4.5.0" diff --git a/src/App.vue b/src/App.vue index dce10d2..6302c4a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -5,8 +5,6 @@ import sTheme from '@core/theme.service'; diff --git a/src/ant.scss b/src/ant.scss index 60deae1..e69de29 100644 --- a/src/ant.scss +++ b/src/ant.scss @@ -1,4 +0,0 @@ -.ant-app { - width: 100%; - height: 100%; -} diff --git a/src/components/robot-list.vue b/src/components/robot-list.vue new file mode 100644 index 0000000..fed9789 --- /dev/null +++ b/src/components/robot-list.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/src/pages/group-editor.vue b/src/pages/group-editor.vue new file mode 100644 index 0000000..34f22af --- /dev/null +++ b/src/pages/group-editor.vue @@ -0,0 +1,10 @@ + + + + + diff --git a/src/pages/home.vue b/src/pages/home.vue deleted file mode 100644 index 16e09c7..0000000 --- a/src/pages/home.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - - - diff --git a/src/pages/scene-editor.vue b/src/pages/scene-editor.vue new file mode 100644 index 0000000..fb9036d --- /dev/null +++ b/src/pages/scene-editor.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/services/editor.service.ts b/src/services/editor.service.ts index 106c587..5da0dbb 100644 --- a/src/services/editor.service.ts +++ b/src/services/editor.service.ts @@ -1,47 +1,62 @@ -import { EDITOR_CONFIG, type MapPen, MapPointType } from '@api/map'; +import { EDITOR_CONFIG, MapAreaType, type MapPen, MapPointType } from '@api/map'; import sTheme from '@core/theme.service'; -import { LockState, Meta2d } from '@meta2d/core'; +import { EditType, LockState, Meta2d } from '@meta2d/core'; +import { useObservable } from '@vueuse/rxjs'; import THEME from 'asset/themes/editor.json'; +import { cloneDeep, pick } from 'lodash-es'; +import { debounceTime, filter, map, Subject, switchMap } from 'rxjs'; import { watch } from 'vue'; -export class EditorService { - readonly #editor: Meta2d; +export type Point = Record<'x' | 'y', number>; - public destroy() { - this.#editor.destroy(); - } - - public resize(): void { - this.#editor.resize(); - this.#editor.render(); - } - - public open(map?: string, readonly = false): void { +export class EditorService extends Meta2d { + public load(map?: string, readonly = false): void { const data = map ? JSON.parse(map) : undefined; - this.#editor.open(data); - this.#editor.lock(readonly ? LockState.Disable : LockState.None); + this.open(data); + this.lock(readonly ? LockState.Disable : LockState.None); } public save(): string { - const data = this.#editor.data(); + const data = this.data(); const map = JSON.stringify(data); return map; } public export(): string { - const png = this.#editor.toPng(10); + const png = this.toPng(10); return png; } + readonly #mouse$$ = new Subject<{ type: 'click' | 'mousedown' | 'mouseup'; value: Point }>(); + public readonly mouseClick = useObservable( + this.#mouse$$.pipe( + filter(({ type }) => type === 'click'), + debounceTime(100), + map(({ value }) => value), + ), + ); + public readonly mouseBrush = useObservable<[Point, Point]>( + this.#mouse$$.pipe( + filter(({ type }) => type === 'mousedown'), + switchMap(({ value: s }) => + this.#mouse$$.pipe( + filter(({ type }) => type === 'mouseup'), + map(({ value: e }) => <[Point, Point]>[s, e]), + ), + ), + ), + ); + //#region 点位 - public async addPoint(x: number = 0, y: number = 0, type = MapPointType.普通点): Promise { + public async addPoint(p: Point, type = MapPointType.普通点): Promise { const pen: MapPen = { name: 'point', - x, - y, + x: p.x, + y: p.y, width: 24, height: 24, point: { type }, }; - await this.#editor.addPen(pen); + await this.addPen(pen, false, true, true); + this.pushHistory({ type: EditType.Add, pens: [cloneDeep(pen)] }); } //#endregion @@ -49,18 +64,36 @@ export class EditorService { //#endregion //#region 区域 + public async addArea(p1: Point, p2: Point, type = MapAreaType.库区) { + const scale = this.data().scale ?? 1; + const w = Math.abs(p1.x - p2.x); + const h = Math.abs(p1.y - p2.y); + if (w * scale < 50 || h * scale < 60) return; + const pen: MapPen = { + name: 'area', + x: Math.min(p1.x, p2.x), + y: Math.min(p1.y, p2.y), + width: w, + height: h, + area: { type }, + locked: LockState.DisableMoveScale, + }; + const area = await this.addPen(pen, false, true, true); + this.bottom(area); + this.pushHistory({ type: EditType.Add, pens: [cloneDeep(pen)] }); + } //#endregion constructor(container: HTMLDivElement) { - this.#editor = new Meta2d(container, EDITOR_CONFIG); + super(container, EDITOR_CONFIG); (container.children.item(5)).ondrop = null; - this.#editor.on('*', (e, v) => this.#listen(e, v)); + this.on('*', (e, v) => this.#listen(e, v)); this.#register(); watch( () => sTheme.theme, - (v) => this.#editor.setTheme(v), + (v) => this.setTheme(v), { immediate: true }, ); } @@ -68,6 +101,12 @@ export class EditorService { // eslint-disable-next-line @typescript-eslint/no-explicit-any #listen(e: unknown, v: any) { switch (e) { + case 'click': + case 'mousedown': + case 'mouseup': + this.#mouse$$.next({ type: e, value: pick(v, 'x', 'y') }); + break; + default: // console.log(e, v); break; @@ -75,8 +114,9 @@ export class EditorService { } #register() { - this.#editor.store.theme = THEME; - this.#editor.registerCanvasDraw({ point: drawPoint }); + this.store.theme = THEME; + this.registerCanvasDraw({ point: drawPoint, line: drawLine, area: drawArea }); + this.registerAnchors({ point: anchorPoint }); } } @@ -84,7 +124,34 @@ export class EditorService { function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void { const { x = 0, y = 0, width = 0, height = 0 } = pen.calculative?.worldRect ?? {}; const { type } = pen.point ?? {}; - ctx.rect(x, y, width, height); + ctx.save(); + ctx.lineWidth = 2; + ctx.roundRect(x, y, width, height, 4); ctx.stroke(); + ctx.fillStyle = '#fff'; + ctx.fillText(String(type), x + width / 2, y + height / 2); + ctx.restore(); +} +function anchorPoint(pen: MapPen): void { + pen.anchors = [{ x: 0.5, y: 0.5 }]; +} + +function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void { + const [p1, p2] = pen.calculative?.worldAnchors ?? []; + const { direction } = pen.route ?? {}; + ctx.save(); + ctx.lineWidth = 2; + ctx.restore(); +} + +function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void { + const { x = 0, y = 0, width = 0, height = 0 } = pen.calculative?.worldRect ?? {}; + const { type } = pen.area ?? {}; + ctx.save(); + ctx.lineWidth = 1; + ctx.strokeRect(x, y, width, height); + ctx.fillStyle = '#fff'; + ctx.fillText(String(type), x + width / 2, y); + ctx.restore(); } //#endregion diff --git a/src/services/router.ts b/src/services/router.ts index c8df74a..b776985 100644 --- a/src/services/router.ts +++ b/src/services/router.ts @@ -1,51 +1,17 @@ import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'; export const ROUTES = Object.freeze([ - { path: '/:pathMatch(.*)*', redirect: '/home' }, - { path: '/home', component: () => import('@/home.vue') }, - // { - // path: '/', - // component: () => import('@layout/main.vue'), - // children: [ - // { path: '', redirect: 'home' }, - // { name: '首页', path: 'home', component: () => import('@/home.vue') }, - // { name: '任务管理', path: 'task-management', component: () => import('@/home.vue') }, - // { name: '车辆管理', path: 'vehicle-management', component: () => import('@/home.vue') }, - // { - // name: '地图管理', - // path: 'map-management', - // meta: { standalone: true }, - // component: () => import('@/map-management/scene-edit.vue'), - // }, - // { name: '参数设置', path: 'parameter-setting', component: import('@/parameter-setting/index.vue') }, - // { - // name: '调度仿真', - // path: 'scheduling-emulation', - // children: [ - // { - // name: '调度模拟', - // path: 'scheduling-simulation', - // components: { - // default: () => import('@/scheduling-emulation/scheduling-simulation.vue'), - // toolbar: () => import('@common/map-switcher.vue'), - // }, - // }, - // { - // name: '车辆调试', - // path: 'vehicle-commissioning', - // component: () => import('@/scheduling-emulation/vehicle-commissioning.vue'), - // }, - // { - // name: '场景测试', - // path: 'scenario-testing', - // component: () => import('@/scheduling-emulation/scenario-testing.vue'), - // }, - // ], - // }, - // { name: '告警管理', path: 'alarm-management', component: () => import('@/home.vue') }, - // { name: '日志管理', path: 'log-management', component: () => import('@/home.vue') }, - // ], - // }, + { path: '/:pathMatch(.*)*', redirect: '/' }, + { + path: '/scene-editor/:id', + props: true, + component: () => import('@/scene-editor.vue'), + }, + { + path: '/group-editor/:id', + props: true, + component: () => import('@/group-editor.vue'), + }, ]); export const router = createRouter({ diff --git a/src/services/theme.service.ts b/src/services/theme.service.ts index e29e5b3..2327dbc 100644 --- a/src/services/theme.service.ts +++ b/src/services/theme.service.ts @@ -7,7 +7,7 @@ enum Theme { } export const THEMES = Object.freeze<[string, Theme][]>(Object.entries(Theme)); -const THEME_STORAGE_KEY = 'locale'; +const THEME_STORAGE_KEY = 'theme'; class ThemeService { #theme = ref(localStorage.getItem(THEME_STORAGE_KEY) || Theme.Dark); diff --git a/src/style.scss b/src/style.scss index 2c83ad4..828d966 100644 --- a/src/style.scss +++ b/src/style.scss @@ -5,6 +5,8 @@ body { width: 100vw; height: 100vh; margin: 0; + user-select: none; + -webkit-user-drag: none; } #app { @@ -17,3 +19,57 @@ body { height: 100%; overflow: hidden; } + +@for $i from 0 through 100 { + .size-#{$i * 2} { + font-size: $i * 2px !important; + } + + .w-#{$i * 4} { + width: $i * 4px !important; + } + + .p-#{$i * 2} { + padding: $i * 2px !important; + } + .ph-#{$i * 2} { + padding-inline: $i * 2px !important; + } + .pv-#{$i * 2} { + padding-block: $i * 2px !important; + } + .pl-#{$i * 2} { + padding-left: $i * 2px !important; + } + .pr-#{$i * 2} { + padding-right: $i * 2px !important; + } + .pt-#{$i * 2} { + padding-top: $i * 2px !important; + } + .pb-#{$i * 2} { + padding-bottom: $i * 2px !important; + } + + .m-#{$i * 2} { + margin: $i * 2px !important; + } + .mh-#{$i * 2} { + margin-inline: $i * 2px !important; + } + .mv-#{$i * 2} { + margin-block: $i * 2px !important; + } + .ml-#{$i * 2} { + margin-left: $i * 2px !important; + } + .mr-#{$i * 2} { + margin-right: $i * 2px !important; + } + .mt-#{$i * 2} { + margin-top: $i * 2px !important; + } + .mb-#{$i * 2} { + margin-bottom: $i * 2px !important; + } +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 7a8e688..11f02fe 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,7 +1 @@ /// - -declare module '*.vue' { - import type { DefineComponent } from 'vue'; - const vueComponent: DefineComponent; - export default vueComponent; -} diff --git a/tsconfig.app.json b/tsconfig.app.json index 4f4385b..816cb7f 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -16,9 +16,9 @@ "@/*": ["src/pages/*"], "@api/*": ["src/apis/*"], "@common/*": ["src/components/*"], - "@core/*": ["src/services/*"], - "@layout/*": ["src/layouts/*"] + "@core/*": ["src/services/*"] } }, - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], + "files": ["components.d.ts"] } diff --git a/vite.config.ts b/vite.config.ts index 8474842..e0dfc5b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,7 +6,13 @@ import { defineConfig } from 'vite'; export default ({ mode }: Record) => defineConfig({ - plugins: [vue(), Components({ resolvers: [AntDesignVueResolver({ importStyle: false, resolveIcons: true })] })], + plugins: [ + vue(), + Components({ + dts: true, + resolvers: [AntDesignVueResolver({ importStyle: false, resolveIcons: true })], + }), + ], base: '/', envPrefix: 'ENV_', resolve: { @@ -16,7 +22,6 @@ export default ({ mode }: Record) => '@api': resolve(__dirname, 'src/apis/'), '@common': resolve(__dirname, 'src/components/'), '@core': resolve(__dirname, 'src/services/'), - '@layout': resolve(__dirname, 'src/layouts/'), }, }, css: {