2025-04-27 00:05:18 +08:00
|
|
|
import { EDITOR_CONFIG, MapAreaType, type MapPen, MapPointType } from '@api/map';
|
2025-04-20 00:49:14 +08:00
|
|
|
import sTheme from '@core/theme.service';
|
2025-04-27 00:05:18 +08:00
|
|
|
import { EditType, LockState, Meta2d } from '@meta2d/core';
|
|
|
|
import { useObservable } from '@vueuse/rxjs';
|
2025-04-20 00:49:14 +08:00
|
|
|
import THEME from 'asset/themes/editor.json';
|
2025-04-27 00:05:18 +08:00
|
|
|
import { cloneDeep, pick } from 'lodash-es';
|
|
|
|
import { debounceTime, filter, map, Subject, switchMap } from 'rxjs';
|
2025-04-20 00:49:14 +08:00
|
|
|
import { watch } from 'vue';
|
|
|
|
|
2025-04-27 00:05:18 +08:00
|
|
|
export type Point = Record<'x' | 'y', number>;
|
2025-04-20 00:49:14 +08:00
|
|
|
|
2025-04-27 00:05:18 +08:00
|
|
|
export class EditorService extends Meta2d {
|
|
|
|
public load(map?: string, readonly = false): void {
|
2025-04-20 00:49:14 +08:00
|
|
|
const data = map ? JSON.parse(map) : undefined;
|
2025-04-27 00:05:18 +08:00
|
|
|
this.open(data);
|
|
|
|
this.lock(readonly ? LockState.Disable : LockState.None);
|
2025-04-20 00:49:14 +08:00
|
|
|
}
|
|
|
|
public save(): string {
|
2025-04-27 00:05:18 +08:00
|
|
|
const data = this.data();
|
2025-04-20 00:49:14 +08:00
|
|
|
const map = JSON.stringify(data);
|
|
|
|
return map;
|
|
|
|
}
|
|
|
|
public export(): string {
|
2025-04-27 00:05:18 +08:00
|
|
|
const png = this.toPng(10);
|
2025-04-20 00:49:14 +08:00
|
|
|
return png;
|
|
|
|
}
|
|
|
|
|
2025-04-27 00:05:18 +08:00
|
|
|
readonly #mouse$$ = new Subject<{ type: 'click' | 'mousedown' | 'mouseup'; value: Point }>();
|
|
|
|
public readonly mouseClick = useObservable<Point>(
|
|
|
|
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]),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
2025-04-20 00:49:14 +08:00
|
|
|
//#region 点位
|
2025-04-27 00:05:18 +08:00
|
|
|
public async addPoint(p: Point, type = MapPointType.普通点): Promise<void> {
|
2025-04-20 00:49:14 +08:00
|
|
|
const pen: MapPen = {
|
|
|
|
name: 'point',
|
2025-04-27 00:05:18 +08:00
|
|
|
x: p.x,
|
|
|
|
y: p.y,
|
2025-04-20 00:49:14 +08:00
|
|
|
width: 24,
|
|
|
|
height: 24,
|
|
|
|
point: { type },
|
|
|
|
};
|
2025-04-27 00:05:18 +08:00
|
|
|
await this.addPen(pen, false, true, true);
|
|
|
|
this.pushHistory({ type: EditType.Add, pens: [cloneDeep(pen)] });
|
2025-04-20 00:49:14 +08:00
|
|
|
}
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
//#region 线路
|
|
|
|
//#endregion
|
|
|
|
|
|
|
|
//#region 区域
|
2025-04-27 00:05:18 +08:00
|
|
|
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)] });
|
|
|
|
}
|
2025-04-20 00:49:14 +08:00
|
|
|
//#endregion
|
|
|
|
|
|
|
|
constructor(container: HTMLDivElement) {
|
2025-04-27 00:05:18 +08:00
|
|
|
super(container, EDITOR_CONFIG);
|
2025-04-20 00:49:14 +08:00
|
|
|
|
|
|
|
(<HTMLDivElement>container.children.item(5)).ondrop = null;
|
2025-04-27 00:05:18 +08:00
|
|
|
this.on('*', (e, v) => this.#listen(e, v));
|
2025-04-20 00:49:14 +08:00
|
|
|
this.#register();
|
|
|
|
|
|
|
|
watch(
|
|
|
|
() => sTheme.theme,
|
2025-04-27 00:05:18 +08:00
|
|
|
(v) => this.setTheme(v),
|
2025-04-20 00:49:14 +08:00
|
|
|
{ immediate: true },
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
#listen(e: unknown, v: any) {
|
|
|
|
switch (e) {
|
2025-04-27 00:05:18 +08:00
|
|
|
case 'click':
|
|
|
|
case 'mousedown':
|
|
|
|
case 'mouseup':
|
|
|
|
this.#mouse$$.next({ type: e, value: pick(v, 'x', 'y') });
|
|
|
|
break;
|
|
|
|
|
2025-04-20 00:49:14 +08:00
|
|
|
default:
|
|
|
|
// console.log(e, v);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#register() {
|
2025-04-27 00:05:18 +08:00
|
|
|
this.store.theme = THEME;
|
|
|
|
this.registerCanvasDraw({ point: drawPoint, line: drawLine, area: drawArea });
|
|
|
|
this.registerAnchors({ point: anchorPoint });
|
2025-04-20 00:49:14 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//#region 绘制函数
|
|
|
|
function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
|
|
|
|
const { x = 0, y = 0, width = 0, height = 0 } = pen.calculative?.worldRect ?? {};
|
|
|
|
const { type } = pen.point ?? {};
|
2025-04-27 00:05:18 +08:00
|
|
|
ctx.save();
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
ctx.roundRect(x, y, width, height, 4);
|
2025-04-20 00:49:14 +08:00
|
|
|
ctx.stroke();
|
2025-04-27 00:05:18 +08:00
|
|
|
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();
|
2025-04-20 00:49:14 +08:00
|
|
|
}
|
|
|
|
//#endregion
|