web-map/src/services/editor.service.ts

248 lines
7.7 KiB
TypeScript
Raw Normal View History

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-28 20:04:46 +08:00
import { CanvasLayer, EditType, LockState, Meta2d } from '@meta2d/core';
2025-04-27 00:05:18 +08:00
import { useObservable } from '@vueuse/rxjs';
2025-04-28 00:43:33 +08:00
import { cloneDeep, get, pick } from 'lodash-es';
2025-04-27 00:05:18 +08:00
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-28 20:04:46 +08:00
public override find(target: string): MapPen[] {
return super.find(target);
}
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 = {
2025-04-28 00:43:33 +08:00
...p,
...this.#mapPoint(type),
2025-04-28 20:04:46 +08:00
...this.#mapPointImage(type),
2025-04-20 00:49:14 +08:00
name: 'point',
2025-04-28 20:04:46 +08:00
tags: ['point', `point-${type}`],
label: 'POINT',
2025-04-20 00:49:14 +08:00
point: { type },
};
2025-04-28 00:43:33 +08:00
const { x, y, width, height } = this.getPenRect(pen);
pen.x = x - width / 2;
pen.y = y - height / 2;
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
}
2025-04-28 00:43:33 +08:00
2025-04-28 20:04:46 +08:00
#mapPoint(type: MapPointType): Required<Pick<MapPen, 'width' | 'height' | 'lineWidth' | 'iconSize'>> {
2025-04-28 00:43:33 +08:00
const width = type < 10 ? 24 : 48;
const height = type < 10 ? 24 : 60;
const lineWidth = type < 10 ? 2 : 3;
2025-04-28 20:04:46 +08:00
const iconSize = type < 10 ? 4 : 10;
return { width, height, lineWidth, iconSize };
}
#mapPointImage(type: MapPointType): Required<Pick<MapPen, 'image' | 'canvasLayer'>> {
const theme = this.data().theme;
2025-04-28 00:43:33 +08:00
const image = type < 10 ? '' : `/point/${type}-${theme}.png`;
2025-04-28 20:04:46 +08:00
return { image, canvasLayer: CanvasLayer.CanvasMain };
2025-04-28 00:43:33 +08:00
}
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',
2025-04-28 20:04:46 +08:00
tags: ['area', `area-${type}`],
2025-04-27 00:05:18 +08:00
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-28 20:04:46 +08:00
(v) => this.#load(v),
2025-04-20 00:49:14 +08:00
{ immediate: true },
);
}
2025-04-28 20:04:46 +08:00
#load(theme?: string): void {
if (theme) {
this.setTheme(theme);
}
this.find('point').forEach((pen) => {
if (!pen.point?.type) return;
if (pen.point.type < 10) return;
this.canvas.updateValue(pen, this.#mapPointImage(pen.point.type));
});
this.render();
}
2025-04-20 00:49:14 +08:00
// eslint-disable-next-line @typescript-eslint/no-explicit-any
#listen(e: unknown, v: any) {
switch (e) {
2025-04-28 20:04:46 +08:00
case 'opened':
this.#load();
break;
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.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 {
2025-04-28 00:43:33 +08:00
const theme = sTheme.editor;
2025-04-28 20:04:46 +08:00
const { active, iconSize: r = 0, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {};
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
2025-04-20 00:49:14 +08:00
const { type } = pen.point ?? {};
2025-04-28 00:43:33 +08:00
const { label = '' } = pen ?? {};
2025-04-27 00:05:18 +08:00
ctx.save();
2025-04-28 00:43:33 +08:00
switch (type) {
case MapPointType.:
case MapPointType.:
case MapPointType.:
case MapPointType.:
2025-04-28 20:04:46 +08:00
ctx.beginPath();
ctx.moveTo(x + w / 2 - r, y + r);
ctx.arcTo(x + w / 2, y, x + w - r, y + h / 2 - r, r);
ctx.arcTo(x + w, y + h / 2, x + w / 2 + r, y + h - r, r);
ctx.arcTo(x + w / 2, y + h, x + r, y + h / 2 + r, r);
ctx.arcTo(x, y + h / 2, x + r, y + h / 2 - r, r);
ctx.closePath();
ctx.fillStyle = get(theme, `point-s.fill-${type}`) ?? '';
ctx.fill();
ctx.strokeStyle = get(theme, active ? 'point-s.strokeActive' : 'point-s.stroke') ?? '';
if (type === MapPointType.) {
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x + 0.66 * r, y + h / 2 - 0.66 * r);
ctx.lineTo(x + r, y + h / 2 - r);
ctx.moveTo(x + w / 2 - 0.66 * r, y + 0.66 * r);
ctx.lineTo(x + w / 2 - r, y + r);
ctx.moveTo(x + w / 2 + 0.66 * r, y + 0.66 * r);
ctx.lineTo(x + w / 2 + r, y + r);
ctx.moveTo(x + w - 0.66 * r, y + h / 2 - 0.66 * r);
ctx.lineTo(x + w - r, y + h / 2 - r);
ctx.moveTo(x + w - 0.66 * r, y + h / 2 + 0.66 * r);
ctx.lineTo(x + w - r, y + h / 2 + r);
ctx.moveTo(x + w / 2 + 0.66 * r, y + h - 0.66 * r);
ctx.lineTo(x + w / 2 + r, y + h - r);
ctx.moveTo(x + w / 2 - 0.66 * r, y + h - 0.66 * r);
ctx.lineTo(x + w / 2 - r, y + h - r);
ctx.moveTo(x + 0.66 * r, y + h / 2 + 0.66 * r);
ctx.lineTo(x + r, y + h / 2 + r);
}
ctx.stroke();
2025-04-28 00:43:33 +08:00
break;
case MapPointType.:
case MapPointType.:
case MapPointType.:
case MapPointType.:
case MapPointType.:
2025-04-28 20:04:46 +08:00
ctx.roundRect(x, y, w, h, r);
2025-04-28 00:43:33 +08:00
ctx.strokeStyle = get(theme, active ? 'point-l.strokeActive' : 'point-l.stroke') ?? '';
ctx.stroke();
break;
default:
break;
}
ctx.fillStyle = get(theme, 'color') ?? '';
2025-04-28 20:04:46 +08:00
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
2025-04-28 00:43:33 +08:00
ctx.textAlign = 'center';
2025-04-28 20:04:46 +08:00
ctx.textBaseline = 'top';
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
2025-04-27 00:05:18 +08:00
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