2025-04-27 00:05:18 +08:00
|
|
|
import { EDITOR_CONFIG, MapAreaType, type MapPen, MapPointType } from '@api/map';
|
2025-05-05 01:06:09 +08:00
|
|
|
import type { RobotGroup, RobotInfo } from '@api/robot';
|
|
|
|
import type { SceneData } from '@api/scene';
|
2025-04-20 00:49:14 +08:00
|
|
|
import sTheme from '@core/theme.service';
|
2025-05-05 23:21:31 +08:00
|
|
|
import { CanvasLayer, EditType, LockState, Meta2d, type Pen, s8 } from '@meta2d/core';
|
2025-04-27 00:05:18 +08:00
|
|
|
import { useObservable } from '@vueuse/rxjs';
|
2025-05-05 23:21:31 +08:00
|
|
|
import { clone, cloneDeep, get, isNil, isString, pick, remove, some } from 'lodash-es';
|
2025-05-05 01:06:09 +08:00
|
|
|
import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs';
|
2025-05-05 23:21:31 +08:00
|
|
|
import { reactive, watch } from 'vue';
|
2025-04-20 00:49:14 +08:00
|
|
|
|
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 {
|
2025-05-05 23:21:31 +08:00
|
|
|
public async load(map?: string, editable = false): Promise<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);
|
2025-05-05 23:21:31 +08:00
|
|
|
this.setState(editable);
|
2025-04-20 00:49:14 +08:00
|
|
|
}
|
|
|
|
public save(): string {
|
2025-04-27 00:05:18 +08:00
|
|
|
const data = this.data();
|
2025-05-05 23:21:31 +08:00
|
|
|
return JSON.stringify(data);
|
2025-04-20 00:49:14 +08:00
|
|
|
}
|
2025-05-05 23:21:31 +08:00
|
|
|
public export(): void {
|
|
|
|
const json = this.save();
|
|
|
|
console.log(json);
|
|
|
|
}
|
|
|
|
|
|
|
|
public setState(editable?: boolean): void {
|
|
|
|
this.lock(editable ? LockState.None : LockState.Disable);
|
|
|
|
}
|
|
|
|
|
|
|
|
public override data(): SceneData {
|
|
|
|
return super.data();
|
2025-04-20 00:49:14 +08:00
|
|
|
}
|
|
|
|
|
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-05-05 01:06:09 +08:00
|
|
|
//#region 机器人
|
2025-05-05 23:21:31 +08:00
|
|
|
readonly #robotMap = reactive<Map<RobotInfo['id'], RobotInfo>>(new Map());
|
2025-05-05 01:06:09 +08:00
|
|
|
public get robots(): RobotInfo[] {
|
|
|
|
return Array.from(this.#robotMap.values());
|
|
|
|
}
|
|
|
|
|
|
|
|
public getRobotById(id: RobotInfo['id']): RobotInfo | undefined {
|
|
|
|
return this.#robotMap.get(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
public addRobots(gid: RobotInfo['gid'], robots: RobotInfo[]): void {
|
|
|
|
const groups = clone(this.#robotGroups$$.value);
|
|
|
|
const group = groups.find((v) => v.id === gid);
|
|
|
|
if (isNil(group)) throw Error('未找到目标机器人组');
|
|
|
|
group.robots ??= [];
|
|
|
|
robots.forEach((v) => {
|
|
|
|
if (this.#robotMap.has(v.id)) return;
|
|
|
|
this.#robotMap.set(v.id, { ...v, gid });
|
|
|
|
group.robots?.push(v.id);
|
|
|
|
});
|
|
|
|
this.#robotGroups$$.next(groups);
|
|
|
|
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
|
|
|
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
|
|
|
}
|
2025-05-05 23:21:31 +08:00
|
|
|
public removeRobots(ids: RobotInfo['id'][]): void {
|
|
|
|
ids?.forEach((v) => this.#robotMap.delete(v));
|
|
|
|
const groups = clone(this.#robotGroups$$.value);
|
|
|
|
groups.forEach(({ robots }) => remove(robots ?? [], (v) => !this.#robotMap.has(v)));
|
|
|
|
this.#robotGroups$$.next(groups);
|
|
|
|
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
|
|
|
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
|
|
|
}
|
|
|
|
public updateRobots(ids: RobotInfo['id'][], values: Partial<RobotInfo>): void {
|
|
|
|
ids?.forEach((v) => {
|
|
|
|
const robot = this.#robotMap.get(v);
|
|
|
|
if (isNil(robot)) return;
|
|
|
|
this.#robotMap.set(v, { ...robot, ...values });
|
|
|
|
});
|
|
|
|
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
|
|
|
}
|
2025-05-05 01:06:09 +08:00
|
|
|
|
|
|
|
readonly #robotGroups$$ = new BehaviorSubject<RobotGroup[]>([]);
|
|
|
|
public readonly robotGroups = useObservable<RobotGroup[]>(this.#robotGroups$$.pipe(debounceTime(300)));
|
|
|
|
|
|
|
|
public createRobotGroup(): void {
|
|
|
|
const id = s8();
|
|
|
|
const label = `RG-${id}`;
|
|
|
|
const groups = clone(this.#robotGroups$$.value);
|
|
|
|
groups.push({ id, label });
|
|
|
|
this.#robotGroups$$.next(groups);
|
|
|
|
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
|
|
|
}
|
|
|
|
public deleteRobotGroup(id: RobotGroup['id']): void {
|
|
|
|
const groups = clone(this.#robotGroups$$.value);
|
|
|
|
const group = groups.find((v) => v.id === id);
|
|
|
|
group?.robots?.forEach((v) => this.#robotMap.delete(v));
|
|
|
|
remove(groups, group);
|
|
|
|
this.#robotGroups$$.next(groups);
|
|
|
|
(<SceneData>this.store.data).robots = [...this.#robotMap.values()];
|
|
|
|
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
|
|
|
}
|
|
|
|
public updateRobotGroupLabel(id: RobotGroup['id'], label: RobotGroup['label']): void {
|
|
|
|
const groups = this.#robotGroups$$.value;
|
|
|
|
const group = groups.find((v) => v.id === id);
|
|
|
|
if (isNil(group)) throw Error('未找到目标机器人组');
|
|
|
|
if (some(groups, ['label', label])) throw Error('机器人组名称已经存在');
|
|
|
|
group.label = label;
|
|
|
|
this.#robotGroups$$.next([...groups]);
|
|
|
|
(<SceneData>this.store.data).robotGroups = this.#robotGroups$$.value;
|
|
|
|
}
|
|
|
|
//#endregion
|
|
|
|
|
2025-05-05 23:21:31 +08:00
|
|
|
readonly #change$$ = new Subject<boolean>();
|
|
|
|
public readonly selected = useObservable<string[], string[]>(
|
|
|
|
this.#change$$.pipe(
|
|
|
|
filter((v) => !v),
|
|
|
|
debounceTime(100),
|
|
|
|
map(() => this.store.active?.map(({ id }) => id).filter((v) => !isNil(v)) ?? []),
|
|
|
|
),
|
|
|
|
{ initialValue: new Array<string>() },
|
|
|
|
);
|
|
|
|
public readonly pens = useObservable<MapPen[]>(
|
|
|
|
this.#change$$.pipe(
|
|
|
|
filter((v) => v),
|
|
|
|
debounceTime(100),
|
|
|
|
map(() => this.data().pens),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
public override find(target: string): MapPen[] {
|
|
|
|
return super.find(target);
|
|
|
|
}
|
|
|
|
|
|
|
|
public override active(target: string | Pen[], emit?: boolean): void {
|
|
|
|
const pens = isString(target) ? this.find(target) : target;
|
|
|
|
super.active(pens, emit);
|
|
|
|
this.render();
|
|
|
|
}
|
|
|
|
public override inactive(): void {
|
|
|
|
super.inactive();
|
|
|
|
this.render();
|
|
|
|
}
|
|
|
|
|
2025-04-20 00:49:14 +08:00
|
|
|
//#region 点位
|
2025-05-05 23:21:31 +08:00
|
|
|
public readonly points = useObservable<MapPen[], MapPen[]>(
|
|
|
|
this.#change$$.pipe(
|
|
|
|
filter((v) => v),
|
|
|
|
debounceTime(100),
|
|
|
|
map(() => this.find('point')),
|
|
|
|
),
|
|
|
|
{ initialValue: new Array<MapPen>() },
|
|
|
|
);
|
|
|
|
|
2025-04-27 00:05:18 +08:00
|
|
|
public async addPoint(p: Point, type = MapPointType.普通点): Promise<void> {
|
2025-05-05 01:06:09 +08:00
|
|
|
const id = s8();
|
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-05-05 01:06:09 +08:00
|
|
|
id,
|
2025-04-20 00:49:14 +08:00
|
|
|
name: 'point',
|
2025-04-28 20:04:46 +08:00
|
|
|
tags: ['point', `point-${type}`],
|
2025-05-05 01:06:09 +08:00
|
|
|
label: `P-${id}`,
|
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);
|
2025-05-05 23:21:31 +08:00
|
|
|
// 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-05-05 23:21:31 +08:00
|
|
|
#load(theme: string): void {
|
|
|
|
this.setTheme(theme);
|
2025-04-28 20:04:46 +08:00
|
|
|
|
2025-05-05 01:06:09 +08:00
|
|
|
const { robots, robotGroups } = this.data();
|
|
|
|
this.#robotMap.clear();
|
|
|
|
robots?.forEach((r) => this.#robotMap.set(r.id, r));
|
|
|
|
this.#robotGroups$$.next(robotGroups ?? []);
|
|
|
|
|
2025-04-28 20:04:46 +08:00
|
|
|
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':
|
2025-05-05 23:21:31 +08:00
|
|
|
this.#load(sTheme.theme);
|
|
|
|
this.#change$$.next(true);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'add':
|
|
|
|
case 'delete':
|
|
|
|
case 'update':
|
|
|
|
case 'valueUpdate':
|
|
|
|
this.#change$$.next(true);
|
2025-04-28 20:04:46 +08:00
|
|
|
break;
|
|
|
|
|
2025-05-05 23:21:31 +08:00
|
|
|
case 'active':
|
|
|
|
case 'inactive':
|
|
|
|
this.#change$$.next(false);
|
|
|
|
break;
|
|
|
|
|
|
|
|
// case 'undo':
|
|
|
|
// case 'redo':
|
|
|
|
// {
|
|
|
|
// const { type, pens = [], initPens = [] } = data ?? {};
|
|
|
|
// switch (type) {
|
|
|
|
// case EditType.Add:
|
|
|
|
// pens?.forEach((pen: SceneMapPen) => {
|
|
|
|
// if (pen.name === 'point') {
|
|
|
|
// const { image } = this.#mapPointType(pen.point!.type);
|
|
|
|
// const rect = this.#editor?.getPenRect(pen);
|
|
|
|
// this.#editor?.setValue({ id: pen.id, image, ...rect }, { render: true, history: false });
|
|
|
|
// }
|
|
|
|
// });
|
|
|
|
// break;
|
|
|
|
// case EditType.Update:
|
|
|
|
// (event === 'undo' ? initPens : pens)?.forEach((pen: SceneMapPen) => {
|
|
|
|
// if (pen.name === 'point') {
|
|
|
|
// const { image } = this.#mapPointType(pen.point!.type);
|
|
|
|
// this.#editor?.setValue({ id: pen.id, image }, { render: true, history: false });
|
|
|
|
// }
|
|
|
|
// });
|
|
|
|
// break;
|
|
|
|
// default:
|
|
|
|
// break;
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// 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-05-05 23:21:31 +08:00
|
|
|
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
|