diff --git a/src/drawApp/graphics/PolygonInteraction.ts b/src/drawApp/graphics/PolygonInteraction.ts new file mode 100644 index 0000000..7e8cfe3 --- /dev/null +++ b/src/drawApp/graphics/PolygonInteraction.ts @@ -0,0 +1,61 @@ +import * as pb_1 from 'google-protobuf'; +import { IPolygonData } from 'src/graphics/polygon/Polygon'; +import { graphicData } from 'src/protos/stationLayoutGraphics'; +import { GraphicDataBase } from './GraphicDataBase'; +import { IPointData } from 'pixi.js'; + +export class PolygonData extends GraphicDataBase implements IPolygonData { + constructor(data?: graphicData.Polygon) { + let Polygon; + if (!data) { + Polygon = new graphicData.Polygon({ + common: GraphicDataBase.defaultCommonInfo(), + }); + } else { + Polygon = data; + } + super(Polygon); + } + + public get data(): graphicData.Polygon { + return this.getData(); + } + + get code(): string { + return this.data.code; + } + set code(v: string) { + this.data.code = v; + } + get lineWidth(): number { + return this.data.lineWidth; + } + set lineWidth(v: number) { + this.data.lineWidth = v; + } + get lineColor(): string { + return this.data.lineColor; + } + set lineColor(v: string) { + this.data.lineColor = v; + } + get points(): IPointData[] { + return this.data.points; + } + set points(points: IPointData[]) { + this.data.points = points.map( + (p) => new graphicData.Point({ x: p.x, y: p.y }) + ); + } + + + clone(): PolygonData { + return new PolygonData(this.data.cloneMessage()); + } + copyFrom(data: PolygonData): void { + pb_1.Message.copyInto(data.data, this.data); + } + eq(other: PolygonData): boolean { + return pb_1.Message.equals(this.data, other.data); + } +} diff --git a/src/drawApp/index.ts b/src/drawApp/index.ts index 6a121dc..1e59366 100644 --- a/src/drawApp/index.ts +++ b/src/drawApp/index.ts @@ -25,6 +25,9 @@ import { graphicData } from 'src/protos/stationLayoutGraphics'; import { Rect } from 'src/graphics/rect/Rect'; import { RectDraw } from 'src/graphics/rect/RectDrawAssistant'; import { RectData } from './graphics/RectInteraction'; +import { Polygon } from 'src/graphics/polygon/Polygon'; +import { PolygonDraw } from 'src/graphics/polygon/PolygonDrawAssistant'; +import { PolygonData } from './graphics/PolygonInteraction'; import { Platform } from 'src/graphics/platform/Platform'; import { PlatformData } from './graphics/PlatformInteraction'; import { PlatformDraw } from 'src/graphics/platform/PlatformDrawAssistant'; @@ -130,6 +133,7 @@ export function initDrawApp(dom: HTMLElement): JlDrawApp { | StationLineDraw | RectDraw | TrainLineDraw + | PolygonDraw )[] = []; if (draftType === 'Line') { drawAssistants = [ @@ -149,6 +153,9 @@ export function initDrawApp(dom: HTMLElement): JlDrawApp { // return new TrainData(); // }), new SectionDraw(app, () => new SectionData()), + /* new PolygonDraw(app, () => { + return new PolygonData(); + }), */ ]; } else { drawAssistants = [ diff --git a/src/graphics/polygon/Polygon.ts b/src/graphics/polygon/Polygon.ts new file mode 100644 index 0000000..d677534 --- /dev/null +++ b/src/graphics/polygon/Polygon.ts @@ -0,0 +1,70 @@ +import { Color, Graphics, IPointData } from 'pixi.js'; +import { GraphicData, JlGraphic, JlGraphicTemplate } from 'src/jl-graphic'; + +export interface IPolygonData extends GraphicData { + get code(): string; // 编号 + set code(v: string); + get lineWidth(): number; // 线宽 + set lineWidth(v: number); + get lineColor(): string; // 线色 + set lineColor(v: string); + get points(): IPointData[]; // 多边形坐标点 + set points(points: IPointData[]); + clone(): IPolygonData; + copyFrom(data: IPolygonData): void; + eq(other: IPolygonData): boolean; +} + +const polygonConsts = { + lineWidth: 2, + lineColor: '0xff0000', +}; + +export class Polygon extends JlGraphic { + static Type = 'Polygon'; + polygonGraphic: Graphics; + constructor() { + super(Polygon.Type); + this.polygonGraphic = new Graphics(); + this.addChild(this.polygonGraphic); + } + + get datas(): IPolygonData { + return this.getDatas(); + } + doRepaint(): void { + const polygonGraphic = this.polygonGraphic; + polygonGraphic.clear(); + polygonGraphic.lineStyle( + this.datas.lineWidth, + new Color(this.datas.lineColor) + ); + polygonGraphic.drawPolygon(this.datas.points); + } + get linePoints(): IPointData[] { + return this.datas.points; + } + set linePoints(points: IPointData[]) { + const old = this.datas.clone(); + old.points = points; + this.updateData(old); + } + addOnePoints(): IPointData[] { + const ps = [...this.datas.points]; + ps.push(this.datas.points[0]); + return ps; + } +} + +export class PolygonTemplate extends JlGraphicTemplate { + lineWidth: number; + lineColor: string; + constructor() { + super(Polygon.Type); + this.lineWidth = polygonConsts.lineWidth; + this.lineColor = polygonConsts.lineColor; + } + new(): Polygon { + return new Polygon(); + } +} diff --git a/src/graphics/polygon/PolygonDrawAssistant.ts b/src/graphics/polygon/PolygonDrawAssistant.ts new file mode 100644 index 0000000..798f0d6 --- /dev/null +++ b/src/graphics/polygon/PolygonDrawAssistant.ts @@ -0,0 +1,270 @@ +import { + FederatedPointerEvent, + Graphics, + Point, + IHitArea, + DisplayObject, + FederatedMouseEvent, +} from 'pixi.js'; +import { + DraggablePoint, + GraphicApp, + GraphicDrawAssistant, + GraphicInteractionPlugin, + GraphicTransformEvent, + JlDrawApp, + JlGraphic, + calculateLineSegmentingPoint, + linePoint, +} from 'src/jl-graphic'; + +import AbsorbablePoint, { + AbsorbablePosition, +} from 'src/jl-graphic/graphic/AbsorbablePosition'; +import { + ILineGraphic, + PolylineEditPlugin, + addWaySegmentingConfig, + clearWayPoint, + clearWaypointsConfig, + getWayLineIndex, +} from 'src/jl-graphic/plugins/GraphicEditPlugin'; +import { ContextMenu } from 'src/jl-graphic/ui/ContextMenu'; + +import { IPolygonData, Polygon, PolygonTemplate } from './Polygon'; +import { Link } from '../link/Link'; + +export interface IPolygonDrawOptions { + newData: () => IPolygonData; +} + +export class PolygonDraw extends GraphicDrawAssistant< + PolygonTemplate, + IPolygonData +> { + points: Point[] = []; + polygonGraphic: Graphics = new Graphics(); + + constructor(app: JlDrawApp, createData: () => IPolygonData) { + super( + app, + new PolygonTemplate(), + createData, + 'sym_o_square', + '多边形Polygon' + ); + this.container.addChild(this.polygonGraphic); + PolygonPointsEditPlugin.init(app); + } + + bind(): void { + super.bind(); + } + unbind(): void { + super.unbind(); + } + + clearCache(): void { + this.points = []; + this.polygonGraphic.clear(); + } + onLeftDown(e: FederatedPointerEvent): void { + const { x, y } = this.toCanvasCoordinates(e.global); + const p = new Point(x, y); + this.points.push(p); + } + onRightClick(): void { + this.createAndStore(true); + } + redraw(p: Point): void { + if (this.points.length < 1) return; + const polygonGraphic = this.polygonGraphic; + const template = this.graphicTemplate; + const ps = [...this.points]; + ps.push(p); + polygonGraphic.clear(); + polygonGraphic.lineStyle(template.lineWidth, template.lineColor); + polygonGraphic.drawPolygon(ps); + } + + prepareData(data: IPolygonData): boolean { + if (this.points.length < 2) { + console.log('Polygon绘制因点不够取消绘制'); + return false; + } + const template = this.graphicTemplate; + data.lineWidth = template.lineWidth; + data.lineColor = template.lineColor; + data.points = this.points; + return true; + } +} + +//碰撞检测 +export class PolygonGraphicHitArea implements IHitArea { + polygon: Polygon; + constructor(polygon: Polygon) { + this.polygon = polygon; + } + contains(x: number, y: number): boolean { + let contains = false; + const p = new Point(x, y); + const polygonData = this.polygon.datas; + //contains = pointPolygon(p, polygonData.points, polygonData.lineWidth);是否包含多边形内部 + const ps = this.polygon.addOnePoints(); + const tolerance = polygonData.lineWidth; + for (let i = 0; i < ps.length - 1; i++) { + const p1 = ps[i]; + const p2 = ps[i + 1]; + contains = contains || linePoint(p1, p2, p, tolerance); + if (contains) { + break; + } + } + return contains; + } +} + +/** + * 构建吸附位置 + * @param polygon + * @returns + */ +function buildAbsorbablePositions(polygon: Polygon): AbsorbablePosition[] { + const aps: AbsorbablePosition[] = []; + const polygons = polygon.queryStore.queryByType(Polygon.Type); + const links = polygon.queryStore.queryByType(Link.Type); + + links.forEach((other) => { + const apa = new AbsorbablePoint( + other.localToCanvasPoint(other.getStartPoint()) + ); + const apb = new AbsorbablePoint( + other.localToCanvasPoint(other.getEndPoint()) + ); + aps.push(apa, apb); + }); + + polygons.forEach((other) => { + if (other.id == polygon.id) { + return; + } + other.linePoints.forEach((point) => { + const absorbablePoint = new AbsorbablePoint( + other.localToCanvasPoint(point) + ); + aps.push(absorbablePoint); + }); + }); + + return aps; +} + +/** + * 端点拖拽添加吸附位置 + * @param g + * @param dp + * @param index + */ +function onEditPointCreate(g: ILineGraphic, dp: DraggablePoint): void { + const polygon = g as Polygon; + // 端点 + dp.on('transformstart', (e: GraphicTransformEvent) => { + if (e.isShift()) { + polygon.getGraphicApp().setOptions({ + absorbablePositions: buildAbsorbablePositions(polygon), + }); + } + }); +} + +const PolygonEditMenu: ContextMenu = ContextMenu.init({ + name: '矩形编辑菜单', + groups: [ + { + items: [addWaySegmentingConfig, clearWaypointsConfig], + }, + ], +}); + +/** + * polygon路径编辑 + */ +export class PolygonPointsEditPlugin extends GraphicInteractionPlugin { + static Name = 'PolygonPointsDrag'; + constructor(app: GraphicApp) { + super(PolygonPointsEditPlugin.Name, app); + app.registerMenu(PolygonEditMenu); + } + static init(app: GraphicApp): PolygonPointsEditPlugin { + return new PolygonPointsEditPlugin(app); + } + filter(...grahpics: JlGraphic[]): Polygon[] | undefined { + return grahpics.filter((g) => g.type == Polygon.Type) as Polygon[]; + } + bind(g: Polygon): void { + g.polygonGraphic.eventMode = 'static'; + g.polygonGraphic.cursor = 'pointer'; + g.polygonGraphic.hitArea = new PolygonGraphicHitArea(g); + g.on('_rightclick', this.onContextMenu, this); + g.on('selected', this.onSelected, this); + g.on('unselected', this.onUnselected, this); + } + unbind(g: Polygon): void { + g.off('_rightclick', this.onContextMenu, this); + g.off('selected', this.onSelected, this); + g.off('unselected', this.onUnselected, this); + } + + onContextMenu(e: FederatedMouseEvent) { + const target = e.target as DisplayObject; + const polygon = target.getGraphic() as Polygon; + this.app.updateSelected(polygon); + addWaySegmentingConfig.handler = () => { + const linePoints = polygon.addOnePoints(); + const p = polygon.screenToLocalPoint(e.global); + const { start, end } = getWayLineIndex(linePoints, p); + this.addPolygonSegmentingPoint(polygon, start, end); + }; + clearWaypointsConfig.handler = () => { + clearWayPoint(polygon, false); + }; + PolygonEditMenu.open(e.global); + } + addPolygonSegmentingPoint( + graphic: Polygon, + start: number, + end: number, + knife = 2 + ) { + const linePoints = graphic.addOnePoints(); + const points = linePoints.slice(0, start + 1); + points.push( + ...calculateLineSegmentingPoint(linePoints[start], linePoints[end], knife) + ); + points.push(...linePoints.slice(end)); + points.pop(); + graphic.linePoints = points; + } + + onSelected(g: DisplayObject): void { + const polygon = g as Polygon; + let lep = polygon.getAssistantAppend( + PolylineEditPlugin.Name + ); + if (!lep) { + lep = new PolylineEditPlugin(polygon, { onEditPointCreate }); + polygon.addAssistantAppend(lep); + } + lep.showAll(); + } + onUnselected(g: DisplayObject): void { + const polygon = g as Polygon; + const lep = polygon.getAssistantAppend( + PolylineEditPlugin.Name + ); + if (lep) { + lep.hideAll(); + } + } +} diff --git a/src/protos/stationLayoutGraphics.ts b/src/protos/stationLayoutGraphics.ts index c1b03ec..343dc1e 100644 --- a/src/protos/stationLayoutGraphics.ts +++ b/src/protos/stationLayoutGraphics.ts @@ -21,9 +21,10 @@ export namespace graphicData { stationLines?: StationLine[]; runLines?: RunLine[]; trainLines?: TrainLine[]; + polygons?: Polygon[]; }) { super(); - pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], this.#one_of_decls); + pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], this.#one_of_decls); if (!Array.isArray(data) && typeof data == "object") { if ("canvas" in data && data.canvas != undefined) { this.canvas = data.canvas; @@ -64,6 +65,9 @@ export namespace graphicData { if ("trainLines" in data && data.trainLines != undefined) { this.trainLines = data.trainLines; } + if ("polygons" in data && data.polygons != undefined) { + this.polygons = data.polygons; + } } } get canvas() { @@ -147,6 +151,12 @@ export namespace graphicData { set trainLines(value: TrainLine[]) { pb_1.Message.setRepeatedWrapperField(this, 13, value); } + get polygons() { + return pb_1.Message.getRepeatedWrapperField(this, Polygon, 14) as Polygon[]; + } + set polygons(value: Polygon[]) { + pb_1.Message.setRepeatedWrapperField(this, 14, value); + } static fromObject(data: { canvas?: ReturnType; links?: ReturnType[]; @@ -161,6 +171,7 @@ export namespace graphicData { stationLines?: ReturnType[]; runLines?: ReturnType[]; trainLines?: ReturnType[]; + polygons?: ReturnType[]; }): RtssGraphicStorage { const message = new RtssGraphicStorage({}); if (data.canvas != null) { @@ -202,6 +213,9 @@ export namespace graphicData { if (data.trainLines != null) { message.trainLines = data.trainLines.map(item => TrainLine.fromObject(item)); } + if (data.polygons != null) { + message.polygons = data.polygons.map(item => Polygon.fromObject(item)); + } return message; } toObject() { @@ -219,6 +233,7 @@ export namespace graphicData { stationLines?: ReturnType[]; runLines?: ReturnType[]; trainLines?: ReturnType[]; + polygons?: ReturnType[]; } = {}; if (this.canvas != null) { data.canvas = this.canvas.toObject(); @@ -259,6 +274,9 @@ export namespace graphicData { if (this.trainLines != null) { data.trainLines = this.trainLines.map((item: TrainLine) => item.toObject()); } + if (this.polygons != null) { + data.polygons = this.polygons.map((item: Polygon) => item.toObject()); + } return data; } serialize(): Uint8Array; @@ -291,6 +309,8 @@ export namespace graphicData { writer.writeRepeatedMessage(12, this.runLines, (item: RunLine) => item.serialize(writer)); if (this.trainLines.length) writer.writeRepeatedMessage(13, this.trainLines, (item: TrainLine) => item.serialize(writer)); + if (this.polygons.length) + writer.writeRepeatedMessage(14, this.polygons, (item: Polygon) => item.serialize(writer)); if (!w) return writer.getResultBuffer(); } @@ -339,6 +359,9 @@ export namespace graphicData { case 13: reader.readMessage(message.trainLines, () => pb_1.Message.addToRepeatedWrapperField(message, 13, TrainLine.deserialize(reader), TrainLine)); break; + case 14: + reader.readMessage(message.polygons, () => pb_1.Message.addToRepeatedWrapperField(message, 14, Polygon.deserialize(reader), Polygon)); + break; default: reader.skipField(); } } @@ -1399,6 +1422,168 @@ export namespace graphicData { return Rect.deserialize(bytes); } } + export class Polygon extends pb_1.Message { + #one_of_decls: number[][] = []; + constructor(data?: any[] | { + common?: CommonInfo; + code?: string; + lineWidth?: number; + lineColor?: string; + points?: Point[]; + }) { + super(); + pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [5], this.#one_of_decls); + if (!Array.isArray(data) && typeof data == "object") { + if ("common" in data && data.common != undefined) { + this.common = data.common; + } + if ("code" in data && data.code != undefined) { + this.code = data.code; + } + if ("lineWidth" in data && data.lineWidth != undefined) { + this.lineWidth = data.lineWidth; + } + if ("lineColor" in data && data.lineColor != undefined) { + this.lineColor = data.lineColor; + } + if ("points" in data && data.points != undefined) { + this.points = data.points; + } + } + } + get common() { + return pb_1.Message.getWrapperField(this, CommonInfo, 1) as CommonInfo; + } + set common(value: CommonInfo) { + pb_1.Message.setWrapperField(this, 1, value); + } + get has_common() { + return pb_1.Message.getField(this, 1) != null; + } + get code() { + return pb_1.Message.getFieldWithDefault(this, 2, "") as string; + } + set code(value: string) { + pb_1.Message.setField(this, 2, value); + } + get lineWidth() { + return pb_1.Message.getFieldWithDefault(this, 3, 0) as number; + } + set lineWidth(value: number) { + pb_1.Message.setField(this, 3, value); + } + get lineColor() { + return pb_1.Message.getFieldWithDefault(this, 4, "") as string; + } + set lineColor(value: string) { + pb_1.Message.setField(this, 4, value); + } + get points() { + return pb_1.Message.getRepeatedWrapperField(this, Point, 5) as Point[]; + } + set points(value: Point[]) { + pb_1.Message.setRepeatedWrapperField(this, 5, value); + } + static fromObject(data: { + common?: ReturnType; + code?: string; + lineWidth?: number; + lineColor?: string; + points?: ReturnType[]; + }): Polygon { + const message = new Polygon({}); + if (data.common != null) { + message.common = CommonInfo.fromObject(data.common); + } + if (data.code != null) { + message.code = data.code; + } + if (data.lineWidth != null) { + message.lineWidth = data.lineWidth; + } + if (data.lineColor != null) { + message.lineColor = data.lineColor; + } + if (data.points != null) { + message.points = data.points.map(item => Point.fromObject(item)); + } + return message; + } + toObject() { + const data: { + common?: ReturnType; + code?: string; + lineWidth?: number; + lineColor?: string; + points?: ReturnType[]; + } = {}; + if (this.common != null) { + data.common = this.common.toObject(); + } + if (this.code != null) { + data.code = this.code; + } + if (this.lineWidth != null) { + data.lineWidth = this.lineWidth; + } + if (this.lineColor != null) { + data.lineColor = this.lineColor; + } + if (this.points != null) { + data.points = this.points.map((item: Point) => item.toObject()); + } + return data; + } + serialize(): Uint8Array; + serialize(w: pb_1.BinaryWriter): void; + serialize(w?: pb_1.BinaryWriter): Uint8Array | void { + const writer = w || new pb_1.BinaryWriter(); + if (this.has_common) + writer.writeMessage(1, this.common, () => this.common.serialize(writer)); + if (this.code.length) + writer.writeString(2, this.code); + if (this.lineWidth != 0) + writer.writeInt32(3, this.lineWidth); + if (this.lineColor.length) + writer.writeString(4, this.lineColor); + if (this.points.length) + writer.writeRepeatedMessage(5, this.points, (item: Point) => item.serialize(writer)); + if (!w) + return writer.getResultBuffer(); + } + static deserialize(bytes: Uint8Array | pb_1.BinaryReader): Polygon { + const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new Polygon(); + while (reader.nextField()) { + if (reader.isEndGroup()) + break; + switch (reader.getFieldNumber()) { + case 1: + reader.readMessage(message.common, () => message.common = CommonInfo.deserialize(reader)); + break; + case 2: + message.code = reader.readString(); + break; + case 3: + message.lineWidth = reader.readInt32(); + break; + case 4: + message.lineColor = reader.readString(); + break; + case 5: + reader.readMessage(message.points, () => pb_1.Message.addToRepeatedWrapperField(message, 5, Point.deserialize(reader), Point)); + break; + default: reader.skipField(); + } + } + return message; + } + serializeBinary(): Uint8Array { + return this.serialize(); + } + static deserializeBinary(bytes: Uint8Array): Polygon { + return Polygon.deserialize(bytes); + } + } export class Platform extends pb_1.Message { #one_of_decls: number[][] = []; constructor(data?: any[] | {