import { SMouseEvent, SNetUtil } from "@saga-web/base"; import { SCanvasPaintEngine, SCanvasView, SPainter, SPoint, SRect, SSvgPaintEngine } from "@saga-web/draw"; import { SGraphScene } from "./SGraphScene"; import { SGraphItem } from "./SGraphItem"; import { v1 as uuidv1 } from "uuid"; import { SColor } from "@saga-web/draw"; /** * Graph 图形引擎视图类 * * @author 庞利祥 */ export class SGraphView extends SCanvasView { /** 场景对象 */ private _scene: SGraphScene | null = null; get scene(): SGraphScene | null { return this._scene; } set scene(v: SGraphScene | null) { if (this._scene != null) { this._scene.view = null; } this._scene = v; if (this._scene != null) { this._scene.view = this; } this.update(); } /** 背景色 */ backgroundColor: SColor = SColor.Transparent; /** 旋转角度 */ rotate: number = 0; /** * 构造函数 * * @param id 画布对象 ID */ constructor(id: string) { super(id); } /** * 保存场景 SVG 文件 * * @param name 文件名 * @param width svg 宽度 * @param height svg 高度 */ saveSceneSvg(name: string, width: number, height: number): void { let url = URL.createObjectURL( new Blob([this.sceneSvgData(width, height)], { type: "text/xml,charset=UTF-8" }) ); SNetUtil.downLoad(name, url); } /** * 场景 SVG 图形的数据 * * @param width svg 宽度 * @param height svg 高度 * @return URL 地址 */ sceneSvgData(width: number, height: number): string { if (null == this.scene) { return ""; } let engine = new SSvgPaintEngine(width, height); let painter = new SPainter(engine); // 保存视图缩放比例与原点位置 let s0 = this.scale; let x0 = this.origin.x; let y0 = this.origin.y; // 场景中无对象 let rect = this.scene.allItemRect(); this.fitRectToSize(width, height, rect); this.onDraw(painter); // 恢复视图缩放比例与原点位置 this.scale = s0; this.origin.x = x0; this.origin.y = y0; return engine.toSvg(); } /** * 适配视图到视图 */ fitSceneToView(): void { if (null == this.scene) { return; } // 场景中无对象 let rect = this.scene.allItemRect(); this.fitRectToSize(this.width, this.height, rect); } /** * 适配选中的对象在视图中可见 */ fitSelectedToView(): void { if (null == this.scene) { return; } // 场景中无对象 let rect = this.scene.selectedItemRect(); this.fitRectToSize(this.width, this.height, rect); } /** * 适配任意对象在视图中可见 */ fitItemToView(itemList: SGraphItem[]): void { if (null == this.scene) { return; } let rect: SRect | null = null; // 依次取 item 列中的所有 item。将所有 item 的边界做并焦处理。 for (let item of itemList) { if (rect == null) { rect = item.boundingRect().translated(item.pos.x, item.pos.y); } else { rect.union( item.boundingRect().translated(item.pos.x, item.pos.y) ); } } // 场景中无对象 this.fitRectToSize(this.width, this.height, rect); } /** * 将场景中的 x, y 坐标转换成视图坐标。 * * @param x 场景中的横坐标 * @param y 场景中的纵坐标 * @return 视图坐标 */ mapFromScene(x: number, y: number): SPoint; /** * 将场景中的 x, y 坐标转换成视图坐标。 * * @param pos 场景中的坐标 * @return 视图坐标 */ mapFromScene(pos: SPoint): SPoint; /** * 将场景中的 x, y 坐标转换成视图坐标(重载实现)。 * * @param x 场景中的横坐标 * @param y 场景中的纵坐标 * @return 视图坐标 */ mapFromScene(x: number | SPoint, y?: number): SPoint { if (x instanceof SPoint) { // 如果传入的是 SPoint 对象 return new SPoint( x.x * this.scale + this.origin.x, x.y * this.scale + this.origin.y ); } // @ts-ignore return new SPoint( x * this.scale + this.origin.x, (y == undefined ? 0 : y) * this.scale + this.origin.y ); } /** * 将视图的 x, y 坐标转换成场景坐标。 * * @param x 视图横坐标 * @param y 视图纵坐标 * @return 场景坐标 */ mapToScene(x: number, y: number): SPoint; /** * 将视图的 x, y 坐标转换成场景坐标。 * * @param pos 视图坐标 * @return 场景坐标 */ mapToScene(pos: SPoint): SPoint; /** * 将视图的 x, y 坐标转换成场景坐标。(不推荐在外部调用) * * @param x 视图的横坐标或 SPoint 对象 * @param y 视图的纵坐标 * @return 场景坐标 */ mapToScene(x: number | SPoint, y?: number): SPoint { if (x instanceof SPoint) { // 如果传入的是 SPoint 对象 return new SPoint( (x.x - this.origin.x) / this.scale, (x.y - this.origin.y) / this.scale ); } return new SPoint( (x - this.origin.x) / this.scale, ((y == undefined ? 0 : y) - this.origin.y) / this.scale ); } /** * 保存指定大小图像,并且适配到中央而不影响原图 * * @param name 名称 * @param type 图像类型 * @param width 要保存图形的宽 * @param height 要保存图形的高 * @param bg 生成图的背景色 */ saveImageSize( name: string, type: string, width: number, height: number, bg: string = "#ffffff" ): void { const can = document.createElement("CANVAS") as HTMLCanvasElement; let vi: any = this.generateView(can, width, height, bg); vi.saveImage(name, type); // @ts-ignore this.scene.view = this; can.remove(); vi = null; } /** * 保存指定大小图像,并且适配到中央而不影响原图所使用的图像的 URL * * @param type 图像类型 * @param width 要保存图形的宽 * @param height 要保存图形的高 * @param bg 生成图的背景色 * @return 图像的 URL */ imageSizeUrl( type: string, width: number, height: number, bg: string = "#FFFFFF" ): string { const can = document.createElement("CANVAS") as HTMLCanvasElement; let vi: any = this.generateView(can, width, height, bg); const str = vi.canvasView.toDataURL(`image/${type}`); can.remove(); vi = null; return str; } /** * 生成新的view * * @param can canvas dom * @param width 要保存图形的宽 * @param height 要保存图形的高 * @param bg 生成图的背景色 * @return 图像的 URL */ private generateView( can: HTMLCanvasElement, width: number, height: number, bg: string = "#FFFFFF" ): SGraphView { can.width = width; can.height = height; can.id = uuidv1(); const body = document.getElementsByTagName("body")[0]; body.appendChild(can); let engine = new SCanvasPaintEngine( can.getContext("2d") as CanvasRenderingContext2D ); let painter = new SPainter(engine); let vi = new SGraphView(can.id); vi.scene = this.scene; vi.fitSceneToView(); vi.backgroundColor = new SColor(bg); vi.onDraw(painter); return vi; } /** * 绘制视图 * * @param painter 绘制对象 */ protected onDraw(painter: SPainter): void { painter.save(); painter.clearRect(0, 0, this.width, this.height); painter.restore(); // 如果未设备场景 if (this.scene == null) { return; } // 绘制背景 painter.save(); this.drawBackground(painter); painter.restore(); // 绘制场景 painter.save(); painter.translate(this.origin.x, this.origin.y); painter.scale(this.scale, this.scale); painter.rotate(this.rotate); this.scene.drawScene(painter, new SRect()); painter.restore(); // 绘制前景 painter.save(); this.drawForeground(painter); painter.restore(); } /** * 绘制场景背景 * * @param painter 绘制对象 */ protected drawBackground(painter: SPainter): void { painter.brush.color = this.backgroundColor; painter.pen.color = SColor.Transparent; painter.drawRect(0, 0, this.width, this.height); // DO NOTHING } /** * 绘制场景前景 * * @param painter 绘制对象 */ protected drawForeground(painter: SPainter): void { // DO NOTHING } /** * 鼠标双击事件 * * @param event 事件参数 */ protected onDoubleClick(event: MouseEvent): void { if (this.scene != null) { let ce = this.toSceneMotionEvent(event); this.scene.onDoubleClick(ce); } } /** * 鼠标按下事件 * * @param event 事件参数 */ protected onMouseDown(event: MouseEvent): void { super.onMouseDown(event); if (this.scene != null) { let ce = this.toSceneMotionEvent(event); this.scene.onMouseDown(ce); } } /** * 鼠标移动事件 * * @param event 事件参数 */ protected onMouseMove(event: MouseEvent): void { super.onMouseMove(event); if (this.scene != null) { let ce = this.toSceneMotionEvent(event); this.scene.onMouseMove(ce); } } /** * 鼠标松开事件 * * @param event 事件参数 */ protected onMouseUp(event: MouseEvent): void { super.onMouseUp(event); if (this.scene != null) { let ce = this.toSceneMotionEvent(event); this.scene.onMouseUp(ce); } } /** * 上下文菜单事件 * * @param event 事件参数 */ protected onContextMenu(event: MouseEvent): void { if (this.scene != null) { let ce = this.toSceneMotionEvent(event); this.scene.onContextMenu(ce); } } /** * 按键按下事件 * * @param event 事件参数 */ protected onKeyDown(event: KeyboardEvent): void { if (this.scene != null) { this.scene.onKeyDown(event); } } /** * 按键松开事件 * * @param event 事件参数 */ protected onKeyUp(event: KeyboardEvent): void { if (this.scene != null) { this.scene.onKeyUp(event); } } /** * 适配场景在视图中可见 * * @param width 宽度 * @param height 高度 * @param rect 对象的矩阵大小 */ private fitRectToSize( width: number, height: number, rect: SRect | null ): void { // 未设置场景 if (null == rect || !rect.isValid()) { return; } this.scale = Math.min(width / rect.width, height / rect.height) * 0.8; // 计算场景中心点 let center = rect.center(); this.origin.x = width / 2.0 - center.x * this.scale; this.origin.y = height / 2.0 - center.y * this.scale; } /** * MouseEvent 事件转换成场景 SMouseEvent 事件 * * @param event 事件参数 * @return MouseEvent 事件转换成场景 SMouseEvent 事件 */ private toSceneMotionEvent(event: MouseEvent): SMouseEvent { let se = new SMouseEvent(event); se.matrix.translate(this.origin.x, this.origin.y); se.matrix.scale(this.scale, this.scale); se.matrix.rotate(this.rotate); const mp = new SPoint(event.offsetX, event.offsetY).matrixTransform( se.matrix.inversed() ); se.x = mp.x; se.y = mp.y; return se; } }