web-map/src/services/editor.service.ts
2025-05-17 22:53:30 +08:00

778 lines
26 KiB
TypeScript

import {
EDITOR_CONFIG,
type MapAreaInfo,
MapAreaType,
type MapPen,
type MapPointInfo,
MapPointType,
type MapRouteInfo,
MapRoutePassType,
MapRouteType,
type Point,
} from '@api/map';
import type { RobotGroup, RobotInfo } from '@api/robot';
import type {
GroupSceneDetail,
SceneData,
StandardScene,
StandardSceneArea,
StandardScenePoint,
StandardSceneRoute,
} from '@api/scene';
import sTheme from '@core/theme.service';
import { CanvasLayer, LockState, Meta2d, type Meta2dStore, type Pen, s8 } from '@meta2d/core';
import { useObservable } from '@vueuse/rxjs';
import { clone, get, isEmpty, isNil, isString, pick, remove, some } from 'lodash-es';
import { BehaviorSubject, debounceTime, filter, map, Subject, switchMap } from 'rxjs';
import { reactive, watch } from 'vue';
export class EditorService extends Meta2d {
//#region 场景文件
public async load(map?: string, editable = false, detail?: Partial<GroupSceneDetail>): Promise<void> {
const scene: StandardScene = map ? JSON.parse(map) : {};
if (!isEmpty(detail?.group)) {
scene.robotGroups = [detail.group];
scene.robots = detail.robots;
}
const { robotGroups, robots, points, routes, areas } = scene;
this.open();
this.setState(editable);
this.#loadRobots(robotGroups, robots);
await this.#loadScenePoints(points);
this.#loadSceneRoutes(routes);
await this.#loadSceneAreas(areas);
this.store.historyIndex = undefined;
this.store.histories = [];
}
public save(): string {
const scene: StandardScene = {
robotGroups: this.robotGroups.value,
robots: this.robots,
points: this.points.value.map((v) => this.#mapScenePoint(v)).filter((v) => !isNil(v)),
routes: this.routes.value.map((v) => this.#mapSceneRoute(v)).filter((v) => !isNil(v)),
areas: this.areas.value.map((v) => this.#mapSceneArea(v)).filter((v) => !isNil(v)),
blocks: [],
};
return JSON.stringify(scene);
}
#loadRobots(groups?: RobotGroup[], robots?: RobotInfo[]): void {
this.#robotMap.clear();
robots?.forEach((v) => this.#robotMap.set(v.id, v));
this.#robotGroups$$.next(groups ?? []);
}
async #loadScenePoints(points?: StandardScenePoint[]): Promise<void> {
if (!points?.length) return;
await Promise.all(
points.map(async (v) => {
const { id, name, desc, x, y, type, robots, actions, properties } = v;
await this.addPoint({ x, y }, type, id);
this.setValue(
{ id, label: name, desc, properties, point: { type, robots, actions } },
{ render: false, history: false, doEvent: false },
);
}),
);
}
#loadSceneRoutes(routes?: StandardSceneRoute[]): void {
if (!routes?.length) return;
routes.map((v) => {
const { id, desc, from, to, type, pass, c1, c2, properties } = v;
this.addRoute([from, to], <MapRouteType>type, id);
this.setValue(
{ id, desc, properties, route: { type, pass, c1, c2 } },
{ render: false, history: false, doEvent: false },
);
});
}
async #loadSceneAreas(areas?: StandardSceneArea[]): Promise<void> {
if (!areas?.length) return;
await Promise.all(
areas.map(async (v) => {
const { id, name, desc, x, y, w, h, type, points, routes, properties } = v;
await this.addArea({ x, y }, { x: x + w, y: y + h }, type, id);
this.setValue(
{ id, label: name, desc, properties, area: { type, points, routes } },
{ render: false, history: false, doEvent: false },
);
}),
);
}
#mapScenePoint(pen?: MapPen): StandardScenePoint | null {
if (!pen?.id || isEmpty(pen?.point)) return null;
const { id, label, desc, properties } = pen;
const { type, robots, actions } = pen.point;
const { x, y } = this.getPenRect(pen);
const point: StandardScenePoint = {
id: id,
name: label || id,
desc,
x,
y,
type,
config: {},
properties,
};
if ([MapPointType., MapPointType.].includes(type)) {
point.robots = robots?.filter((v) => this.#robotMap.has(v));
}
if (MapPointType. === type) {
point.actions = actions?.filter((v) => this.getPenById(v)?.point?.type === MapPointType.);
}
return point;
}
#mapSceneRoute(pen?: MapPen): StandardSceneRoute | null {
if (!pen?.id || pen.anchors?.length !== 2 || isEmpty(pen?.route)) return null;
const { id, anchors, desc, properties } = pen;
const { type, direction = 1, pass, c1, c2 } = pen.route;
const [p1, p2] = anchors.map((v) => v.connectTo!);
const route: StandardSceneRoute = {
id: id,
desc,
from: direction < 0 ? p2 : p1,
to: direction < 0 ? p1 : p2,
type,
pass,
config: {},
properties,
};
switch (type) {
case MapRouteType.二阶贝塞尔曲线:
route.c1 = c1;
break;
case MapRouteType.三阶贝塞尔曲线:
route.c1 = direction < 0 ? c2 : c1;
route.c2 = direction < 0 ? c1 : c2;
break;
default:
break;
}
return route;
}
#mapSceneArea(pen: MapPen): StandardSceneArea | null {
if (!pen.id || isEmpty(pen.area)) return null;
const { id, label, desc, properties } = pen;
const { type, points, routes } = pen.area;
const { x, y, width, height } = this.getPenRect(pen);
const area: StandardSceneArea = {
id,
name: label || id,
desc,
x,
y,
w: width,
h: height,
type,
config: {},
properties,
};
if (MapAreaType. === type) {
area.points = points?.filter((v) => this.getPenById(v)?.point?.type === MapPointType.);
}
if ([MapAreaType., MapAreaType.].includes(type)) {
area.points = points?.filter((v) => {
const { point } = this.getPenById(v) ?? {};
if (isNil(point)) return false;
if (point.type === MapPointType.) return false;
return true;
});
}
if (MapAreaType. === type) {
area.routes = routes?.filter((v) => !isEmpty(this.getPenById(v)?.area));
}
return area;
}
//#endregion
public setState(editable?: boolean): void {
this.lock(editable ? LockState.None : LockState.DisableEdit);
}
public override data(): SceneData {
return super.data();
}
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]),
),
),
),
);
//#region 机器人
readonly #robotMap = reactive<Map<RobotInfo['id'], RobotInfo>>(new Map());
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;
}
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()];
}
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
readonly #change$$ = new Subject<boolean>();
public readonly current = useObservable<MapPen>(
this.#change$$.pipe(
debounceTime(100),
map(() => <MapPen>clone(this.store.active?.[0])),
),
);
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 getPenById(id?: string): MapPen | undefined {
if (!id) return;
return this.find(id)[0];
}
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();
}
public deleteById(id?: string): void {
const pen = this.getPenById(id);
console.log(pen);
if (pen?.name !== 'area') return;
this.delete([pen], true, true);
}
public updatePen(id: string, pen: Partial<MapPen>, record = true): void {
this.setValue({ ...pen, id }, { render: true, history: record, doEvent: true });
}
//#region 点位
public readonly points = useObservable<MapPen[], MapPen[]>(
this.#change$$.pipe(
filter((v) => v),
debounceTime(100),
map(() => this.find('point')),
),
{ initialValue: new Array<MapPen>() },
);
public async addPoint(p: Point, type = MapPointType., id?: string): Promise<MapPen> {
id ||= s8();
const pen: MapPen = {
...p,
...this.#mapPoint(type),
...this.#mapPointImage(type),
id,
name: 'point',
tags: ['point'],
label: `P${id}`,
point: { type },
};
return await this.addPen(pen, false, true, true);
}
public updatePoint(id: string, info: Partial<MapPointInfo>): void {
const { point } = this.getPenById(id) ?? {};
if (!point?.type) return;
const o = { ...point, ...info };
this.setValue({ id, point: o }, { render: true, history: true, doEvent: true });
}
public changePointType(id: string, type: MapPointType): void {
this.setValue(
{ id, ...this.#mapPoint(type), ...this.#mapPointImage(type), point: { type } },
{ render: true, history: true, doEvent: true },
);
}
#mapPoint(type: MapPointType): Required<Pick<MapPen, 'width' | 'height' | 'lineWidth' | 'iconSize'>> {
const width = type < 10 ? 24 : 48;
const height = type < 10 ? 24 : 60;
const lineWidth = type < 10 ? 2 : 3;
const iconSize = type < 10 ? 4 : 10;
return { width, height, lineWidth, iconSize };
}
#mapPointImage(type: MapPointType): Required<Pick<MapPen, 'image' | 'canvasLayer'>> {
const theme = this.data().theme;
const image = type < 10 ? '' : `/point/${type}-${theme}.png`;
return { image, canvasLayer: CanvasLayer.CanvasMain };
}
//#endregion
//#region 线路
public readonly routes = useObservable<MapPen[], MapPen[]>(
this.#change$$.pipe(
filter((v) => v),
debounceTime(100),
map(() => this.find('route').map((v) => ({ ...v, label: this.getRouteLabel(v.id) }))),
),
{ initialValue: new Array<MapPen>() },
);
public getRouteLabel(id?: string, d?: number): string {
if (!id) return '';
const pen = this.getPenById(id);
if (isNil(pen)) return '';
const [a1, a2] = pen.anchors ?? [];
if (!a1?.connectTo || !a2?.connectTo) return '';
const p1 = this.getPenById(a1.connectTo);
const p2 = this.getPenById(a2.connectTo);
if (isNil(p1) || isNil(p2)) return '';
const { direction = 1 } = pen.route ?? {};
return `${p1.label}${(d ?? direction) > 0 ? '→' : '←'}${p2.label}`;
}
public addRoute(p: [string, string], type = MapRouteType.线, id?: string): void {
const [p1, p2] = p.map((v) => this.getPenById(v));
if (!p1?.anchors?.length || !p2?.anchors?.length) return;
const line = this.connectLine(p1, p2, undefined, undefined, false);
id ||= line.id!;
this.changePenId(line.id!, id);
const pen: MapPen = { tags: ['route'], route: { type }, lineWidth: 2, iconSize: 10 };
this.setValue({ id, ...pen }, { render: false, history: false, doEvent: false });
this.updateLineType(line, type);
this.active(id);
this.render();
}
public updateRoute(id: string, info: Partial<MapRouteInfo>): void {
const { route } = this.getPenById(id) ?? {};
if (!route?.type) return;
const o = { ...route, ...info };
this.setValue({ id, route: o }, { render: true, history: true, doEvent: true });
}
public changeRouteType(id: string, type: MapRouteType): void {
const pen = this.getPenById(id);
if (isNil(pen)) return;
this.updateLineType(pen, type);
this.setValue({ id, route: { type } }, { render: true, history: true, doEvent: true });
}
//#endregion
//#region 区域
public readonly areas = useObservable<MapPen[], MapPen[]>(
this.#change$$.pipe(
filter((v) => v),
debounceTime(100),
map(() => this.find('area')),
),
{ initialValue: new Array<MapPen>() },
);
public getBoundAreas(id: string = '', name: 'point' | 'line', type: MapAreaType): MapPen[] {
if (!id) return [];
return this.find(`area-${type}`).filter(({ area }) => {
if (name === 'point') return area?.points?.includes(id);
if (name === 'line') return area?.routes?.includes(id);
return false;
});
}
public async addArea(p1: Point, p2: Point, type = MapAreaType., id?: string) {
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 points = new Array<string>();
const routes = new Array<string>();
if (id) {
const selected = <MapPen[]>this.store.active;
switch (type) {
case MapAreaType.库区:
selected?.filter(({ point }) => point?.type === MapPointType.).forEach(({ id }) => points.push(id!));
break;
case MapAreaType.互斥区:
selected?.filter(({ point }) => point?.type).forEach(({ id }) => points.push(id!));
selected?.filter(({ route }) => route?.type).forEach(({ id }) => routes.push(id!));
break;
case MapAreaType.非互斥区:
selected?.filter(({ point }) => point?.type).forEach(({ id }) => points.push(id!));
break;
default:
break;
}
} else {
id = s8();
}
const pen: MapPen = {
id,
name: 'area',
tags: ['area', `area-${type}`],
label: `A${id}`,
x: Math.min(p1.x, p2.x),
y: Math.min(p1.y, p2.y),
width: w,
height: h,
lineWidth: 1,
area: { type, points, routes },
locked: LockState.DisableMoveScale,
};
const area = await this.addPen(pen, true, true, true);
this.bottom(area);
}
public updateArea(id: string, info: Partial<MapAreaInfo>): void {
const { area } = this.getPenById(id) ?? {};
if (!area?.type) return;
const o = { ...area, ...info };
this.setValue({ id, area: o }, { render: true, history: true, doEvent: true });
}
//#endregion
constructor(container: HTMLDivElement) {
super(container, EDITOR_CONFIG);
(<HTMLDivElement>container.children.item(5)).ondrop = null;
this.on('*', (e, v) => this.#listen(e, v));
this.#register();
watch(
() => sTheme.theme,
(v) => this.#load(v),
{ immediate: true },
);
}
#load(theme: string): void {
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();
}
#onDelete(pens?: MapPen[]): void {
pens?.forEach((pen) => {
switch (pen.name) {
case 'point':
this.delete(this.getLines(pen), true, false);
break;
default:
break;
}
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
#listen(e: unknown, v: any) {
switch (e) {
case 'opened':
this.#load(sTheme.theme);
this.#change$$.next(true);
break;
case 'add':
this.#change$$.next(true);
break;
case 'delete':
this.#onDelete(v);
this.#change$$.next(true);
break;
case 'update':
this.#change$$.next(true);
break;
case 'valueUpdate':
this.#change$$.next(true);
break;
case 'active':
case 'inactive':
this.#change$$.next(false);
break;
case 'click':
case 'mousedown':
case 'mouseup':
this.#mouse$$.next({ type: e, value: pick(v, 'x', 'y') });
break;
default:
// console.log(e, v);
break;
}
}
#register() {
this.register({ line: () => new Path2D() });
this.registerCanvasDraw({ point: drawPoint, line: drawLine, area: drawArea });
this.registerAnchors({ point: anchorPoint });
this.addDrawLineFn('bezier2', lineBezier2);
this.addDrawLineFn('bezier3', lineBezier3);
}
}
//#region 绘制函数
function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const theme = sTheme.editor;
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 ?? {};
const { type } = pen.point ?? {};
const { label = '' } = pen ?? {};
ctx.save();
switch (type) {
case MapPointType.普通点:
case MapPointType.等待点:
case MapPointType.避让点:
case MapPointType.临时避让点:
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();
break;
case MapPointType.电梯点:
case MapPointType.自动门点:
case MapPointType.充电点:
case MapPointType.停靠点:
case MapPointType.动作点:
case MapPointType.禁行点:
ctx.roundRect(x, y, w, h, r);
ctx.strokeStyle = get(theme, active ? 'point-l.strokeActive' : 'point-l.stroke') ?? '';
ctx.stroke();
break;
default:
break;
}
ctx.fillStyle = get(theme, 'color') ?? '';
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
ctx.restore();
}
function anchorPoint(pen: MapPen): void {
pen.anchors = [
{ penId: pen.id, id: '0', x: 0.5, y: 0.5 },
// { penId: pen.id, id: 't', x: 0.5, y: 0 },
// { penId: pen.id, id: 'b', x: 0.5, y: 1 },
// { penId: pen.id, id: 'l', x: 0, y: 0.5 },
// { penId: pen.id, id: 'r', x: 1, y: 0.5 },
];
}
function drawLine(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const theme = sTheme.editor;
const { iconSize: s = 10 } = pen.calculative ?? {};
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
const { type, direction = 1, pass = 0, c1, c2 } = pen.route ?? {};
const { x: dx1 = x2 - x1, y: dy1 = 0 } = c1 ?? {};
const { x: dx2 = 0, y: dy2 = y1 - y2 } = c2 ?? {};
ctx.save();
ctx.beginPath();
ctx.strokeStyle = get(theme, `route.stroke-${pass}`) ?? '';
ctx.moveTo(x1, y1);
switch (type) {
case MapRouteType.直线:
ctx.lineTo(x2, y2);
break;
case MapRouteType.二阶贝塞尔曲线:
ctx.quadraticCurveTo(x1 + dx1, y1 + dy1, x2, y2);
p1.next = { x: x1 + (2 / 3) * dx1, y: y1 + (2 / 3) * dy1 };
p2.prev = { x: x2 / 3 + (2 / 3) * (x1 + dx1), y: y2 / 3 + (2 / 3) * (y1 + dy1) };
break;
case MapRouteType.三阶贝塞尔曲线:
ctx.bezierCurveTo(x1 + dx1, y1 + dy1, x2 + dx2, y2 + dy2, x2, y2);
p1.next = { x: x1 + dx1, y: y1 + dy1 };
p2.prev = { x: x2 + dx2, y: y2 + dy2 };
break;
default:
break;
}
if (pass === MapRoutePassType.) {
ctx.setLineDash([s / 2]);
}
ctx.stroke();
ctx.beginPath();
ctx.setLineDash([0]);
let r = (() => {
switch (type) {
case MapRouteType.直线:
return Math.atan2(y2 - y1, x2 - x1);
case MapRouteType.二阶贝塞尔曲线:
return direction < 0 ? Math.atan2(dy1, dx1) : Math.atan2(y2 - y1 - dy1, x2 - x1 - dx1);
case MapRouteType.三阶贝塞尔曲线:
return direction < 0 ? Math.atan2(dy1, dx1) : Math.atan2(-dy2, -dx2);
default:
return 0;
}
})();
if (direction < 0) {
ctx.translate(x1, y1);
} else {
ctx.translate(x2, y2);
r += Math.PI;
}
ctx.moveTo(Math.cos(r + Math.PI / 5) * s, Math.sin(r + Math.PI / 5) * s);
ctx.lineTo(0, 0);
ctx.lineTo(Math.cos(r - Math.PI / 5) * s, Math.sin(r - Math.PI / 5) * s);
ctx.stroke();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.restore();
}
function lineBezier2(_: Meta2dStore, pen: MapPen): void {
if (pen.calculative?.worldAnchors?.length !== 2) return;
const { c1 } = pen.route ?? {};
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
const { x: dx = x2 - x1, y: dy = 0 } = c1 ?? {};
pen.calculative.worldAnchors[0].next = { x: x1 + (2 / 3) * dx, y: y1 + (2 / 3) * dy };
pen.calculative.worldAnchors[1].prev = { x: x2 / 3 + (2 / 3) * (x1 + dx), y: y2 / 3 + (2 / 3) * (y1 + dy) };
}
function lineBezier3(_: Meta2dStore, pen: MapPen): void {
if (pen.calculative?.worldAnchors?.length !== 2) return;
const { c1, c2 } = pen.route ?? {};
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
const { x: dx1 = x2 - x1, y: dy1 = 0 } = c1 ?? {};
const { x: dx2 = 0, y: dy2 = y1 - y2 } = c2 ?? {};
pen.calculative.worldAnchors[0].next = { x: x1 + dx1, y: y1 + dy1 };
pen.calculative.worldAnchors[1].prev = { x: x2 + dx2, y: y2 + dy2 };
}
function drawArea(ctx: CanvasRenderingContext2D, pen: MapPen): void {
const theme = sTheme.editor;
const { active, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {};
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
const { type } = pen.area ?? {};
const { label = '' } = pen ?? {};
ctx.save();
ctx.rect(x, y, w, h);
ctx.fillStyle = get(theme, `area.fill-${type}`) ?? '';
ctx.fill();
ctx.strokeStyle = get(theme, active ? 'area.strokeActive' : `area.stroke-${type}`) ?? '';
ctx.stroke();
ctx.fillStyle = get(theme, 'color') ?? '';
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
ctx.restore();
}
//#endregion