import * as graphicsExtras from '@pixi/graphics-extras'; export { graphicsExtras as GraphicsExtras }; import { Graphics, Container, Text, Point, Matrix, Color, Polygon, DisplayObject, Rectangle, Application, BitmapFont, BitmapText } from 'pixi.js'; import EventEmitter from 'eventemitter3'; import { Viewport } from 'pixi-viewport'; import { Client } from '@stomp/stompjs'; import mqtt from 'mqtt'; /** * ID生成器 */ class IdGenerator { serial = 0; type; constructor(type) { this.type = type; } next() { ++this.serial; // console.log(this.getType() + this.serial) return this.serial; } getType() { return this.type; } initSerial(serial) { this.serial = serial; } } const GraphicIdGenerator = new IdGenerator(''); /** * 图形动画管理 */ class AnimationManager { app; _pause; /** * key - graphic.id */ graphicAnimationMap; constructor(app) { this.app = app; this._pause = false; this.graphicAnimationMap = new Map(); // 动画控制 app.pixi.ticker.add(this.run, this); } run(dt) { if (this._pause) { // 暂停 return; } this.graphicAnimationMap.forEach((map) => { map.forEach((animation) => { if (animation.running) { animation.run(dt); } }); }); } pause() { this._pause = true; } resume() { this._pause = false; } destroy() { this.app.pixi.ticker.remove(this.run, this); } /** * 图形对象的所有动画map * @param graphic * @returns */ animationMap(graphic) { let map = this.graphicAnimationMap.get(graphic.id); if (!map) { map = new Map(); this.graphicAnimationMap.set(graphic.id, map); } return map; } /** * 注册图形动画 * @param graphic * @param animation */ registerAnimation(graphic, animation) { this.animationMap(graphic).set(animation.name, animation); } /** * 删除图形动画 * @param graphic * @param name */ unregisterAnimation(graphic, name) { this.animationMap(graphic).delete(name); } /** * 删除所有图形动画 * @param graphic */ unregisterGraphicAnimations(graphic) { this.animationMap(graphic).clear(); } /** * 获取图形指定名称动画 * @param graphic * @param name * @returns */ animation(graphic, name) { return this.animationMap(graphic).get(name); } } var InteractionPluginType; (function (InteractionPluginType) { InteractionPluginType["App"] = "app"; InteractionPluginType["Graphic"] = "graphic"; InteractionPluginType["Other"] = "other"; })(InteractionPluginType || (InteractionPluginType = {})); class InteractionPluginBase { _type; name; // 唯一标识 app; _pause; constructor(app, name, type) { this._type = type; this.app = app; this.name = name; this._pause = true; app.registerInteractionPlugin(this); } /** * 恢复 */ resume() { this.bind(); this._pause = false; this.app.emit('interaction-plugin-resume', this); } /** * 停止 */ pause() { this.unbind(); this._pause = true; this.app.emit('interaction-plugin-pause', this); } /** * 是否生效 */ isActive() { return !this._pause; } isGraphicPlugin() { return this._type === InteractionPluginType.Graphic; } isAppPlugin() { return this._type === InteractionPluginType.App; } isOtherPlugin() { return this._type === InteractionPluginType.Other; } } class OtherInteractionPlugin extends InteractionPluginBase { constructor(app, name) { super(app, name, InteractionPluginType.Other); } } class AppDragEvent { app; type; target; original; start; // 画布坐标 constructor(app, type, target, original, start) { this.app = app; this.type = type; this.target = target; this.original = original; this.start = start; } get isMouse() { return this.original.pointerType === 'mouse'; } get isLeftButton() { return (this.isMouse && ((this.original.button === -1 && this.original.buttons === 1) || (this.original.button === 0 && this.original.buttons === 0))); } get isRightButton() { return (this.isMouse && ((this.original.button === -1 && this.original.buttons === 2) || (this.original.button === 2 && this.original.buttons === 0))); } get isMiddleButton() { return (this.isMouse && ((this.original.button === -1 && this.original.buttons === 4) || (this.original.button === 1 && this.original.buttons === 0))); } get isTouch() { return this.original.pointerType === 'touch'; } /** * 终点坐标(画布坐标) */ get end() { return this.app.toCanvasCoordinates(this.original.global); } get dx() { const move = this.original.movement; return move.x / this.app.viewport.scaled; } get dy() { const move = this.original.movement; return move.y / this.app.viewport.scaled; } get dsx() { return this.end.x - this.start.x; } get dsy() { return this.end.y - this.start.y; } /** * 转换为目标对象的位移距离 */ toTargetShiftLen(target) { const sl = target.canvasToLocalPoint(this.start); const el = target.canvasToLocalPoint(this.end); return { dx: el.x - sl.x, dy: el.y - sl.y }; } } /** * 拖拽操作插件 */ class DragPlugin extends OtherInteractionPlugin { static Name = '__drag_operation_plugin'; threshold = 3; target = null; start = null; startClientPoint = null; drag = false; constructor(app) { super(app, DragPlugin.Name); app.on('options-update', (options) => { if (options.threshold !== undefined) { this.threshold = options.threshold; } }); } static new(app) { return new DragPlugin(app); } bind() { const canvas = this.app.canvas; canvas.on('pointerdown', this.onPointerDown, this); } unbind() { const canvas = this.app.canvas; canvas.off('pointerdown', this.onPointerDown, this); canvas.off('pointerup', this.onPointerUp, this); canvas.off('pointerupoutside', this.onPointerUp, this); } onPointerDown(e) { this.target = e.target; this.start = this.app.toCanvasCoordinates(e.global); this.startClientPoint = e.global.clone(); const canvas = this.app.canvas; canvas.on('pointermove', this.onPointerMove, this); canvas.on('pointerup', this.onPointerUp, this); canvas.on('pointerupoutside', this.onPointerUp, this); } onPointerMove(e) { if (this.start && this.startClientPoint) { const current = e.global; const sgp = this.startClientPoint; const dragStart = Math.abs(current.x - sgp.x) > this.threshold || Math.abs(current.y - sgp.y) > this.threshold; if (this.target && this.start && !this.drag && dragStart) { this.app.emit('drag_op_start', new AppDragEvent(this.app, 'start', this.target, e, this.start)); this.drag = true; } // drag移动处理 if (this.target && this.drag && this.start) { this.app.emit('drag_op_move', new AppDragEvent(this.app, 'move', this.target, e, this.start)); } } } onPointerUp(e) { if (this.target && this.drag && this.start) { this.app.emit('drag_op_end', new AppDragEvent(this.app, 'end', this.target, e, this.start)); } else if (this.target && this.start && !this.drag) { // this.target.emit('click', this.target); const ade = new AppDragEvent(this.app, 'end', this.target, e, this.start); const graphic = this.target.getGraphic(); if (ade.isRightButton) { this.target.emit('_rightclick', e); if (graphic != null) { graphic.emit('_rightclick', e); } } else if (ade.isLeftButton) { this.target.emit('_leftclick', e); if (graphic != null) { graphic.emit('_leftclick', e); } } } const canvas = this.app.canvas; canvas.off('mousemove', this.onPointerMove, this); canvas.off('mouseup', this.onPointerUp, this); canvas.off('mouseupoutside', this.onPointerUp, this); this.clearCache(); } /** * 清理缓存 */ clearCache() { this.drag = false; this.start = null; this.startClientPoint = null; this.target = null; } } /** * 视口移动插件 */ class ViewportMovePlugin extends OtherInteractionPlugin { static Name = '__viewport_move_plugin'; static MoveInterval = 20; // 移动间隔,单位ms static TriggerRange = 100; // 边界触发范围,单位px static DefaultMoveSpeed = 200 / ViewportMovePlugin.MoveInterval; // 默认移动速度 moveHandler = null; moveSpeedx = 0; moveSpeedy = 0; constructor(app) { super(app, ViewportMovePlugin.Name); } static new(app) { return new ViewportMovePlugin(app); } pause() { super.pause(); this.stopMove(); } bind() { this.app.canvas.on('pointermove', this.viewportMove, this); } unbind() { this.app.canvas.off('pointermove', this.viewportMove, this); } startMove(moveSpeedx, moveSpeedy) { this.moveSpeedx = moveSpeedx; this.moveSpeedy = moveSpeedy; if (this.moveHandler == null) { const viewport = this.app.viewport; this.moveHandler = setInterval(() => { viewport.moveCorner(viewport.corner.x + this.moveSpeedx, viewport.corner.y + this.moveSpeedy); }, ViewportMovePlugin.MoveInterval); } } stopMove() { if (this.moveHandler != null) { clearInterval(this.moveHandler); this.moveHandler = null; this.app.canvas.cursor = 'auto'; } } calculateBoundaryMoveSpeed(sp) { let moveSpeedx = 0; let moveSpeedy = 0; const range = ViewportMovePlugin.TriggerRange; const viewport = this.app.viewport; if (sp.x < range) { moveSpeedx = this.calculateMoveSpeed(sp.x - range); } else if (sp.x > viewport.screenWidth - range) { moveSpeedx = this.calculateMoveSpeed(sp.x + range - viewport.screenWidth); } else { moveSpeedx = 0; } if (sp.y < range) { moveSpeedy = this.calculateMoveSpeed(sp.y - range); } else if (sp.y > viewport.screenHeight - range) { moveSpeedy = this.calculateMoveSpeed(sp.y + range - viewport.screenHeight); } else { moveSpeedy = 0; } return { moveSpeedx, moveSpeedy }; } calculateMoveSpeed(dd) { return ((dd / ViewportMovePlugin.TriggerRange) * ViewportMovePlugin.DefaultMoveSpeed); } viewportMove(e) { const sp = e.global; const { moveSpeedx, moveSpeedy } = this.calculateBoundaryMoveSpeed(sp); if (moveSpeedx == 0 && moveSpeedy == 0) { this.app.canvas.cursor = 'auto'; this.stopMove(); } else { this.app.canvas.cursor = 'grab'; this.startMove(moveSpeedx, moveSpeedy); } } } /** * 应用交互插件,同时只能生效一个 */ class AppInteractionPlugin extends InteractionPluginBase { constructor(name, app) { super(app, name, InteractionPluginType.App); } /** * 恢复,app交互插件同时只能生效一个 */ resume() { this.app.pauseAppInteractionPlugins(); super.resume(); } } /** * 图形交互插件,可同时生效 */ class GraphicInteractionPlugin { _type = InteractionPluginType.Graphic; app; name; // 唯一标识 _pause; constructor(name, app) { this.app = app; this.name = name; this._pause = true; app.registerInteractionPlugin(this); this.resume(); // 新增的图形对象绑定 this.app.on('graphicstored', (g) => { if (this.isActive()) { this.binds(this.filter(g)); } }); this.app.on('graphicdeleted', (g) => { if (this.isActive()) { this.unbinds(this.filter(g)); } }); } isActive() { return !this._pause; } isAppPlugin() { return false; } isOtherPlugin() { return false; } isGraphicPlugin() { return true; } resume() { const list = this.filter(...this.app.queryStore.getAllGraphics()); this.binds(list); this._pause = false; this.app.emit('interaction-plugin-resume', this); } pause() { const list = this.filter(...this.app.queryStore.getAllGraphics()); this.unbinds(list); this._pause = true; this.app.emit('interaction-plugin-pause', this); } binds(list) { if (list) { list.forEach((g) => this.bind(g)); } } unbinds(list) { if (list) { list.forEach((g) => this.unbind(g)); } } } class CompleteMouseToolOptions { boxSelect; viewportDrag; viewportDragLeft; wheelZoom; selectFilter; constructor() { this.boxSelect = true; this.viewportDrag = true; this.wheelZoom = true; this.viewportDragLeft = false; } update(options) { if (options.boxSelect != undefined) { this.boxSelect = options.boxSelect; } if (options.viewportDrag != undefined) { this.viewportDrag = options.viewportDrag; } if (options.viewportDragLeft != undefined) { this.viewportDragLeft = options.viewportDragLeft; } if (options.wheelZoom != undefined) { this.wheelZoom = options.wheelZoom; } this.selectFilter = options.selectFilter; } } /** * 通用交互工具 */ class CommonMouseTool extends AppInteractionPlugin { static Name = 'mouse-tool'; static SelectBox = '__select_box'; options; box; leftDownTarget = null; drag = false; graphicSelect = false; rightTarget = null; constructor(scene) { super(CommonMouseTool.Name, scene); this.options = new CompleteMouseToolOptions(); this.box = new Graphics(); this.box.name = CommonMouseTool.SelectBox; this.box.visible = false; this.app.canvas.addAssistantAppends(this.box); scene.on('options-update', (options) => { if (options.mouseToolOptions) { this.options.update(options.mouseToolOptions); if (this.isActive()) { this.pause(); this.resume(); } } }); } static new(app) { return new CommonMouseTool(app); } bind() { const canvas = this.app.canvas; canvas.on('mousedown', this.onMouseDown, this); canvas.on('mouseup', this.onMouseUp, this); this.app.on('drag_op_start', this.onDragStart, this); this.app.on('drag_op_move', this.onDragMove, this); this.app.on('drag_op_end', this.onDragEnd, this); if (this.options.viewportDrag) { if (this.options.viewportDragLeft) { this.app.viewport.drag({ mouseButtons: 'left', }); canvas.on('mousedown', this.setLeftCursor, this); canvas.on('mouseup', this.resumeLeftCursor, this); canvas.on('mouseupoutside', this.resumeLeftCursor, this); } else { this.app.viewport.drag({ mouseButtons: 'right', }); canvas.on('rightdown', this.setCursor, this); canvas.on('rightup', this.resumeCursor, this); canvas.on('rightupoutside', this.resumeCursor, this); } } if (this.options.wheelZoom) { this.app.viewport.wheel({ percent: 0.01, }); } } unbind() { const canvas = this.app.canvas; // 确保所有事件取消监听 canvas.off('mousedown', this.onMouseDown, this); canvas.off('mouseup', this.onMouseUp, this); this.app.off('drag_op_start', this.onDragStart, this); this.app.off('drag_op_move', this.onDragMove, this); this.app.off('drag_op_end', this.onDragEnd, this); this.app.viewport.plugins.remove('drag'); canvas.off('mousedown', this.setLeftCursor, this); canvas.off('mouseup', this.resumeLeftCursor, this); canvas.off('mouseupoutside', this.resumeLeftCursor, this); canvas.off('rightdown', this.setCursor, this); canvas.off('rightup', this.resumeCursor, this); canvas.off('rightupoutside', this.resumeCursor, this); this.app.viewport.plugins.remove('wheel'); this.clearCache(); } onDragStart(event) { if (this.boxSelect && event.target.isCanvas() && event.isLeftButton) { this.box.visible = true; this.app.interactionPlugin(ViewportMovePlugin.Name).resume(); } this.drag = true; } onDragMove(event) { if (this.boxSelect && event.target.isCanvas()) { this.boxSelectDraw(event.start, event.end); } } onDragEnd(event) { if (this.boxSelect && event.target.isCanvas() && event.isLeftButton) { this.boxSelectDraw(event.start, event.end); this.boxSelectGraphicCheck(); this.app.interactionPlugin(ViewportMovePlugin.Name).pause(); this.box.clear(); this.box.visible = false; } } setLeftCursor(e) { const target = e.target; this.leftDownTarget = target; if (target.isCanvas() && this.app.pixi.view.style) { this.app.pixi.view.style.cursor = 'grab'; } } resumeLeftCursor() { if (this.leftDownTarget && this.leftDownTarget.isCanvas() && this.app.pixi.view.style) { this.app.pixi.view.style.cursor = 'inherit'; } this.leftDownTarget = null; } setCursor(e) { const target = e.target; this.rightTarget = target; if (target.isCanvas() && this.app.pixi.view.style) { this.app.pixi.view.style.cursor = 'grab'; } } resumeCursor() { if (this.rightTarget && this.rightTarget.isCanvas() && this.app.pixi.view.style) { this.app.pixi.view.style.cursor = 'inherit'; } this.rightTarget = null; } onMouseDown(e) { this.leftDownTarget = e.target; this.graphicSelect = false; // 图形 const graphic = this.leftDownTarget.getGraphic(); if (graphic) { const app = this.app; // 图形选中 if (!e.ctrlKey && !graphic.selected && graphic.selectable) { app.updateSelected(graphic); graphic.childEdit = false; this.graphicSelect = true; } else if (!e.ctrlKey && graphic.selected && graphic.childEdit) { if (this.leftDownTarget.isGraphicChild() && this.leftDownTarget.selectable) { graphic.setChildSelected(this.leftDownTarget); } else { graphic.exitChildEdit(); } } } } /** * 选中处理 * @param e */ onMouseUp(e) { const app = this.app; if (!this.drag) { const target = e.target; const graphic = e.target.getGraphic(); if (graphic && graphic.selected && !this.graphicSelect && app.selectedGraphics.length == 1 && target === this.leftDownTarget && target.isGraphicChild() && target.selectable) { graphic.childEdit = true; } if (e.ctrlKey) { // 多选 if (graphic) { if (graphic.childEdit && target === this.leftDownTarget) { graphic.invertChildSelected(target); } else { graphic.invertSelected(); } } } else { // 非多选 if (e.target.isCanvas()) { this.app.updateSelected(); } else { if (graphic && graphic.childEdit && app.selectedGraphics.length === 1 && target === this.leftDownTarget) { graphic.setChildSelected(target); } } } // 多个图形选中,退出子元素编辑模式 const selecteds = this.app.selectedGraphics; if (selecteds.length > 1) { selecteds.forEach((g) => g.exitChildEdit()); } } this.clearCache(); } /** * 清理缓存 */ clearCache() { this.drag = false; this.leftDownTarget = null; } get boxSelect() { return this.options.boxSelect; } get selectFilter() { return this.options.selectFilter; } /** * 框选图形绘制并检查 */ boxSelectDraw(start, end) { if (!this.drag) return; // 绘制框选矩形框 this.box.clear(); this.box.lineStyle({ width: 2, color: this.app.appOptions.assistantElementColor || AppConsts.assistantElementColor, }); const dsx = end.x - start.x; const dsy = end.y - start.y; let { x, y } = start; if (dsx < 0) { x += dsx; } if (dsy < 0) { y += dsy; } const width = Math.abs(dsx); const height = Math.abs(dsy); this.box.drawRect(x, y, width, height); } /** * 框选图形判断 * @returns */ boxSelectGraphicCheck() { if (!this.drag) return; // 遍历筛选 const boxRect = this.box.getLocalBounds(); const app = this.app; const selects = []; app.queryStore.getAllGraphics().forEach((g) => { if ((this.selectFilter == undefined && g.visible) || (!!this.selectFilter && this.selectFilter(g))) { // 选择过滤器 if (g.boxIntersectCheck(boxRect)) { selects.push(g); } } }); app.updateSelected(...selects); } } let target; class GlobalKeyboardHelper { appKeyboardPluginMap = []; constructor() { window.onkeydown = (e) => { this.appKeyboardPluginMap.forEach((plugin) => { const listenerMap = plugin.getKeyListener(e); listenerMap?.forEach((listener) => { if (listener.global) { listener.press(e, plugin.app); } }); }); if (e.ctrlKey) { if (e.code == 'KeyS') { // 屏蔽全局Ctrl+S保存操作 return false; } } if (target && target.nodeName == 'CANVAS') { // 事件的目标是画布时,屏蔽总的键盘操作操作 if (e.ctrlKey) { if (e.code == 'KeyA' || e.code == 'KeyS') { // 屏蔽Canvas上的Ctrl+A、Ctrl+S操作 return false; } } } return true; }; window.onkeyup = (e) => { this.appKeyboardPluginMap.forEach((plugin) => { const listenerMap = plugin.getKeyListener(e); listenerMap?.forEach((listener) => { if (listener.global) { listener.release(e, plugin.app); } }); }); }; } registerGAKPlugin(plugin) { if (!this.appKeyboardPluginMap.find((pg) => pg == plugin)) { this.appKeyboardPluginMap.push(plugin); } } removeGAKPlugin(plugin) { const index = this.appKeyboardPluginMap.findIndex((pg) => pg == plugin); if (index >= 0) { this.appKeyboardPluginMap.splice(index, 1); } } } const GlobalKeyboardPlugin = new GlobalKeyboardHelper(); class JlGraphicAppKeyboardPlugin { app; /** * 结构为Map> */ keyListenerMap = new Map(); // 键值监听map keyListenerStackMap = new Map(); // 键值监听栈(多次注册相同的监听会把之前注册的监听器入栈,移除最新的监听会从栈中弹出一个作为指定事件监听处理器) constructor(app) { this.app = app; GlobalKeyboardPlugin.registerGAKPlugin(this); const onMouseUpdateTarget = (e) => { const node = e.target; target = node; }; const keydownHandle = (e) => { // console.debug(e.key, e.code, e.keyCode); if (target && target == this.app.dom?.getElementsByTagName('canvas')[0]) { const listenerMap = this.getKeyListener(e); listenerMap?.forEach((listener) => { if (!listener.global) { listener.press(e, this.app); } }); } }; const keyupHandle = (e) => { if (target && target == this.app.dom?.getElementsByTagName('canvas')[0]) { const listenerMap = this.getKeyListener(e); listenerMap?.forEach((listener) => { if (!listener.global) { listener.release(e, this.app); } }); } }; document.addEventListener('mousedown', onMouseUpdateTarget, false); document.addEventListener('keydown', keydownHandle, false); document.addEventListener('keyup', keyupHandle, false); this.app.on('destroy', () => { document.removeEventListener('mousedown', onMouseUpdateTarget, false); document.removeEventListener('keydown', keydownHandle, false); document.removeEventListener('keyup', keyupHandle, false); GlobalKeyboardPlugin.removeGAKPlugin(this); }); } getOrInit(key) { let map = this.keyListenerMap.get(key); if (map === undefined) { map = new Map(); this.keyListenerMap.set(key, map); } return map; } getOrInitStack(key) { let stack = this.keyListenerStackMap.get(key); if (stack === undefined) { stack = []; this.keyListenerStackMap.set(key, stack); } return stack; } /** * 注册按键监听,若有旧的,旧的入栈 * @param keyListener */ addKeyListener(keyListener) { const map = this.getOrInit(keyListener.value); // 查询是否有旧的监听,若有入栈 const old = map.get(keyListener.identifier); if (old) { const stack = this.getOrInitStack(keyListener.identifier); stack.push(old); } map.set(keyListener.identifier, keyListener); } /** * 移除按键监听,若是当前注册的监听,尝试从栈中取出作为按键监听器,若是旧的,则同时移除栈中的监听 * @param keyListener */ removeKeyListener(keyListener) { keyListener.onRemove(); const map = this.getOrInit(keyListener.value); const old = map.get(keyListener.identifier); map.delete(keyListener.identifier); const stack = this.getOrInitStack(keyListener.identifier); if (old && old === keyListener) { // 是旧的监听 const listener = stack.pop(); if (listener) { map.set(keyListener.identifier, listener); } } else { // 移除栈中的 const index = stack.findIndex((ls) => ls === keyListener); if (index >= 0) { stack.splice(index, 1); } } } getKeyListenerBy(key) { return this.keyListenerMap.get(key); } getKeyListener(e) { return (this.getKeyListenerBy(e.key) || this.getKeyListenerBy(e.code) || this.getKeyListenerBy(e.keyCode)); } isKeyListened(key) { return this.getOrInit(key).size > 0; } /** * 获取所有注册监听的键值(组合键) */ getAllListenedKeys() { const keys = []; this.keyListenerMap.forEach((v) => v.forEach((_listener, ck) => keys.push(ck))); return keys; } } var CombinationKey; (function (CombinationKey) { CombinationKey["Ctrl"] = "Ctrl"; CombinationKey["Alt"] = "Alt"; CombinationKey["Shift"] = "Shift"; })(CombinationKey || (CombinationKey = {})); const DefaultKeyListenerOptions = { value: '', combinations: [], global: false, onPress: undefined, pressTriggerAsOriginalEvent: false, onRelease: undefined, }; class KeyListener { // value 支持keyCode,key,code三种值 options; isPress = false; constructor(options) { this.options = Object.assign({}, DefaultKeyListenerOptions, options); } static create(options) { return new KeyListener(options); } get value() { return this.options.value; } get combinations() { return this.options.combinations; } get identifier() { return this.options.combinations.join('+') + '+' + this.options.value; } get global() { return this.options.global; } get onPress() { return this.options.onPress; } set onPress(v) { this.options.onPress = v; } get onRelease() { return this.options.onRelease; } set onRelease(v) { this.options.onRelease = v; } get pressTriggerEveryTime() { return this.options.pressTriggerAsOriginalEvent; } set pressTriggerEveryTime(v) { this.options.pressTriggerAsOriginalEvent = v; } press(e, app) { if (!this.checkCombinations(e)) { console.debug('组合键不匹配, 不执行press', e, this); return; } if (this.pressTriggerEveryTime || !this.isPress) { this.isPress = true; if (this.onPress) { this.onPress(e, app); } } } /** * 检查组合键是否匹配 */ checkCombinations(e) { const cbs = this.combinations; if (cbs.length > 0) { if (((e.altKey && cbs.includes(CombinationKey.Alt)) || (!e.altKey && !cbs.includes(CombinationKey.Alt))) && ((e.ctrlKey && cbs.includes(CombinationKey.Ctrl)) || (!e.ctrlKey && !cbs.includes(CombinationKey.Ctrl))) && ((e.shiftKey && cbs.includes(CombinationKey.Shift)) || (!e.shiftKey && !cbs.includes(CombinationKey.Shift)))) { return true; } } else { return !e.altKey && !e.ctrlKey && !e.shiftKey; } return false; } release(e, app) { if (this.isPress) { this.isPress = false; if (this.onRelease) { this.onRelease(e, app); } } } onRemove() { // 重置按下状态 this.isPress = false; } } /** * 图形复制插件 */ class GraphicCopyPlugin { container; scene; keyListeners; graphicControlers; copys; start; running = false; moveLimit; constructor(scene) { this.scene = scene; this.container = new Container(); this.graphicControlers = []; this.copys = []; this.keyListeners = []; this.keyListeners.push(new KeyListener({ // ESC 用于取消复制操作 value: 'Escape', global: true, // combinations: [CombinationKey.Ctrl], onPress: () => { this.cancle(); }, }), new KeyListener({ // X 限制只能在x轴移动 value: 'KeyX', global: true, // combinations: [CombinationKey.Ctrl], onPress: () => { this.updateMoveLimit('x'); }, }), new KeyListener({ // Y 限制只能在y轴移动 value: 'KeyY', global: true, // combinations: [CombinationKey.Ctrl], onPress: () => { this.updateMoveLimit('y'); }, })); } updateMoveLimit(limit) { if (this.moveLimit === limit) { this.moveLimit = undefined; } else { this.moveLimit = limit; } } init() { if (this.running) return; if (this.scene.selectedGraphics.length === 0) { throw new Error('没有选中图形,复制取消'); } this.running = true; this.copys = []; this.container.alpha = 0.5; this.scene.canvas.addChild(this.container); const app = this.scene; this.scene.selectedGraphics.forEach((g) => { const template = app.getGraphicTemplatesByType(g.type); const clone = template.clone(g); this.copys.push(clone); this.container.position.set(0, 0); this.container.addChild(clone); clone.repaint(); }); this.graphicControlers.forEach((graphicControler) => { if (graphicControler.check()) { this.keyListeners.push(...graphicControler.controlerList); } }); this.scene.canvas.on('mousemove', this.onPointerMove, this); this.scene.canvas.on('mouseup', this.onFinish, this); this.scene.canvas.on('rightup', this.cancle, this); this.keyListeners.forEach((kl) => { this.scene.app.addKeyboardListener(kl); }); } addGraphicControlers(graphicControlers) { this.graphicControlers.push(...graphicControlers); } clear() { this.running = false; this.start = undefined; this.moveLimit = undefined; this.copys = []; this.container.removeChildren(); this.scene.canvas.removeChild(this.container); this.scene.canvas.off('mousemove', this.onPointerMove, this); this.scene.canvas.off('mouseup', this.onFinish, this); this.scene.canvas.off('rightup', this.cancle, this); this.keyListeners.forEach((kl) => { this.scene.app.removeKeyboardListener(kl); }); this.keyListeners = this.keyListeners.splice(0, 3); } onPointerMove(e) { const cp = this.scene.toCanvasCoordinates(e.global); if (!this.start) { this.start = cp; } else { if (this.moveLimit === 'x') { const dx = cp.x - this.start.x; this.container.position.x = dx; this.container.position.y = 0; } else if (this.moveLimit === 'y') { const dy = cp.y - this.start.y; this.container.position.x = 0; this.container.position.y = dy; } else { const dx = cp.x - this.start.x; const dy = cp.y - this.start.y; this.container.position.x = dx; this.container.position.y = dy; } } for (let i = 0; i < this.graphicControlers.length; i++) { if (this.graphicControlers[i].moveLimitOption?.moveLimitName == this.moveLimit) { this.graphicControlers[i].moveLimitOption?.moveLimit(e); } break; } } onFinish() { console.log('复制确认'); // 将图形添加到app this.copys.forEach((g) => { g.position.x += this.container.position.x; g.position.y += this.container.position.y; }); this.saveCopyGraphic(); this.clear(); } saveCopyGraphic() { this.scene.app.addGraphicAndRecord(...this.copys); this.scene.detectRelations(); this.scene.updateSelected(...this.copys); } cancle() { console.log('复制操作取消'); this.scene.canvas.removeChild(this.container); this.clear(); } } class VectorGraphicUtil { static handle(obj) { const vg = obj; const onScaleChange = function (obj) { if (vg.isParent(obj)) { vg.updateOnScaled(); } }; const registerScaleChange = function registerScaleChange(obj) { if (!obj.scaledListenerOn) { obj.scaledListenerOn = true; obj.getCanvas().scene.on('viewport-scaled', onScaleChange); } }; const unregisterScaleChange = function unregisterScaleChange(obj) { obj.scaledListenerOn = false; obj.getCanvas().scene.off('viewport-scaled', onScaleChange); }; obj.onAddToCanvas = function onAddToCanvas() { obj.updateOnScaled(); registerScaleChange(obj); }; obj.onRemoveFromCanvas = function onRemoveFromCanvas() { // console.debug('矢量图像onRemoveFromCanvas'); unregisterScaleChange(obj); }; obj.on('added', (container) => { setTimeout(() => { if (container.isInCanvas()) { obj.onAddToCanvas(container.getCanvas()); } }, 0); }); } } /** * 矢量文字.实现原理:在缩放发生变化时,更新fontSize */ class VectorText extends Text { vectorFontSize = 8; scaled = 1; scaledListenerOn = false; constructor(text, style, canvas) { super(text, style, canvas); VectorGraphicUtil.handle(this); } updateOnScaled() { const scaled = this.getAllParentScaled(); const scale = Math.max(scaled.x, scaled.y); this.style.fontSize = this.vectorFontSize * scale; this.scale.set(1 / scale, 1 / scale); } /** * 设置矢量文字的字体大小 */ setVectorFontSize(fontSize) { if (this.vectorFontSize !== fontSize) { this.vectorFontSize = fontSize; this.updateOnScaled(); } } } /** * 拖拽点参数 */ const DraggablePointParam = { lineWidth: 1, lineColor: 0x000000, fillColor: 0xffffff, radius: 5, // 半径 }; const DraggablePointGraphic = new Graphics(); DraggablePointGraphic.lineStyle(DraggablePointParam.lineWidth, DraggablePointParam.lineColor); DraggablePointGraphic.beginFill(DraggablePointParam.fillColor); DraggablePointGraphic.drawCircle(0, 0, DraggablePointParam.radius); DraggablePointGraphic.endFill(); /** * 拖拽点,用于更新图形属性 */ class DraggablePoint extends Graphics { scaledListenerOn = false; /** * * @param point 画布坐标点 */ constructor(point) { super(DraggablePointGraphic.geometry); this.position.copyFrom(point); this.eventMode = 'static'; this.draggable = true; this.cursor = 'crosshair'; VectorGraphicUtil.handle(this); } updateOnScaled() { const scaled = this.getAllParentScaled(); const scale = Math.max(scaled.x, scaled.y); this.scale.set(1 / scale, 1 / scale); } } /** * 浮点数相等判断误差值 */ const epsilon = 0.00001; /** * 判断浮点数是不是0 * @param v * @returns */ function isZero(v) { if (Math.abs(v) < epsilon) { return true; } return false; } /** * 两浮点数是否相等 * @param f1 * @param f2 * @returns */ function floatEquals(f1, f2) { return isZero(f1 - f2); } /* eslint-disable @typescript-eslint/no-this-alias */ class Vector2 { constructor(values) { if (values !== undefined) { this.xy = values; } } static from(p) { return new Vector2([p.x, p.y]); } values = new Float32Array(2); static zero = new Vector2([0, 0]); static one = new Vector2([1, 1]); get x() { return this.values[0]; } set x(value) { this.values[0] = value; } get y() { return this.values[1]; } set y(value) { this.values[1] = value; } get xy() { return [this.values[0], this.values[1]]; } set xy(values) { this.values[0] = values[0]; this.values[1] = values[1]; } at(index) { return this.values[index]; } reset() { this.x = 0; this.y = 0; } copy(dest) { if (!dest) { dest = new Vector2(); } dest.x = this.x; dest.y = this.y; return dest; } negate(dest) { if (!dest) { dest = this; } dest.x = -this.x; dest.y = -this.y; return dest; } equals(vector, threshold = epsilon) { if (Math.abs(this.x - vector.x) > threshold) { return false; } if (Math.abs(this.y - vector.y) > threshold) { return false; } return true; } length() { return Math.sqrt(this.squaredLength()); } squaredLength() { const x = this.x; const y = this.y; return x * x + y * y; } add(vector) { this.x += vector.x; this.y += vector.y; return this; } subtract(vector) { this.x -= vector.x; this.y -= vector.y; return this; } multiply(vector) { this.x *= vector.x; this.y *= vector.y; return this; } divide(vector) { this.x /= vector.x; this.y /= vector.y; return this; } scale(value, dest) { if (!dest) { dest = this; } dest.x *= value; dest.y *= value; return dest; } normalize(dest) { if (!dest) { dest = this; } let length = this.length(); if (length === 1) { return this; } if (length === 0) { dest.x = 0; dest.y = 0; return dest; } length = 1.0 / length; dest.x *= length; dest.y *= length; return dest; } // multiplyMat2(matrix: mat2, dest?: Vector2): Vector2 { // if (!dest) { // dest = this; // } // return matrix.multiplyVec2(this, dest); // } // multiplyMat3(matrix: mat3, dest?: Vector2): Vector2 { // if (!dest) { // dest = this; // } // return matrix.multiplyVec2(this, dest); // } // static cross(vector: Vector2, vector2: Vector2, dest?: vec3): vec3 { // if (!dest) { // dest = new vec3(); // } // const x = vector.x; // const y = vector.y; // const x2 = vector2.x; // const y2 = vector2.y; // const z = x * y2 - y * x2; // dest.x = 0; // dest.y = 0; // dest.z = z; // return dest; // } /** * 向量点乘 * @param vector * @param vector2 * @returns */ static dot(vector, vector2) { return vector.x * vector2.x + vector.y * vector2.y; } /** * 向量长度 * @param vector * @param vector2 * @returns */ static distance(vector, vector2) { return Math.sqrt(this.squaredDistance(vector, vector2)); } /** * 向量长度平方 * @param vector * @param vector2 * @returns */ static squaredDistance(vector, vector2) { const x = vector2.x - vector.x; const y = vector2.y - vector.y; return x * x + y * y; } /** * v2->v1的方向的单位向量 * @param v1 * @param v2 * @param dest * @returns */ static direction(v1, v2, dest) { if (!dest) { dest = new Vector2(); } const x = v1.x - v2.x; const y = v1.y - v2.y; let length = Math.sqrt(x * x + y * y); if (length === 0) { dest.x = 0; dest.y = 0; return dest; } length = 1 / length; dest.x = x * length; dest.y = y * length; return dest; } static mix(vector, vector2, time, dest) { if (!dest) { dest = new Vector2(); } const x = vector.x; const y = vector.y; const x2 = vector2.x; const y2 = vector2.y; dest.x = x + time * (x2 - x); dest.y = y + time * (y2 - y); return dest; } /** * 向量加法 * @param vector * @param vector2 * @param dest * @returns */ static sum(vector, vector2, dest) { if (!dest) { dest = new Vector2(); } dest.x = vector.x + vector2.x; dest.y = vector.y + vector2.y; return dest; } /** * 向量减法 * @param vector * @param vector2 * @param dest * @returns */ static difference(vector, vector2, dest) { if (!dest) { dest = new Vector2(); } dest.x = vector.x - vector2.x; dest.y = vector.y - vector2.y; return dest; } /** * 向量乘法 * @param vector * @param vector2 * @param dest * @returns */ static product(vector, vector2, dest) { if (!dest) { dest = new Vector2(); } dest.x = vector.x * vector2.x; dest.y = vector.y * vector2.y; return dest; } /** * 向量除法 * @param vector * @param vector2 * @param dest * @returns */ static quotient(vector, vector2, dest) { if (!dest) { dest = new Vector2(); } dest.x = vector.x / vector2.x; dest.y = vector.y / vector2.y; return dest; } } /** * 递归父节点执行逻辑 * @param obj * @param handler */ function recursiveParents(obj, handler) { if (obj.parent) { handler(obj.parent); recursiveParents(obj.parent, handler); } } /** * 递归父节点查询父节点对象 * @param obj * @param finder * @returns */ function recursiveFindParent(obj, finder) { if (obj.parent) { if (finder(obj.parent)) { return obj.parent; } else { return recursiveFindParent(obj.parent, finder); } } return null; } /** * 递归子节点执行逻辑 * @param container * @param handler */ function recursiveChildren(container, handler) { container.children.forEach((child) => { handler(child); if (child.children) { recursiveChildren(child, handler); } }); } /** * 递归子节点查询子节点对象 */ function recursiveFindChild(container, finder) { let result = null; for (let i = 0; i < container.children.length; i++) { const child = container.children[i]; if (finder(child)) { return child; } else if (child.children) { result = recursiveFindChild(child, finder); } } return result; } /** * 判断贝塞尔曲线数据是否正确 * @param points */ function assertBezierPoints(points) { if (points.length < 4 || points.length % 3 !== 1) { throw new Error(`bezierCurve 数据错误: ${points}`); } } /** * 转换为贝塞尔曲线参数 * @param points * @returns */ function convertToBezierParams(points) { assertBezierPoints(points); const bps = []; for (let i = 0; i < points.length - 3; i += 3) { const p1 = new Point(points[i].x, points[i].y); const p2 = new Point(points[i + 3].x, points[i + 3].y); const cp1 = new Point(points[i + 1].x, points[i + 1].y); const cp2 = new Point(points[i + 2].x, points[i + 2].y); bps.push({ p1, p2, cp1, cp2, }); } return bps; } /** * 根据分段数计算贝塞尔曲线所有点坐标 * @param basePoints * @param segmentsCount * @returns */ function calculateBezierPoints(basePoints, segmentsCount) { const bps = convertToBezierParams(basePoints); const points = []; bps.forEach((bp) => { points.push(...calculateOneBezierPoints(bp.p1, bp.p2, bp.cp1, bp.cp2, segmentsCount)); }); return points; } /** * 根据分段数计算贝塞尔曲线所有点坐标 * @param basePoints * @param segmentsCount * @returns */ function calculateOneBezierPoints(p1, p2, cp1, cp2, segmentsCount) { const points = []; const fromX = p1.x; const fromY = p1.y; const n = segmentsCount; let dt = 0; let dt2 = 0; let dt3 = 0; let t2 = 0; let t3 = 0; const cpX = cp1.x; const cpY = cp1.y; const cpX2 = cp2.x; const cpY2 = cp2.y; const toX = p2.x; const toY = p2.y; points.push(new Point(p1.x, p1.y)); for (let i = 1, j = 0; i <= n; ++i) { j = i / n; dt = 1 - j; dt2 = dt * dt; dt3 = dt2 * dt; t2 = j * j; t3 = t2 * j; const px = dt3 * fromX + 3 * dt2 * j * cpX + 3 * dt * t2 * cpX2 + t3 * toX; const py = dt3 * fromY + 3 * dt2 * j * cpY + 3 * dt * t2 * cpY2 + t3 * toY; points.push(new Point(px, py)); } return points; } /** * 计算矩形中点 */ function getRectangleCenter(rectangle) { return new Point(rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2); } /** * 计算两个矩形中心对齐的坐标, PS: 计算的是较大包围框的中心 * @param rect1 * @param rect2 * @returns */ function getCenterOfTwoRectangle(rect1, rect2) { const x = Math.abs(rect1.width - rect2.width) / 2; const y = Math.abs(rect1.height - rect2.height) / 2; return new Point(x, y); } /** * 序列化图形变换 * @param obj * @returns */ function serializeTransform(obj) { const position = obj.position; const scale = obj.scale; const angle = obj.angle; const skew = obj.skew; return [position.x, position.y, scale.x, scale.y, angle, skew.x, skew.y]; } /** * 反序列化变换数据到图形对象 * @param obj * @param transform */ function deserializeTransformInto(obj, transform) { if (transform.length === 7) { obj.position.set(transform[0], transform[1]); obj.scale.set(transform[2], transform[3]); obj.angle = transform[4]; obj.skew.set(transform[5], transform[6]); } else if (transform.length > 0) { console.warn('错误的变换数据', transform); } } /** * 将直线转换为多边形 * @param p1 * @param p2 * @param thick * @returns */ function convertLineToPolygonPoints(p1, p2, thick) { const angle = Math.atan2(p2.y - p1.y, p2.x - p1.x) - Math.PI / 2; const half = thick / 2; const cos = Math.cos(angle) * half; const sin = Math.sin(angle) * half; return [ new Point(p1.x - cos, p1.y - sin), new Point(p2.x - cos, p2.y - sin), new Point(p2.x + cos, p2.y + sin), new Point(p1.x + cos, p1.y + sin), ]; } /** * 转换矩形为多边形点坐标 * @param rect 矩形 * @returns */ function convertRectangleToPolygonPoints(rect) { return [ new Point(rect.x, rect.y), new Point(rect.x + rect.width, rect.y), new Point(rect.x + rect.width, rect.y + rect.height), new Point(rect.x, rect.y + rect.height), ]; } /** * 计算线段中点坐标 * @param p1 * @param p2 * @returns */ function calculateLineMidpoint(p1, p2) { const x = (p1.x + p2.x) / 2; const y = (p1.y + p2.y) / 2; return new Point(x, y); } /** * 计算线段细分坐标--线段分成几份 * @param p1 * @param p2 * @param knife * @returns */ function calculateLineSegmentingPoint(p1, p2, knife) { const segmentingPoints = []; const x = p1.x < p2.x ? p1.x : p2.x; const y = p1.y < p2.y ? p1.y : p2.y; const w = Math.abs(p1.x - p2.x); const h = Math.abs(p1.y - p2.y); for (let i = 0; i < knife - 1; i++) { const pointX = x + (w * (i + 1)) / knife; const pointy = y + (h * (i + 1)) / knife; segmentingPoints.push(new Point(pointX, pointy)); } return segmentingPoints; } /** * 计算点到直线距离 * @param p1 * @param p2 * @param p */ function calculateDistanceFromPointToLine(p1, p2, p) { // 求直线的一般方程参数ABC,直线的一般式方程AX+BY+C=0 const A = p1.y - p2.y; const B = p2.x - p1.x; const C = p1.x * p2.y - p1.y * p2.x; // 计算点到直线垂直距离: d = |Ax+By+C|/sqrt(A*A+B*B),其中x,y为点坐标 const dl = Math.abs(A * p.x + B * p.y + C) / Math.sqrt(A * A + B * B); return dl; } /** * 计算点到直线的垂足坐标 * @param p * @param p1 * @param p2 */ function calculateFootPointFromPointToLine(p1, p2, p) { if (p1.x == p2.x && p1.y == p2.y) { throw new Error(`直线两坐标点相等:${p1}`); } const k = -(((p1.x - p.x) * (p2.x - p1.x) + (p1.y - p.y) * (p2.y - p1.y)) / (Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2))); if (isZero(k)) { return new Point(p.x, p.y); } const xf = k * (p2.x - p1.x) + p1.x; const yf = k * (p2.y - p1.y) + p1.y; return new Point(xf, yf); } /** * 计算直线与圆的交点 * 1用直线到圆心的距离和半径相比,判断是否和圆有交点; * 2求出圆心在直线上面的垂点; * 3算出直线的单位向量e; * 4求出一侧交点(Intersection)到projectPoint的长度(sideLength); * 5求出sideLength和这侧端点到projectPoint距离的比例(ratio); * 6projectPoint +/- ratio * e = 两侧交点; * @param p0 圆心坐标 * @param radius 圆半径 * @param p1 直线坐标1 * @param p2 直线坐标2 * @returns 交点坐标,可能2/1/0个 */ function calculateIntersectionPointOfCircleAndLine(p0, radius, p1, p2) { const distance = calculateDistanceFromPointToLine(p1, p2, p0); if (distance <= radius) { // 有交点 // 计算垂点 const pr = calculateFootPointFromPointToLine(p1, p2, p0); if (floatEquals(distance, radius)) { // 切线 return [pr]; } const vpr = new Vector2([pr.x, pr.y]); const vc = new Vector2([p0.x, p0.y]); // 计算直线单位向量 const v1 = new Vector2([p1.x, p1.y]); const v2 = new Vector2([p2.x, p2.y]); const ve = Vector2.direction(v2, v1); const base = Math.sqrt(Math.abs(radius * radius - Vector2.difference(vpr, vc).squaredLength())); const vl = ve.scale(base); const ip1 = Vector2.sum(vpr, vl); const ip2 = Vector2.difference(vpr, vl); return [new Point(ip1.x, ip1.y), new Point(ip2.x, ip2.y)]; } else { // 无交点 return []; } } /** * 计算圆心与圆心外一点与圆的交点(取圆心到点的向量与圆的交点) * @param p0 圆心坐标 * @param radius 圆半径 * @param p 点坐标 * @returns */ function calculateIntersectionPointOfCircleAndPoint(p0, radius, p) { const points = calculateIntersectionPointOfCircleAndLine(p0, radius, p0, p); const vc = new Vector2([p0.x, p0.y]); const vp = new Vector2([p.x, p.y]); const vecp = Vector2.direction(vp, vc); for (let i = 0; i < points.length; i++) { const ip = points[i]; const ve = Vector2.direction(new Vector2([ip.x, ip.y]), vc); if (ve.equals(vecp)) { return ip; } } throw new Error('计算圆心与圆心外一点与圆的交点逻辑错误'); } /** * 计算点基于点的镜像点坐标 * @param bp 基准点 * @param p 待镜像的点坐标 * @param distance 镜像点到基准点的距离,默认为p到基准点的距离,即对称 * @returns */ function calculateMirrorPoint(bp, p, distance) { const vbp = Vector2.from(bp); const vp = Vector2.from(p); const direction = Vector2.direction(vbp, vp); if (distance == undefined) { distance = Vector2.distance(vbp, vp); } const vmp = Vector2.sum(vbp, direction.scale(distance)); return new Point(vmp.x, vmp.y); } /** * 计算基于给定轴的给定点的镜像点坐标 * @param pa 给定轴线的坐标 * @param pb 给定轴线的坐标 * @param p 待镜像点坐标 * @param distance * @returns */ function calculateMirrorPointBasedOnAxis(pa, pb, p, distance) { const fp = calculateFootPointFromPointToLine(pa, pb, p); if (fp.equals(p)) { return fp; } else { return calculateMirrorPoint(fp, p, distance); } } /** * 计算直线与水平夹角,角度按顺时针,从0开始 * @param p1 * @param p2 * @returns 角度,范围[0, 360) */ function angleToAxisx(p1, p2) { if (p1.x == p2.x && p1.y == p2.y) { throw new Error('一个点无法计算角度'); } const dx = Math.abs(p1.x - p2.x); const dy = Math.abs(p1.y - p2.y); if (p2.x == p1.x) { if (p2.y > p1.y) { return 90; } else { return 270; } } if (p2.y == p1.y) { if (p2.x > p1.x) { return 0; } else { return 180; } } const angle = (Math.atan2(dy, dx) * 180) / Math.PI; if (p2.x > p1.x) { if (p2.y > p1.y) { return angle; } else if (p2.y < p1.y) { return 360 - angle; } } else if (p2.x < p1.x) { if (p2.y > p1.y) { return 180 - angle; } else { return 180 + angle; } } return angle; } /** * 计算两线夹角,pc与pa,pb的夹角,顺时针为正,逆时针为负 * @param pa 交点 * @param pb 锚定 * @param pc * @returns 夹角, [-180, 180] */ function angleOfIncludedAngle(pa, pb, pc) { const abAngle = angleToAxisx(pa, pb); const acAngle = angleToAxisx(pa, pc); let angle = acAngle - abAngle; if (angle < -180) { angle = 360 + angle; } else if (angle > 180) { angle = -(360 - angle); } return angle; } /** * 计算两点连线的法向量 * @param point1 * @param point2 * @returns 单位法向量 */ function getNormalVector(point1, point2) { const x1 = point1.x, y1 = point1.y; const x2 = point2.x, y2 = point2.y; const length = Math.sqrt((y2 - y1) ** 2 + (x2 - x1) ** 2); return [(y2 - y1) / length, (x1 - x2) / length]; } /** * 点延向量方向移动 * @param point * @param normal 单位向量 * @param length 平移长度 * @returns 移动后的点 */ function movePointAlongNormal(point, normal, length) { const newPoint = new Point(point.x + length * normal[0], point.y + length * normal[1]); return newPoint; } /** * 计算两组点各自组成直线的相交点(若两线平行 返回第一组坐标第一个点) * @param line1 两点坐标列表 * @param line2 两点坐标列表 * @returns 相交点 */ function getIntersectionPoint(line1, line2) { const a1 = line1[0], b1 = line1[1]; const a2 = line1[2], b2 = line1[3]; const a3 = line2[0], b3 = line2[1]; const a4 = line2[2], b4 = line2[3]; const denominator = (a3 - a4) * (b1 - b2) - (a1 - a2) * (b3 - b4); if (denominator === 0) { return new Point(a1, b1); } const x = ((a3 - a4) * (a2 * b1 - a1 * b2) - (a1 - a2) * (a4 * b3 - a3 * b4)) / denominator; const y = ((b3 - b4) * (b2 * a1 - b1 * a2) - (b1 - b2) * (b4 * a3 - b3 * a4)) / -denominator; return new Point(x, y); } /** * 是否平行线 * @param p1 * @param p2 * @param pa * @param pb * @returns */ function isParallelLines(p1, p2, pa, pb) { const vle1 = Vector2.direction(Vector2.from(p1), Vector2.from(p2)); const vle2 = Vector2.direction(Vector2.from(pa), Vector2.from(pb)); if (vle2.equals(vle1)) { return true; } return vle1.equals(Vector2.direction(Vector2.from(pb), Vector2.from(pa))); } /** * 点是否在线段上 * @param p1 * @param p2 * @param p * @returns */ function isPointOnLine(p1, p2, p) { const vp1 = Vector2.from(p1); const vp2 = Vector2.from(p2); const vp = Vector2.from(p); if (vp1.equals(vp) || vp2.equals(vp)) { return true; } const vle = Vector2.direction(vp1, Vector2.from(p2)); const vpe = Vector2.direction(vp1, vp); if (vle.equals(vpe)) { return (Vector2.difference(vp1, vp2).squaredLength() >= Vector2.difference(vp1, vp).squaredLength()); } return false; } /** * 两条线段是否存在包含关系 * @param line1 * @param line2 * @returns */ function isLineContainOther(line1, line2) { return ((isPointOnLine(line1.p1, line1.p2, line2.p1) && isPointOnLine(line1.p1, line1.p2, line2.p2)) || (isPointOnLine(line2.p1, line2.p2, line1.p1) && isPointOnLine(line2.p1, line2.p2, line1.p2))); } /** 均分线段, 返回各线段端点 */ function splitLineEvenly(p1, p2, count) { const [stepX, stepY] = [(p2.x - p1.x) / count, (p2.y - p1.y) / count]; return Array(count) .fill(1) .map((_, i) => { return [ { x: p1.x + stepX * i, y: p1.y + stepY * i }, { x: p1.x + stepX * (i + 1), y: p1.y + stepY * (i + 1) }, ]; }); } function splitPolyline(points, count) { if (points.length !== 2) { let totalLen = 0; const lengths = []; for (let i = 1; i < points.length; i++) { const { x: x1, y: y1 } = points[i - 1], { x: x2, y: y2 } = points[i]; const len = new Vector2([x2 - x1, y2 - y1]).length(); totalLen += len; lengths.push(len); } const counts = lengths.map((length) => Math.round((count * length) / totalLen)); if (counts.reduce((p, c) => p + c, 0) !== count) { const intersection = counts.reduce((p, c) => p + c, 0) - count; let maxCountIndex = 0, maxCount = 0; counts.forEach((c, i) => { if (c > maxCount) { maxCount = c; maxCountIndex = i; } }); counts[maxCountIndex] + intersection; } return counts .map((count, i) => { return splitLineEvenly(points[i], points[i + 1], count); }) .flat(); } else { return splitLineEvenly(points[0], points[points.length - 1], count); } } function getParallelOfPolyline(points, offset, side) { const { PI, cos, acos } = Math; const angleBase = side === 'L' ? -PI / 2 : PI / 2; return points.map((p, i) => { let baseUnitVec; //上一段的基准单位向量 let angle; //偏转角度 let len; //结合偏转角度的实际偏移量 if (!points[i - 1] || !points[i + 1]) { angle = angleBase; len = offset; baseUnitVec = points[i - 1] ? new Vector2([ p.x - points[i - 1].x, p.y - points[i - 1].y, ]).normalize() : new Vector2([ points[i + 1].x - p.x, points[i + 1].y - p.y, ]).normalize(); } else { const vp = new Vector2([p.x - points[i - 1].x, p.y - points[i - 1].y]); const vn = new Vector2([points[i + 1].x - p.x, points[i + 1].y - p.y]); const cosTheta = Vector2.dot(vn, vp) / (vp.length() * vn.length()); const direction = vp.x * vn.y - vp.y * vn.x > 0; //det(vp|vn)>0? const theta = direction ? acos(cosTheta) : -acos(cosTheta); angle = angleBase + theta / 2; len = offset / cos(theta / 2); baseUnitVec = Vector2.from(vp).normalize(); } return new Matrix() .scale(len, len) .rotate(angle) .translate(p.x, p.y) .apply(baseUnitVec); }); } // /** // * 点线碰撞检测 // * @param pa 线段a端坐标 // * @param pb 线段b端坐标 // * @param p 点坐标 // * @param tolerance 容忍度,越大检测范围越大 // * @returns // */ // export function linePoint(pa: Point, pb: Point, p: Point, tolerance: number): boolean { // return (Math.abs(distanceSquared(pa.x, pa.y, pb.x, pb.y) - (distanceSquared(pa.x, pa.y, p.x, p.y) + distanceSquared(pb.x, pb.y, p.x, p.y))) <= tolerance) // } /** * 根据点到直线的垂直距离计算碰撞 * @param pa 线段a端坐标 * @param pb 线段b端坐标 * @param p 点坐标 * @param lineWidth 线宽 * @param exact 是否精确(使用给定线宽,否则线宽会设置为8) * @returns */ function linePoint(pa, pb, p, lineWidth, exact = false) { if (!exact && lineWidth < 6) { lineWidth = 6; } // 求直线的一般方程参数ABC,直线的一般式方程AX+BY+C=0 const A = pa.y - pb.y; const B = pb.x - pa.x; const C = pa.x * pb.y - pa.y * pb.x; // 计算点到直线垂直距离: d = |Ax+By+C|/sqrt(A*A+B*B),其中x,y为点坐标 const dl = Math.abs(A * p.x + B * p.y + C) / Math.sqrt(A * A + B * B); const intersect = dl <= lineWidth / 2; if (intersect) { // 距离在线宽范围内,再判断点是否超过线段两端点范围外(两端点外会有一点误差,两端点线宽一半半径的圆范围内) const da = distance(pa.x, pa.y, p.x, p.y); const db = distance(pb.x, pb.y, p.x, p.y); const dab = distance(pa.x, pa.y, pb.x, pb.y); return da <= dl + dab && db <= dl + dab; } return false; } /** * 折线与点碰撞 * @param points 折线端点列表 * @param p 点座标 * @param lineWidth 线宽 */ function polylinePoint(points, p, lineWidth) { const len = points.length; for (let i = 0; i < len - 1; i++) { if (linePoint(points[i], points[i + 1], p, lineWidth)) { return true; } } return false; } /** * 线线碰撞检测 * @param pa 线段1a端坐标 * @param pb 线段1b端坐标 * @param p1 线段2a端坐标 * @param p2 线段2b端坐标 * @returns */ function lineLine(pa, pb, p1, p2) { const x1 = pa.x; const y1 = pa.y; const x2 = pb.x; const y2 = pb.y; const x3 = p1.x; const y3 = p1.y; const x4 = p2.x; const y4 = p2.y; const s1_x = x2 - x1; const s1_y = y2 - y1; const s2_x = x4 - x3; const s2_y = y4 - y3; const s = (-s1_y * (x1 - x3) + s1_x * (y1 - y3)) / (-s2_x * s1_y + s1_x * s2_y); const t = (s2_x * (y1 - y3) - s2_y * (x1 - x3)) / (-s2_x * s1_y + s1_x * s2_y); return s >= 0 && s <= 1 && t >= 0 && t <= 1; } /** * 点和矩形碰撞检测 * @param p 点作弊 * @param rect 矩形 * @returns */ function pointBox(p, rect) { const { x, y, width, height } = rect; const x2 = p.x; const y2 = p.y; return x2 >= x && x2 <= x + width && y2 >= y && y2 <= y + height; } /** * 线和矩形碰撞检测 * @param pa 线段a端坐标 * @param pb 线段b端坐标 * @param rect 矩形 * @returns */ function lineBox(pa, pb, rect) { if (pointBox(pa, rect) || pointBox(pb, rect)) { return true; } const { x, y, width, height } = rect; const rp1 = new Point(x, y); const rp2 = new Point(x + width, y); const rp3 = new Point(x + width, y + height); const rp4 = new Point(x, y + height); return (lineLine(pa, pb, rp1, rp2) || lineLine(pa, pb, rp2, rp3) || lineLine(pa, pb, rp3, rp4) || lineLine(pa, pb, rp1, rp4)); } /** * 多线段和矩形碰撞检测 * @param points * @param rect * @returns false / 碰撞的线段序号 */ function polylineBox(points, rect) { if (points.length < 2) { return false; } for (let i = 0; i < points.length - 1; i++) { const p1 = points[i]; const p2 = points[i + 1]; if (lineBox(p1, p2, rect)) { return true; } } return false; } /** * 两点碰撞检测 * @param p1 * @param p2 * @param tolerance * @returns */ function pointPoint2(p1, p2, tolerance) { return pointPoint(p1.x, p1.y, p2.x, p2.y, tolerance); } /** * 两点碰撞检测 * @param x1 * @param y1 * @param x2 * @param y2 * @param tolerance 容忍度/两点半径和 * @returns */ function pointPoint(x1, y1, x2, y2, tolerance) { return distance(x1, y1, x2, y2) <= tolerance; } /** * 两点距离 * @param x1 * @param y1 * @param x2 * @param y2 * @returns */ function distance(x1, y1, x2, y2) { return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)); } /** * 两点距离 * @param p1 * @param p2 * @returns */ function distance2(p1, p2) { return distance(p1.x, p1.y, p2.x, p2.y); } /** * 圆和点的碰撞检测 * @param x1 圆心x * @param y1 圆心y * @param r1 圆半径 * @param x2 点x * @param y2 点y * @returns */ function circlePoint(x1, y1, r1, x2, y2) { const x = x2 - x1; const y = y2 - y1; return x * x + y * y <= r1 * r1; } /** * 圆和点的碰撞检测--不包括圆内部 */ function circlePoint2(x1, y1, r1, x2, y2, tolerance) { const x = x2 - x1; const y = y2 - y1; return (x * x + y * y <= (r1 + tolerance) * (r1 + tolerance) && x * x + y * y >= (r1 - tolerance) * (r1 - tolerance)); } /** * 点和多边形碰撞检测 */ function pointPolygon(p, points, lineWidth) { const { x, y } = p; const length = points.length; let c = false; let i, j; for (i = 0, j = length - 1; i < length; i++) { if (points[i].y > y !== points[j].y > y && x < ((points[j].x - points[i].x) * (y - points[i].y)) / (points[j].y - points[i].y) + points[i].x) { c = !c; } j = i; } if (c) { return true; } for (i = 0; i < length - 1; i++) { let p1, p2; if (i === length - 1) { p1 = points[i]; p2 = points[0]; } else { p1 = points[i]; p2 = points[i + 1]; } if (linePoint(p1, p2, p, lineWidth)) { return true; } } return false; } /** * 线和多边形碰撞检测 * @param p1 * @param p2 * @param points * @param tolerance 多边形包围线宽 * @returns */ function linePolygon(p1, p2, points, lineWidth, polygonWidth) { if (pointPolygon(p1, points, polygonWidth)) { return true; } const length = points.length; for (let i = 0; i < length; i++) { let pa, pb; if (i === length - 1) { pa = points[i]; pb = points[0]; } else { pa = points[i]; pb = points[i + 1]; } // TODO:此处后续需考虑有线宽的情况 if (lineLine(pa, pb, p1, p2)) { return true; } } return false; } /** * 多边线与多边形碰撞检测 * @param polylinePoints 多边线所有点坐标 * @param polygonPoints 多边形所有点坐标 * @param polylineWidth 多边线的线宽 * @param polygonWidth 多边形线宽 * @returns */ function polylinePolygon(polylinePoints, polygonPoints, polylineWidth, polygonWidth) { const length = polylinePoints.length; for (let i = 0; i < length - 1; i++) { const p1 = polylinePoints[i]; const p2 = polylinePoints[i + 1]; if (linePolygon(p1, p2, polygonPoints, polylineWidth, polygonWidth)) { return true; } } return false; } function debounce(fn, waitMs = 250) { let timeoutId; const debouncedFunction = function (context, ...args) { const invokeFunction = function () { timeoutId = undefined; fn.apply(context, args); }; if (timeoutId !== undefined) { // console.debug('debounce clear timeout', fn); clearTimeout(timeoutId); } timeoutId = setTimeout(invokeFunction, waitMs); }; debouncedFunction.cancel = function () { if (timeoutId !== undefined) { clearTimeout(timeoutId); } }; return debouncedFunction; } const UP = new Point(0, -1); const DOWN = new Point(0, 1); const LEFT = new Point(-1, 0); const RIGHT = new Point(1, 0); /** * 越界结果 */ class OutOfBound { left; top; right; bottom; constructor(left, top, right, bottom) { this.left = left; this.top = top; this.right = right; this.bottom = bottom; } static check(rect, bound) { const left = rect.left < bound.left; const top = rect.top < bound.top; const right = rect.right > bound.right; const bottom = rect.bottom > bound.bottom; return new OutOfBound(left, top, right, bottom); } static none() { return new OutOfBound(false, false, false, false); } static leftOut() { return new OutOfBound(true, false, false, false); } static topOut() { return new OutOfBound(false, true, false, false); } static rightOut() { return new OutOfBound(false, false, true, false); } static bottomOut() { return new OutOfBound(false, false, false, true); } static leftTopOut() { return new OutOfBound(true, true, false, false); } static rightBottomOut() { return new OutOfBound(false, false, true, true); } } /* eslint-disable @typescript-eslint/no-unused-vars */ /** * 可吸附点图形参数 */ const AbsorbablePointParam = { lineWidth: 1, lineColor: '#000000', fillColor: '#E77E0E', radius: 5, // 半径 }; const AbsorbablePointGraphic = new Graphics(); // AbsorbablePointGraphic.lineStyle( // AbsorbablePointParam.lineWidth, // AbsorbablePointParam.lineColor // ); AbsorbablePointGraphic.beginFill(AbsorbablePointParam.fillColor); AbsorbablePointGraphic.drawCircle(0, 0, AbsorbablePointParam.radius); AbsorbablePointGraphic.endFill(); /** * 可吸附点 */ class AbsorbablePoint extends Graphics { _point; absorbRange; scaledListenerOn = false; /** * * @param point 画布坐标 * @param absorbRange */ constructor(point, absorbRange = 10) { super(AbsorbablePointGraphic.geometry); this._point = new Point(point.x, point.y); this.absorbRange = absorbRange; this.position.copyFrom(this._point); this.interactive; VectorGraphicUtil.handle(this); } compareTo(other) { if (other instanceof AbsorbablePoint) { return this.absorbRange - other.absorbRange; } throw new Error('非可吸附点'); } isOverlapping(other) { if (other instanceof AbsorbablePoint) { return (this._point.equals(other._point) && this.absorbRange === other.absorbRange); } return false; } tryAbsorb(...objs) { for (let i = 0; i < objs.length; i++) { const obj = objs[i]; const canvasPosition = obj.getPositionOnCanvas(); if (distance(this._point.x, this._point.y, canvasPosition.x, canvasPosition.y) < this.absorbRange) { obj.updatePositionByCanvasPosition(this._point); } } } updateOnScaled() { const scaled = this.getAllParentScaled(); const scale = Math.max(scaled.x, scaled.y); this.scale.set(1 / scale, 1 / scale); } } /** * 可吸附线 */ class AbsorbableLine extends Graphics { p1; p2; absorbRange; _color = '#E77E0E'; /** * * @param p1 画布坐标 * @param p2 画布坐标 * @param absorbRange */ constructor(p1, p2, absorbRange = 20) { super(); this.p1 = new Point(p1.x, p1.y); this.p2 = new Point(p2.x, p2.y); this.absorbRange = absorbRange; this.redraw(); } isOverlapping(other) { if (other instanceof AbsorbableLine) { const contain = isLineContainOther({ p1: this.p1, p2: this.p2 }, { p1: other.p1, p2: other.p2 }); return contain; } return false; } compareTo(other) { if (other instanceof AbsorbableLine) { return distance2(this.p1, this.p2) - distance2(other.p1, other.p2); } throw new Error('非可吸附线'); } redraw() { this.clear(); this.lineStyle(1, new Color(this._color)); this.moveTo(this.p1.x, this.p1.y); this.lineTo(this.p2.x, this.p2.y); } tryAbsorb(...objs) { for (let i = 0; i < objs.length; i++) { const obj = objs[i]; const canvasPosition = obj.getPositionOnCanvas(); if (linePoint(this.p1, this.p2, canvasPosition, this.absorbRange, true)) { const fp = calculateFootPointFromPointToLine(this.p1, this.p2, canvasPosition); obj.updatePositionByCanvasPosition(fp); } } } } /** * 可吸附圆 */ class AbsorbableCircle extends Graphics { absorbRange; p0; radius; _color = '#E77E0E'; /** * * @param p 画布坐标 * @param radius * @param absorbRange */ constructor(p, radius, absorbRange = 10) { super(); this.p0 = new Point(p.x, p.y); this.radius = radius; this.absorbRange = absorbRange; this.redraw(); } isOverlapping(other) { if (other instanceof AbsorbableCircle) { return this.p0.equals(other.p0) && this.radius === other.radius; } return false; } compareTo(other) { if (other instanceof AbsorbableCircle) { return this.absorbRange - other.absorbRange; } throw new Error('非可吸附圆'); } redraw() { this.clear(); this.lineStyle(1, new Color(this._color)); this.drawCircle(this.p0.x, this.p0.y, this.radius); } tryAbsorb(...objs) { for (let i = 0; i < objs.length; i++) { const obj = objs[i]; const canvasPosition = obj.getPositionOnCanvas(); const len = distance(this.p0.x, this.p0.y, canvasPosition.x, canvasPosition.y); if (len > this.radius - this.absorbRange && len < this.radius + this.absorbRange) { // 吸附,计算直线与圆交点,更新对象坐标 const p = calculateIntersectionPointOfCircleAndPoint(this.p0, this.radius, canvasPosition); obj.updatePositionByCanvasPosition(p); } } } } const DefaultDashedLineOptions = { length: 4, startSpace: 0, space: 4, lineWidth: 1, color: '#0000ff', }; class DashedLine extends Container { p1; p2; _options; constructor(p1, p2, options) { super(); const config = Object.assign({}, DefaultDashedLineOptions, options); this._options = config; this.p1 = new Point(p1.x, p1.y); this.p2 = new Point(p2.x, p2.y); this.redraw(); } setOptions(options) { if (options.startSpace != undefined) { this._options.startSpace = options.startSpace; } if (options.length != undefined) { this._options.length = options.length; } if (options.space != undefined) { this._options.space = options.space; } if (options.lineWidth != undefined) { this._options.lineWidth = options.lineWidth; } if (options.color != undefined) { this._options.color = options.color; } this.redraw(); } redraw() { this.removeChildren(); const p1 = this.p1; const p2 = this.p2; const option = this._options; const total = Math.pow(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2), 0.5); let len = option.startSpace; while (len < total) { let dashedLen = option.length; if (len + option.length > total) { dashedLen = total - len; } const line = new Graphics(); line.lineStyle(option.lineWidth, option.color); line.moveTo(len, 0); line.lineTo(len + dashedLen, 0); this.addChild(line); len = len + dashedLen + option.space; } this.pivot.set(0, option.lineWidth / 2); this.position.set(p1.x, p1.y); this.angle = angleToAxisx(p1, p2); } } class GraphicEditPlugin extends Container { graphic; constructor(g) { super(); this.graphic = g; this.zIndex = 2; this.sortableChildren = true; this.graphic.on('transformstart', this.hideAll, this); this.graphic.on('transformend', this.showAll, this); this.graphic.on('repaint', this.updateEditedPointsPosition, this); } destroy(options) { this.graphic.off('transformstart', this.hideAll, this); this.graphic.off('transformend', this.showAll, this); this.graphic.off('repaint', this.updateEditedPointsPosition, this); super.destroy(options); } hideAll() { this.visible = false; } showAll() { this.updateEditedPointsPosition(); this.visible = true; } } class LineEditPlugin extends GraphicEditPlugin { linePoints; editedPoints = []; constructor(g) { super(g); this.linePoints = g.linePoints; this.graphic.on('dataupdate', this.reset, this); } destroy(options) { this.graphic.off('dataupdate', this.reset, this); super.destroy(options); } reset() { this.linePoints = this.graphic.linePoints; this.removeChildren(); this.editedPoints.splice(0, this.editedPoints.length); this.initEditPoints(); } } function getWayLineIndex(points, p) { let start = 0; let end = 0; let minDistance = 0; for (let i = 1; i < points.length; i++) { const sp = points[i - 1]; const ep = points[i]; let distance = calculateDistanceFromPointToLine(sp, ep, p); distance = Math.round(distance * 100) / 100; if (i == 1) { minDistance = distance; } if (distance == minDistance) { const minX = Math.min(sp.x, ep.x); const maxX = Math.max(sp.x, ep.x); const minY = Math.min(sp.y, ep.y); const maxY = Math.max(sp.y, ep.y); const point = calculateFootPointFromPointToLine(sp, ep, p); if (point.x >= minX && point.x <= maxX && point.y >= minY && point.y <= maxY) { start = i - 1; } } if (distance < minDistance) { minDistance = distance; start = i - 1; } } end = start + 1; return { start, end }; } function getWaypointRangeIndex(points, curve, p, lineWidth) { let start = 0; let end = 0; if (!curve) { // 直线 for (let i = 1; i < points.length; i++) { const sp = points[i - 1]; const ep = points[i]; const fp = calculateFootPointFromPointToLine(sp, ep, p); if (linePoint(sp, ep, fp, 1, true)) { start = i - 1; end = i; break; } } } else { // 贝塞尔曲线 const bps = convertToBezierParams(points); for (let i = 0; i < bps.length; i++) { const bp = bps[i]; if (pointPolygon(p, [bp.p1, bp.cp1, bp.cp2, bp.p2], lineWidth)) { start = i * 3; end = start + 3; } } // assertBezierPoints(points); // for (let i = 0; i < points.length - 3; i += 3) { // const p1 = points[i]; // const cp1 = points[i + 1]; // const cp2 = points[i + 2]; // const p2 = points[i + 3]; // if (pointPolygon(p, [p1, cp1, cp2, p2], lineWidth)) { // start = i; // end = i + 3; // } // } } return { start, end }; } /** * 折线编辑(兼容线段) */ class PolylineEditPlugin extends LineEditPlugin { static Name = 'line_points_edit'; options; constructor(g, options) { super(g); this.options = Object.assign({}, options); this.name = PolylineEditPlugin.Name; this.initEditPoints(); } initEditPoints() { const cps = this.graphic.localToCanvasPoints(...this.linePoints); for (let i = 0; i < cps.length; i++) { const p = cps[i]; const dp = new DraggablePoint(p); dp.on('transforming', () => { const tlp = this.graphic.canvasToLocalPoint(dp.position); const cp = this.linePoints[i]; cp.x = tlp.x; cp.y = tlp.y; this.graphic.repaint(); }); if (this.options.onEditPointCreate) { this.options.onEditPointCreate(this.graphic, dp, i); } this.editedPoints.push(dp); } this.addChild(...this.editedPoints); } updateEditedPointsPosition() { const cps = this.graphic.localToCanvasPoints(...this.linePoints); if (cps.length === this.editedPoints.length) { for (let i = 0; i < cps.length; i++) { const cp = cps[i]; this.editedPoints[i].position.copyFrom(cp); } } } } function addWayPoint(graphic, curve, start, end, p) { if (!curve) { addLineWayPoint(graphic, start, end, p); } else { addBezierWayPoint(graphic, start, end, p); } } function addLineWayPoint(graphic, start, end, p) { const linePoints = graphic.linePoints; const points = linePoints.slice(0, start + 1); points.push(new Point(p.x, p.y)); points.push(...linePoints.slice(end)); graphic.linePoints = points; } function addPolygonSegmentingPoint(graphic, start, end, knife = 2) { const linePoints = graphic.linePoints; const points = linePoints.slice(0, start + 1); points.push(...calculateLineSegmentingPoint(linePoints[start], linePoints[end], knife)); points.push(...linePoints.slice(end)); graphic.linePoints = points; } function assertBezierWayPoint(i) { const c = i % 3; if (c !== 0) { throw new Error(`i=${i}的点不是路径点`); } } function addBezierWayPoint(graphic, start, end, p) { if (start === end) { console.error('添加贝塞尔曲线路径点开始结束点相等', start); throw new Error('开始结束点不能一致'); } assertBezierWayPoint(start); assertBezierWayPoint(end); const linePoints = graphic.linePoints; const points = linePoints.slice(0, start + 2); const ap = new Point(p.x, p.y); points.push(ap.clone(), ap.clone(), ap.clone()); points.push(...linePoints.slice(end - 1)); graphic.linePoints = points; } function removeWayPoint(graphic, curve, i) { if (!curve) { removeLineWayPoint(graphic, i); } else { removeBezierWayPoint(graphic, i); } } function removeLineWayPoint(graphic, i) { const linePoints = graphic.linePoints; if (linePoints.length > 2) { const points = linePoints.slice(0, i); points.push(...linePoints.slice(i + 1)); graphic.linePoints = points; } } function removeBezierWayPoint(graphic, i) { let points; const linePoints = graphic.linePoints; const c = i % 3; if (c !== 0) { throw new Error(`i=${i}的点${linePoints[i]}不是路径点`); } if (i === 0) { // 第一个点 if (linePoints.length > 4) { points = linePoints.slice(3); } else { console.error('不能移除:剩余点数不足'); } } else if (i === linePoints.length - 1) { // 最后一个点 if (linePoints.length > 4) { points = linePoints.slice(0, linePoints.length - 3); } else { console.error('无法移除:剩余点数不足'); } } else { // 中间点 points = linePoints.slice(0, i - 1); points.push(...linePoints.slice(i + 2)); } if (points) { graphic.linePoints = points; } } /** * 清除路径点(只留端点),适用于直线和贝塞尔曲线 * @param graphic * @param curve */ function clearWayPoint(graphic, curve) { const linePoints = graphic.linePoints; if (!curve) { if (linePoints.length > 2) { const points = linePoints.slice(0, 1); points.push(...linePoints.slice(-1)); graphic.linePoints = points; } } else { if (linePoints.length > 4) { const points = linePoints.slice(0, 2); points.push(...linePoints.slice(-2)); graphic.linePoints = points; } } } /** * 贝塞尔曲线编辑 */ class BezierCurveEditPlugin extends LineEditPlugin { static Name = 'bezier_curve_points_edit'; options; // 曲线控制点辅助线 auxiliaryLines = []; constructor(g, options) { super(g); this.options = Object.assign({}, { smooth: true }, options); this.name = BezierCurveEditPlugin.Name; this.initEditPoints(); } reset() { this.auxiliaryLines.splice(0, this.auxiliaryLines.length); super.reset(); } initEditPoints() { const cps = this.graphic.localToCanvasPoints(...this.linePoints); for (let i = 0; i < cps.length; i++) { const p = cps[i]; const dp = new DraggablePoint(p); const startOrEnd = i == 0 || i == cps.length - 1; const c = i % 3; if (c === 1) { // 前一路径点的控制点 dp.zIndex = 2; const fp = cps[i - 1]; const line = new Graphics(); this.drawAuxiliaryLine(line, fp, p); this.auxiliaryLines.push(line); } else if (c === 2) { // 后一路径点的控制点 dp.zIndex = 3; const np = cps[i + 1]; const line = new Graphics(); this.drawAuxiliaryLine(line, p, np); this.auxiliaryLines.push(line); } dp.on('transforming', (e) => { const tlp = this.graphic.canvasToLocalPoint(dp.position); const cp = this.linePoints[i]; cp.x = tlp.x; cp.y = tlp.y; if (this.options.smooth || this.options.symmetry) { if (c === 0 && !startOrEnd) { const shiftData = e.getData(); const fp = this.linePoints[i - 1]; const np = this.linePoints[i + 1]; fp.x = fp.x + shiftData.dx; fp.y = fp.y + shiftData.dy; np.x = np.x + shiftData.dx; np.y = np.y + shiftData.dy; } else if (c === 1 && i !== 1) { const bp = this.linePoints[i - 1]; const fp2 = this.linePoints[i - 2]; let mp; if (this.options.symmetry) { mp = calculateMirrorPoint(bp, cp); } else { const distance = distance2(bp, fp2); mp = calculateMirrorPoint(bp, cp, distance); } fp2.x = mp.x; fp2.y = mp.y; } else if (c === 2 && i !== cps.length - 2) { const bp = this.linePoints[i + 1]; const np2 = this.linePoints[i + 2]; let mp; if (this.options.symmetry) { mp = calculateMirrorPoint(bp, cp); } else { const distance = distance2(bp, np2); mp = calculateMirrorPoint(bp, cp, distance); } np2.x = mp.x; np2.y = mp.y; } } this.graphic.repaint(); }); if (this.options.onEditPointCreate) { this.options.onEditPointCreate(this.graphic, dp, i); } this.editedPoints.push(dp); if (this.auxiliaryLines.length > 0) { this.addChild(...this.auxiliaryLines); } } this.addChild(...this.editedPoints); } drawAuxiliaryLine(line, p1, p2) { line.clear(); if (this.options.auxiliaryLineColor) { line.lineStyle(1, new Color(this.options.auxiliaryLineColor)); } else { line.lineStyle(1, new Color('blue')); } line.moveTo(p1.x, p1.y); line.lineTo(p2.x, p2.y); } updateEditedPointsPosition() { const cps = this.graphic.localToCanvasPoints(...this.linePoints); if (cps.length === this.editedPoints.length) { for (let i = 0; i < cps.length; i++) { const cp = cps[i]; this.editedPoints[i].position.copyFrom(cp); const c = i % 3; const d = Math.floor(i / 3); if (c === 1 || c === 2) { const li = d * 2 + c - 1; const line = this.auxiliaryLines[li]; if (c === 1) { const fp = cps[i - 1]; this.drawAuxiliaryLine(line, fp, cp); } else { const np = cps[i + 1]; this.drawAuxiliaryLine(line, cp, np); } } } } } } /* eslint-disable @typescript-eslint/no-explicit-any */ class ShiftData { /** * 起始位置 */ startPosition; /** * 上一次终点位置 */ lastPosition; /** * 当前位置 */ currentPosition; constructor(startPosition, currentPosition, lastPosition) { this.startPosition = startPosition; this.lastPosition = lastPosition; this.currentPosition = currentPosition; } static new(startPosition, currentPosition, lastPosition) { return new ShiftData(startPosition, currentPosition, lastPosition); } get dx() { if (!this.lastPosition || !this.currentPosition) { throw new Error('错误的位移数据或阶段'); } return this.currentPosition.x - this.lastPosition.x; } get dy() { if (!this.lastPosition || !this.currentPosition) { throw new Error('错误的位移数据或阶段'); } return this.currentPosition.y - this.lastPosition.y; } get dsx() { if (!this.currentPosition) { throw new Error('错误的位移数据或阶段'); } return this.currentPosition.x - this.startPosition.x; } get dsy() { if (!this.currentPosition) { throw new Error('错误的位移数据或阶段'); } return this.currentPosition.y - this.startPosition.y; } } class ScaleData { start; current; last; constructor(start, current, last) { this.start = start; this.current = current; this.last = last; } static new(start, current, last) { return new ScaleData(start, current, last); } } /** * 图形平移事件 */ class GraphicTransformEvent { /** * 图形对象 */ target; type; data; constructor(target, type, data) { this.target = target; this.type = type; this.data = data; } getData() { return this.data; } static shift(target, data) { return new GraphicTransformEvent(target, 'shift', data); } static scale(target) { return new GraphicTransformEvent(target, 'scale', null); } static rotate(target) { return new GraphicTransformEvent(target, 'rotate', null); } static skew(target) { return new GraphicTransformEvent(target, 'skew', null); } isShift() { return this.type === 'shift'; } isRotate() { return this.type === 'rotate'; } isScale() { return this.type === 'scale'; } isSkew() { return this.type === 'skew'; } } class GraphicTransformPlugin extends InteractionPluginBase { static Name = '__graphic_transform_plugin'; /** * 可吸附位置列表 */ absorbablePositions; apContainer; static AbsorbablePosisiontsName = '__AbsorbablePosisionts'; constructor(app) { super(app, GraphicTransformPlugin.Name, InteractionPluginType.Other); this.apContainer = new Container(); this.apContainer.name = GraphicTransformPlugin.AbsorbablePosisiontsName; this.app.canvas.addAssistantAppend(this.apContainer); app.on('options-update', (options) => { if (options.absorbablePositions) { this.absorbablePositions = this.filterAbsorbablePositions(options.absorbablePositions); } }); } /** * 过滤重复的吸附位置 * @param positions * @returns */ filterAbsorbablePositions(positions) { const aps = []; for (let i = 0; i < positions.length; i++) { const ap1 = positions[i]; let ap = ap1; for (let j = positions.length - 1; j > i; j--) { const ap2 = positions[j]; if (ap.isOverlapping(ap2) && ap.compareTo(ap2) <= 0) { ap = null; break; } } if (ap != null) { aps.push(ap); } } return aps; } static new(app) { return new GraphicTransformPlugin(app); } bind() { this.app.on('drag_op_start', this.onDragStart, this); this.app.on('drag_op_move', this.onDragMove, this); this.app.on('drag_op_end', this.onDragEnd, this); this.app.on('graphicselectedchange', this.onGraphicSelectedChange, this); this.app.on('graphicchildselectedchange', this.onGraphicSelectedChange, this); } unbind() { this.app.off('drag_op_start', this.onDragStart, this); this.app.off('drag_op_move', this.onDragMove, this); this.app.off('drag_op_end', this.onDragEnd, this); this.app.off('graphicselectedchange', this.onGraphicSelectedChange, this); this.app.off('graphicchildselectedchange', this.onGraphicSelectedChange, this); } getDraggedTargets(e) { const targets = []; if (e.target.isGraphicChild() && e.target.selected && e.target.draggable) { const graphic = e.target.getGraphic(); // 图形子元素 recursiveChildren(graphic, (child) => { if (child.selected && child.draggable) { targets.push(child); } }); } else if ((e.target.isGraphic() || e.target.isGraphicChild()) && e.target.getGraphic()?.draggable) { // 图形对象 targets.push(...this.app.selectedGraphics); } else if (e.target.draggable) { targets.push(e.target); } return targets; } onDragStart(e) { if (!e.target.isCanvas() && e.isLeftButton) { const targets = this.getDraggedTargets(e); if (targets.length > 0) { targets.forEach((target) => { target.shiftStartPoint = target.position.clone(); target.emit('transformstart', GraphicTransformEvent.shift(target, ShiftData.new(target.shiftStartPoint))); }); // 显示吸附图形 if (this.absorbablePositions && this.absorbablePositions.length > 0) { this.apContainer.removeChildren(); this.apContainer.addChild(...this.absorbablePositions); } } } } onDragMove(e) { if (!e.target.isCanvas() && e.isLeftButton) { const targets = this.getDraggedTargets(e); if (targets.length > 0) { // 处理位移 targets.forEach((target) => { if (target.shiftStartPoint) { target.shiftLastPoint = target.position.clone(); const { dx, dy } = e.toTargetShiftLen(target.parent); target.position.set(target.shiftStartPoint.x + dx, target.shiftStartPoint.y + dy); } }); // 处理吸附 if (this.absorbablePositions) { for (let i = 0; i < this.absorbablePositions.length; i++) { const ap = this.absorbablePositions[i]; ap.tryAbsorb(...targets); } } // const start = new Date().getTime(); // 事件发布 targets.forEach((target) => { if (target.shiftStartPoint && target.shiftLastPoint) { target.emit('transforming', GraphicTransformEvent.shift(target, ShiftData.new(target.shiftStartPoint, target.position.clone(), target.shiftLastPoint))); } }); } } } onDragEnd(e) { if (!e.target.isCanvas() && e.isLeftButton) { const targets = this.getDraggedTargets(e); targets.forEach((target) => { if (target.shiftStartPoint) { target.emit('transformend', GraphicTransformEvent.shift(target, ShiftData.new(target.shiftStartPoint, target.position.clone()))); } target.shiftStartPoint = null; }); } this.clearCache(); } /** * 清理缓存 */ clearCache() { // 移除吸附图形 this.absorbablePositions = []; this.apContainer.removeChildren(); } onGraphicSelectedChange(g, selected) { let br = g.getAssistantAppend(BoundsGraphic.Name); if (!br) { // 绘制辅助包围框 br = new BoundsGraphic(g); } if (selected) { if (br) { br.redraw(); br.visible = true; } } else { if (br) { br.visible = false; } } if (g.scalable || g.rotatable) { // 缩放点 let sp = g.getAssistantAppend(TransformPoints.Name); if (!sp) { sp = new TransformPoints(g); } if (selected) { sp.update(); sp.visible = true; } else { sp.visible = false; } } } } /** * 缩放、旋转辅助 */ class TransformPoints extends Container { static Name = 'transformPoints'; static MinLength = 40; static LeftTopName = 'lt-scale-point'; static TopName = 't-scale-point'; static RightTopName = 'rt-scale-point'; static RightName = 'r-scale-point'; static RightBottomName = 'rb-scale-point'; static BottomName = 'b-scale-point'; static LeftBottomName = 'lb-scale-point'; static LeftName = 'l-scale-point'; static RotateName = 'rotate-point'; obj; ltScalePoint; ltLocal = new Point(); tScalePoint; tLocal = new Point(); tCanvas = new Point(); rtScalePoint; rtLocal = new Point(); rScalePoint; rLocal = new Point(); rbScalePoint; rbLocal = new Point(); bScalePoint; bLocal = new Point(); lbScalePoint; lbLocal = new Point(); lScalePoint; lLocal = new Point(); originScale = new Point(); scalePivot = new Point(); /** * 旋转拖拽点 */ rotatePoint; /** * 旋转中心坐标 */ rotatePivot; /** * 起始旋转坐标 */ rotateLastPoint; /** * 起始图形角度 */ startAngle = 0; /** * 当前角度信息文本辅助 */ angleAssistantText; /** * 旋转角度步长 */ angleStep = 1; /** * 修改旋转步长键盘监听 */ rotateAngleStepKeyListeners = []; constructor(obj) { super(); this.obj = obj; this.name = TransformPoints.Name; this.angleAssistantText = new VectorText('', { fill: AppConsts.assistantElementColor, }); this.angleAssistantText.setVectorFontSize(16); this.angleAssistantText.anchor.set(0.5); // 创建缩放拖拽点 this.ltScalePoint = new DraggablePoint(new Point()); this.ltScalePoint.name = TransformPoints.LeftTopName; this.addChild(this.ltScalePoint); this.tScalePoint = new DraggablePoint(new Point()); this.tScalePoint.name = TransformPoints.TopName; this.addChild(this.tScalePoint); this.rtScalePoint = new DraggablePoint(new Point()); this.rtScalePoint.name = TransformPoints.RightTopName; this.addChild(this.rtScalePoint); this.rScalePoint = new DraggablePoint(new Point()); this.rScalePoint.name = TransformPoints.RightName; this.addChild(this.rScalePoint); this.rbScalePoint = new DraggablePoint(new Point()); this.rbScalePoint.name = TransformPoints.RightBottomName; this.addChild(this.rbScalePoint); this.bScalePoint = new DraggablePoint(new Point()); this.bScalePoint.name = TransformPoints.BottomName; this.addChild(this.bScalePoint); this.lbScalePoint = new DraggablePoint(new Point()); this.lbScalePoint.name = TransformPoints.LeftBottomName; this.addChild(this.lbScalePoint); this.lScalePoint = new DraggablePoint(new Point()); this.lScalePoint.name = TransformPoints.LeftName; this.addChild(this.lScalePoint); this.obj.on('transformstart', this.onObjTransformStart, this); this.obj.on('transformend', this.onObjTransformEnd, this); if (this.obj.children && this.obj.children.length > 0) { recursiveChildren(this.obj, (child) => { child.on('transformstart', this.onObjTransformStart, this); child.on('transformend', this.onObjTransformEnd, this); }); } const pg = this.obj.getGraphic(); if (pg != null) { pg.on('transformstart', this.onObjTransformStart, this); pg.on('transformend', this.onObjTransformEnd, this); } this.obj.on('repaint', this.onGraphicRepaint, this); this.children.forEach((dp) => { dp.on('transformstart', this.onScaleDragStart, this); dp.on('transforming', this.onScaleDragMove, this); dp.on('transformend', this.onScaleDragEnd, this); }); // 创建旋转拖拽点 this.rotatePoint = new DraggablePoint(new Point()); this.addChild(this.rotatePoint); this.rotatePoint.on('transformstart', this.onRotateStart, this); this.rotatePoint.on('transforming', this.onRotateMove, this); this.rotatePoint.on('transformend', this.onRotateEnd, this); this.rotatePivot = new Point(); this.rotateLastPoint = new Point(); // 初始化旋转角度修改键盘监听器 for (let i = 1; i < 10; i++) { this.rotateAngleStepKeyListeners.push(KeyListener.create({ value: '' + i, onPress: () => { this.angleStep = i; }, })); } this.obj.addAssistantAppend(this); } onObjTransformStart() { this.visible = false; } onObjTransformEnd() { this.update(); this.visible = true; } onGraphicRepaint() { if (this.visible) { this.update(); } } /** * 旋转开始 * @param de */ onRotateStart(de) { this.hideAll(); const assistantPoint = this.obj.localToCanvasPoint(this.obj.pivot); this.rotatePivot.copyFrom(assistantPoint); this.rotateLastPoint.copyFrom(de.target.position); this.startAngle = this.obj.angle; const app = this.obj.getGraphicApp(); this.rotateAngleStepKeyListeners.forEach((listener) => app.addKeyboardListener(listener)); this.obj.emit('transformstart', GraphicTransformEvent.rotate(this.obj)); // app.emit('transformstart', app.selectedGraphics); this.obj.getCanvas().addAssistantAppends(this.angleAssistantText); this.updateAngleAssistantText(de); } updateAngleAssistantText(de) { this.angleAssistantText.text = this.obj.angle + '°'; let cursorPoint = de.data?.startPosition; if (de.data?.currentPosition) { cursorPoint = de.data?.currentPosition; } if (cursorPoint) { this.angleAssistantText.position.x = cursorPoint.x; this.angleAssistantText.position.y = cursorPoint.y - 10; } } /** * 旋转移动 * @param de */ onRotateMove(de) { // 旋转角度计算逻辑:取锚点y负方向一点作为旋转点,求旋转点和锚点所形成的直线与x轴角度,此角度+90°即为最终旋转角度,再将旋转角度限制到(-180,180]之间 let angle = angleToAxisx(this.rotatePivot, de.target.position); angle = Math.floor(angle / this.angleStep) * this.angleStep; const parentAngle = this.obj.parent.worldAngle; angle = (angle + 90 - parentAngle) % 360; if (angle > 180) { angle = angle - 360; } this.obj.angle = angle; this.updateAngleAssistantText(de); // this.obj.emit('rotatemove', this.obj); } /** * 旋转结束 * @param de */ onRotateEnd() { this.showAll(); this.obj.getCanvas().removeAssistantAppends(this.angleAssistantText); this.rotateAngleStepKeyListeners.forEach((listener) => this.obj.getGraphicApp().removeKeyboardListener(listener)); this.obj.emit('transformend', GraphicTransformEvent.rotate(this.obj)); } /** * 缩放开始 */ onScaleDragStart() { this.hideAll(); const points = convertRectangleToPolygonPoints(this.obj.getLocalBounds()); const p0 = points[0]; const p1 = points[1]; const p2 = points[2]; const p3 = points[3]; this.scalePivot = this.obj.pivot.clone(); this.ltLocal.copyFrom(p0); this.tCanvas.copyFrom(this.obj.localToCanvasPoint(calculateLineMidpoint(p0, p1))); this.tLocal.copyFrom(calculateLineMidpoint(p0, p1)); this.rtLocal.copyFrom(p1); this.rLocal.copyFrom(calculateLineMidpoint(p1, p2)); this.rbLocal.copyFrom(p2); this.bLocal.copyFrom(calculateLineMidpoint(p2, p3)); this.lbLocal.copyFrom(p3); this.lLocal.copyFrom(calculateLineMidpoint(p0, p3)); this.originScale = this.obj.scale.clone(); this.obj.emit('transformstart', GraphicTransformEvent.scale(this.obj)); } onScaleDragMove(e) { // 缩放计算逻辑:共8个方向缩放点,根据所拖拽的方向: // 1,计算缩放为1时的此点在拖拽开始时的位置到锚点x、y距离, // 2,计算拖拽点的当前位置到锚点的x、y方向距离, // PS:以上两个计算都是在local(也就是图形对象本地)坐标系, // 用当前距离除以原始距离即为缩放比例 const defaultScale = new Point(1, 1); let originWidth = 0; let originHeight = 0; let width = 0; let height = 0; this.obj.scale.copyFrom(defaultScale); const point = this.obj.toLocal(e.target.parent.localToScreenPoint(e.target.position)); if (e.target === this.ltScalePoint) { // 左上角 originWidth = Math.abs(this.ltLocal.x - this.scalePivot.x); originHeight = Math.abs(this.ltLocal.y - this.scalePivot.y); width = Math.abs(point.x - this.scalePivot.x); height = Math.abs(point.y - this.scalePivot.y); } else if (e.target == this.tScalePoint) { // 上 originHeight = Math.abs(this.tLocal.y - this.scalePivot.y); height = Math.abs(point.y - this.scalePivot.y); } else if (e.target == this.rtScalePoint) { // 右上 originWidth = Math.abs(this.rtLocal.x - this.scalePivot.x); originHeight = Math.abs(this.rtLocal.y - this.scalePivot.y); width = Math.abs(point.x - this.scalePivot.x); height = Math.abs(point.y - this.scalePivot.y); } else if (e.target == this.rScalePoint) { // 右 originWidth = Math.abs(this.rLocal.x - this.scalePivot.x); width = Math.abs(point.x - this.scalePivot.x); } else if (e.target == this.rbScalePoint) { // 右下 originWidth = Math.abs(this.rbLocal.x - this.scalePivot.x); originHeight = Math.abs(this.rbLocal.y - this.scalePivot.y); width = Math.abs(point.x - this.scalePivot.x); height = Math.abs(point.y - this.scalePivot.y); } else if (e.target == this.bScalePoint) { // 下 originHeight = Math.abs(this.bLocal.y - this.scalePivot.y); height = Math.abs(point.y - this.scalePivot.y); } else if (e.target == this.lbScalePoint) { // 左下 originWidth = Math.abs(this.lbLocal.x - this.scalePivot.x); originHeight = Math.abs(this.lbLocal.y - this.scalePivot.y); width = Math.abs(point.x - this.scalePivot.x); height = Math.abs(point.y - this.scalePivot.y); } else { // 左 originWidth = Math.abs(this.lLocal.x - this.scalePivot.x); width = Math.abs(point.x - this.scalePivot.x); } // 计算缩放比例,并根据是否保持纵横比两种情况进行缩放处理 const sx = originWidth == 0 ? this.originScale.x : width / originWidth; const sy = originHeight == 0 ? this.originScale.y : height / originHeight; if (this.obj.keepAspectRatio) { let max = Math.max(sx, sy); if (originWidth == 0) { max = sy; } else if (originHeight == 0) { max = sx; } this.obj.scale.set(max, max); } else { this.obj.scale.x = sx; this.obj.scale.y = sy; } } onScaleDragEnd() { this.showAll(); this.obj.emit('transformend', GraphicTransformEvent.scale(this.obj)); } hideOthers(dg) { this.children.forEach((child) => { if (child.name !== dg.name) { child.visible = false; } }); } hideAll() { this.children.forEach((child) => (child.visible = false)); } showAll() { this.update(); this.children.forEach((child) => (child.visible = true)); } getObjBounds() { const points = this.obj.localBoundsToCanvasPoints(); const p0 = points[0]; const p1 = points[1]; const p3 = points[3]; const width = distance(p0.x, p0.y, p1.x, p1.y); const height = distance(p0.x, p0.y, p3.x, p3.y); return { width, height }; } /** * 更新位置和cursor * @returns */ update() { if (this.obj.scalable) { this.updateScalePoints(); } if (this.obj.rotatable) { this.updateRotatePoint(); } } updateRotatePoint() { const rect = this.obj.getLocalBounds(); const lp = this.obj.pivot.clone(); const dy = 10 / this.obj.scale.y; lp.y = rect.y - dy; const p = this.obj.localToCanvasPoint(lp); this.rotatePoint.position.copyFrom(p); } updateScalePoints() { const points = this.obj.localBoundsToCanvasPoints(); this.ltScalePoint.position.copyFrom(points[0]); this.tScalePoint.position.copyFrom(calculateLineMidpoint(points[0], points[1])); this.rtScalePoint.position.copyFrom(points[1]); this.rScalePoint.position.copyFrom(calculateLineMidpoint(points[1], points[2])); this.rbScalePoint.position.copyFrom(points[2]); this.bScalePoint.position.copyFrom(calculateLineMidpoint(points[2], points[3])); this.lbScalePoint.position.copyFrom(points[3]); this.lScalePoint.position.copyFrom(calculateLineMidpoint(points[3], points[0])); const angle = this.obj.worldAngle; const angle360 = (360 + angle) % 360; if ((angle >= -22.5 && angle <= 22.5) || (angle360 >= 180 - 22.5 && angle360 <= 180 + 22.5)) { this.ltScalePoint.cursor = 'nw-resize'; this.tScalePoint.cursor = 'n-resize'; this.rtScalePoint.cursor = 'ne-resize'; this.rScalePoint.cursor = 'e-resize'; this.rbScalePoint.cursor = 'se-resize'; this.bScalePoint.cursor = 's-resize'; this.lbScalePoint.cursor = 'sw-resize'; this.lScalePoint.cursor = 'w-resize'; } else if ((angle >= 22.5 && angle <= 67.5) || (angle360 >= 180 + 22.5 && angle360 <= 180 + 67.5)) { this.ltScalePoint.cursor = 'n-resize'; this.tScalePoint.cursor = 'ne-resize'; this.rtScalePoint.cursor = 'e-resize'; this.rScalePoint.cursor = 'se-resize'; this.rbScalePoint.cursor = 's-resize'; this.bScalePoint.cursor = 'sw-resize'; this.lbScalePoint.cursor = 'w-resize'; this.lScalePoint.cursor = 'nw-resize'; } else if ((angle >= 67.5 && angle < 112.5) || (angle360 >= 180 + 67.5 && angle360 <= 180 + 112.5)) { this.ltScalePoint.cursor = 'ne-resize'; this.tScalePoint.cursor = 'e-resize'; this.rtScalePoint.cursor = 'se-resize'; this.rScalePoint.cursor = 's-resize'; this.rbScalePoint.cursor = 'sw-resize'; this.bScalePoint.cursor = 'w-resize'; this.lbScalePoint.cursor = 'nw-resize'; this.lScalePoint.cursor = 'n-resize'; } else { this.ltScalePoint.cursor = 'e-resize'; this.tScalePoint.cursor = 'se-resize'; this.rtScalePoint.cursor = 's-resize'; this.rScalePoint.cursor = 'sw-resize'; this.rbScalePoint.cursor = 'w-resize'; this.bScalePoint.cursor = 'nw-resize'; this.lbScalePoint.cursor = 'n-resize'; this.lScalePoint.cursor = 'ne-resize'; } } } /** * 包围盒矩形图形,现使用外边框转画布多边形实现 */ class BoundsGraphic extends Graphics { static Name = '_BoundsRect'; static BoundsLineStyle = { width: 1, color: '#1976d2', alpha: 1, }; obj; debouncedRedraw; constructor(graphic) { super(); this.obj = graphic; this.name = BoundsGraphic.Name; this.visible = false; this.debouncedRedraw = debounce(this.doRedraw, 50); this.obj.on('transformstart', this.onObjTransformStart, this); this.obj.on('transformend', this.onObjTransformEnd, this); if (this.obj.children && this.obj.children.length > 0) { recursiveChildren(this.obj, (child) => { child.on('transformstart', this.onObjTransformStart, this); child.on('transformend', this.onObjTransformEnd, this); }); } const pg = this.obj.getGraphic(); if (pg != null) { pg.on('transformstart', this.onObjTransformStart, this); pg.on('transformend', this.onObjTransformEnd, this); } this.obj.on('repaint', this.onGraphicRepaint, this); graphic.addAssistantAppend(this); } onObjTransformStart() { this.visible = false; } onObjTransformEnd() { this.redraw(); this.visible = true; } onGraphicRepaint() { if (this.visible) { this.redraw(); } } destroy(options) { if (this.obj.isGraphic()) { this.obj.off('repaint', this.onGraphicRepaint, this); } super.destroy(options); } redraw() { this.debouncedRedraw(this); } doRedraw() { const visible = this.visible; this.visible = false; // 屏蔽包围框本身 const bounds = new Polygon(this.obj.localBoundsToCanvasPoints()); this.clear().lineStyle(BoundsGraphic.BoundsLineStyle).drawShape(bounds); this.visible = visible; } } /** * 操作 */ class JlOperation { type; // 操作类型/名称 app; obj; // 操作对象 data; // 操作数据 description = ''; // 操作描述 constructor(app, type) { this.app = app; this.type = type; } undo1() { const updates = this.undo(); if (updates) { this.app.updateSelected(...updates); } } redo1() { const updates = this.redo(); if (updates) { this.app.updateSelected(...updates); } } } /** * 操作记录 */ class OperationRecord { undoStack = []; redoStack = []; maxLen; constructor(maxLen = 100) { this.maxLen = maxLen; } get hasUndo() { return this.undoStack.length > 0; } get hasRedo() { return this.redoStack.length > 0; } setMaxLen(v) { this.maxLen = v; const len = this.undoStack.length; if (len > v) { const removeCount = len - v; this.undoStack.splice(0, removeCount); } } /** * 记录 * @param op */ record(op) { if (this.undoStack.length >= this.maxLen) { this.undoStack.shift(); } this.undoStack.push(op); this.redoStack.splice(0, this.redoStack.length); } /** * 撤销 */ undo() { const op = this.undoStack.pop(); if (op) { op.undo1(); this.redoStack.push(op); } } /** * 重做 */ redo() { const op = this.redoStack.pop(); if (op) { op.redo1(); this.undoStack.push(op); } } } /** * 更新画布操作 */ class UpdateCanvasOperation extends JlOperation { obj; old; data; description = ''; constructor(app, obj, old, data) { super(app, 'update-canvas'); this.app = app; this.obj = obj; this.old = old; this.data = data; } undo() { this.obj.update(this.old); return []; } redo() { this.obj.update(this.data); return []; } } /** * 创建图形操作 */ class GraphicCreateOperation extends JlOperation { obj; description = ''; constructor(app, obj) { super(app, 'graphic-create'); this.app = app; this.obj = obj; } undo() { this.app.deleteGraphics(...this.obj); } redo() { this.app.addGraphics(...this.obj); return this.obj; } } /** * 删除图形操作 */ class GraphicDeleteOperation extends JlOperation { obj; constructor(app, obj) { super(app, 'graphic-delete'); this.app = app; this.obj = obj; } undo() { this.app.addGraphics(...this.obj); return this.obj; } redo() { this.app.deleteGraphics(...this.obj); } } class GraphicDataUpdateOperation extends JlOperation { obj; oldData; newData; constructor(app, obj, oldData, newData) { super(app, 'graphic-drag'); this.obj = [...obj]; this.oldData = oldData; this.newData = newData; } undo() { for (let i = 0; i < this.obj.length; i++) { const g = this.obj[i]; // g.exitChildEdit(); g.updateData(this.oldData[i]); } return this.obj; } redo() { for (let i = 0; i < this.obj.length; i++) { const g = this.obj[i]; // g.exitChildEdit(); g.updateData(this.newData[i]); } return this.obj; } } /** * 图形关系数据 */ class GraphicRelationParam { g; param; constructor(g, param = null) { this.g = g; this.param = param; } isTheGraphic(g) { return this.g.id === g.id; } getGraphic() { return this.g; } getParam() { return this.param; } equals(other) { return this.isTheGraphic(other.g) && this.param === other.param; } } /** * 图形关系 */ class GraphicRelation { rp1; rp2; constructor(rp1, rp2) { this.rp1 = rp1; this.rp2 = rp2; } contains(g) { return this.rp1.isTheGraphic(g) || this.rp2.isTheGraphic(g); } /** * 获取给定图形的关系参数 * @param g * @returns */ getRelationParam(g) { if (!this.contains(g)) { throw new Error(`图形关系${this.rp1.g.id}-${this.rp2.g.id}中不包含给定图形${g.id}`); } if (this.rp1.isTheGraphic(g)) { return this.rp1; } else { return this.rp2; } } /** * 获取关联的另一个图形的关系参数 * @param g * @returns */ getOtherRelationParam(g) { if (!this.contains(g)) { throw new Error(`图形关系${this.rp1.g.id}-${this.rp2.g.id}中不包含给定图形${g.id}`); } if (this.rp1.isTheGraphic(g)) { return this.rp2; } else { return this.rp1; } } /** * 获取关联的另一个图形对象 * @param g * @returns graphic */ getOtherGraphic(g) { return this.getOtherRelationParam(g).g; } equals(orp1, orp2) { if (this.rp1.isTheGraphic(orp1.g)) { return this.rp1.equals(orp1) && this.rp2.equals(orp2); } else if (this.rp1.isTheGraphic(orp2.g)) { return this.rp1.equals(orp2) && this.rp2.equals(orp1); } return false; } isEqualOther(other) { return this.equals(other.rp1, other.rp2); } } /** * 图形关系管理 */ class RelationManage { relations = []; isContainsRelation(rp1, rp2) { const relation = this.relations.find((relation) => relation.equals(rp1, rp2)); return !!relation; } addRelation(rp1, rp2) { if (!(rp1 instanceof GraphicRelationParam)) { rp1 = new GraphicRelationParam(rp1); } if (!(rp2 instanceof GraphicRelationParam)) { rp2 = new GraphicRelationParam(rp2); } if (!this.isContainsRelation(rp1, rp2)) { const relation = new GraphicRelation(rp1, rp2); this.relations.push(relation); } } /** * 获取图形的所有关系 * @param g * @returns */ getRelationsOfGraphic(g) { return this.relations.filter((rl) => rl.contains(g)); } /** * 获取指定图形的指定关系图形类型的所有关系 * @param g 指定图形 * @param type 关联图形的类型 * @returns */ getRelationsOfGraphicAndOtherType(g, type) { return this.relations.filter((rl) => rl.contains(g) && rl.getOtherGraphic(g).type === type); } /** * 删除关系 * @param relation */ deleteRelation(relation) { const index = this.relations.findIndex((rl) => rl.isEqualOther(relation)); if (index >= 0) { this.relations.splice(index, 1); } } /** * 删除指定图形的所有关系 * @param g */ deleteRelationOfGraphic(g) { const relations = this.getRelationsOfGraphic(g); relations.forEach((rl) => this.deleteRelation(rl)); } /** * 删除指定图形的所有关系 * @param g */ deleteRelationOfGraphicAndOtherType(g, type) { const relations = this.getRelationsOfGraphicAndOtherType(g, type); relations.forEach((rl) => this.deleteRelation(rl)); } /** * 清空 */ clear() { this.relations.splice(0, this.relations.length); } } /** * 图形存储 */ class GraphicStore { store; relationManage; constructor() { this.store = new Map(); this.relationManage = new RelationManage(); } /** * 获取所有图形对象 */ getAllGraphics() { return [...this.store.values()]; } queryById(id) { let nid = id; if (typeof id === 'string') { nid = parseInt(id); } const graphic = this.store.get(nid); if (!graphic) throw Error(`未找到id为 [${nid}] 的图形.`); return this.store.get(nid); } queryByIdAmbiguous(id) { const nid = id + ''; const list = []; this.store.forEach((g) => { if ((g.id + '').search(nid) >= 0) { list.push(g); } }); return list; } queryByType(type) { const list = []; this.store.forEach((g) => { if (g.type === type) { list.push(g); } }); return list; } queryByCode(code) { const list = []; this.store.forEach((g) => { if (g.code === code) { list.push(g); } }); return list; } queryByCodeAmbiguous(code) { const list = []; this.store.forEach((g) => { if (g.code.search(code) >= 0) { list.push(g); } }); return list; } queryByIdOrCode(s) { const list = []; this.store.forEach((g) => { if (g.isIdOrCode(s)) { list.push(g); } }); return list; } queryByIdOrCodeAndType(s, type) { const list = []; this.store.forEach((g) => { if (g.isIdOrCode(s) && g.type === type) { list.push(g); } }); return list; } queryByCodeAndType(code, type) { for (const item of this.store.values()) { if (item.code === code && item.type === type) { return item; } } } queryByCodeAndTypeAmbiguous(code, type) { const list = []; this.store.forEach((g) => { if (g.type === type && g.code.search(code) >= 0) { list.push(g); } }); return list; } /** * 存储图形对象 * @param graphics 要存储的图形 */ storeGraphics(graphic) { if (!graphic.id || graphic.id === 0) { throw new Error(`存储图形对象异常: id为空, ${graphic}`); } if (this.store.has(graphic.id)) { // 已经存在 const exist = this.store.get(graphic.id); console.error(`已经存在id=${graphic.id}的设备${exist}`); return false; } else { this.store.set(graphic.id, graphic); graphic.queryStore = this; graphic.relationManage = this.relationManage; return true; } } /** * 删除图形 * @param graph 要删除的图形 */ deleteGraphics(graphic) { const id = graphic.id; const remove = this.store.get(id); if (remove) { this.store.delete(id); // 删除图形关系 this.relationManage.deleteRelationOfGraphic(remove); } return remove; } /** * 清空 */ clear() { this.relationManage.clear(); this.store.clear(); } checkIdExist(id) { return this.store.has(id); } } //基础图形对象扩展 DisplayObject.prototype._selectable = false; //是否可选中 DisplayObject.prototype._selected = false; DisplayObject.prototype._childEdit = false; DisplayObject.prototype._transformSave = false; DisplayObject.prototype._assistantAppendMap = null; DisplayObject.prototype._draggable = false; DisplayObject.prototype._shiftStartPoint = null; DisplayObject.prototype._shiftLastPoint = null; DisplayObject.prototype._scalable = false; DisplayObject.prototype._keepAspectRatio = true; DisplayObject.prototype._rotatable = false; Object.defineProperties(DisplayObject.prototype, { assistantAppendMap: { get() { if (this._assistantAppendMap == null) { this._assistantAppendMap = new Map(); } return this._assistantAppendMap; }, }, selectable: { get() { return this._selectable; }, set(value) { this._selectable = value; }, }, selected: { get() { return this._selected; }, set(v) { this._selected = v; }, }, childEdit: { get() { return this._childEdit; }, set(v) { this._childEdit = v; }, }, transformSave: { get() { return this._transformSave; }, set(v) { this._transformSave = v; }, }, draggable: { get() { return this._draggable; }, set(v) { this._draggable = v; }, }, shiftStartPoint: { get() { return this._shiftStartPoint; }, set(v) { this._shiftStartPoint = v; }, }, shiftLastPoint: { get() { return this._shiftLastPoint; }, set(v) { this._shiftLastPoint = v; }, }, scalable: { get() { return this._scalable; }, set(v) { this._scalable = v; }, }, keepAspectRatio: { get() { return this._keepAspectRatio; }, set(v) { this._keepAspectRatio = v; }, }, rotatable: { get() { return this._rotatable; }, set(v) { this._rotatable = v; }, }, worldAngle: { get() { let angle = this.angle; let parent = this.parent; while (parent != undefined && parent != null) { angle += parent.angle; parent = parent.parent; } angle = angle % 360; if (angle > 180) { angle = angle - 360; } return angle; }, }, }); DisplayObject.prototype.getAllParentScaled = function getAllParentScaled() { const scaled = new Point(1, 1); recursiveParents(this, (parent) => { scaled.x *= parent.scale.x; scaled.y *= parent.scale.y; }); return scaled; }; DisplayObject.prototype.getPositionOnCanvas = function getPositionOnCanvas() { if (this.parent.isCanvas()) { return this.position; } else { return this.parent.localToCanvasPoint(this.position); } }; DisplayObject.prototype.updatePositionByCanvasPosition = function updatePositionByCanvasPosition(p) { if (this.parent.isCanvas()) { this.position.copyFrom(p); } else { const localPosition = this.parent.canvasToLocalPoint(p); this.position.copyFrom(localPosition); } }; DisplayObject.prototype.saveTransform = function saveTransform() { return GraphicTransform.fromObject(this); }; DisplayObject.prototype.loadTransform = function loadTransform(transfrom) { this.position.copyFrom(transfrom.position); this.scale.copyFrom(transfrom.scale); this.rotation = transfrom.rotation; this.skew.copyFrom(transfrom.skew); }; DisplayObject.prototype.isChild = function isChild(obj) { return recursiveFindChild(this, (child) => child == obj) != null; }; DisplayObject.prototype.isParent = function isParent(obj) { return recursiveFindParent(this, (parent) => parent == obj) != null; }; DisplayObject.prototype.isAssistantAppend = function isAssistantAppend() { return (recursiveFindParent(this, (parent) => { return parent.name === AppConsts.AssistantAppendsName; }) != null); }; DisplayObject.prototype.addAssistantAppend = function addAssistantAppend(...appends) { appends.forEach((append) => { if (append.name == null || append.name.trim() == '') { throw new Error('辅助附加name不能为空'); } this.assistantAppendMap.set(append.name, append); this.getCanvas().addAssistantAppends(append); }); }; DisplayObject.prototype.getAssistantAppend = function getAssistantAppend(name) { return this.assistantAppendMap.get(name); }; DisplayObject.prototype.removeAssistantAppend = function removeAssistantAppend(...appends) { appends.forEach((append) => { if (append.name) { this.removeAssistantAppendByName(append.name); } }); }; DisplayObject.prototype.removeAssistantAppendByName = function removeAssistantAppendByName(...names) { names.forEach((name) => { const append = this.getAssistantAppend(name); if (append) { this.getCanvas().removeAssistantAppends(append); this.assistantAppendMap.delete(name); append.destroy(); } }); }; DisplayObject.prototype.removeAllAssistantAppend = function removeAllAssistantAppend() { if (this._assistantAppendMap != null) { this.assistantAppendMap.forEach((append) => { append.getCanvas().removeAssistantAppends(append); }); this.assistantAppendMap.clear(); } }; DisplayObject.prototype.isGraphic = function isGraphic() { return Object.hasOwn(this, '__JlGraphic'); }; DisplayObject.prototype.getGraphic = function getGraphic() { let graphic = this; while (graphic && !Object.hasOwn(graphic, '__JlGraphic')) { graphic = graphic.parent; } if (graphic) { return graphic; } return null; }; DisplayObject.prototype.isGraphicChild = function isGraphicChild() { const g = this.getGraphic(); return g != null && !this.isAssistantAppend() && g.isChild(this); }; DisplayObject.prototype.onAddToCanvas = function onAddToCanvas(_canvas) { }; DisplayObject.prototype.onRemoveFromCanvas = function onRemoveFromCanvas(_canvas) { }; DisplayObject.prototype.isInCanvas = function isInCanvas() { let graphic = this; while (graphic && !Object.hasOwn(graphic, '__JlCanvas')) { graphic = graphic.parent; } if (graphic) { return true; } return false; }; DisplayObject.prototype.getCanvas = function getCanvas() { let graphic = this; while (graphic && !Object.hasOwn(graphic, '__JlCanvas')) { graphic = graphic.parent; } if (graphic) { return graphic; } throw new Error(`图形${this.name}不在画布中`); }; DisplayObject.prototype.isCanvas = function isCanvas() { return Object.hasOwn(this, '__JlCanvas'); }; DisplayObject.prototype.getViewport = function getViewport() { const canvas = this.getCanvas(); return canvas.parent; }; DisplayObject.prototype.getGraphicApp = function getGraphicApp() { const canvas = this.getCanvas(); return canvas.scene.app; }; DisplayObject.prototype.localToCanvasPoint = function localToCanvasPoint(p) { return this.getViewport().toWorld(this.toGlobal(p)); }; DisplayObject.prototype.localToCanvasPoints = function localToCanvasPoints(...points) { return points.map((p) => this.localToCanvasPoint(p)); }; DisplayObject.prototype.canvasToLocalPoint = function canvasToLocalPoint(p) { return this.toLocal(this.getViewport().toScreen(p)); }; DisplayObject.prototype.canvasToLocalPoints = function canvasToLocalPoints(...points) { return points.map((p) => this.canvasToLocalPoint(p)); }; DisplayObject.prototype.localToScreenPoint = function localToScreenPoint(p) { return this.toGlobal(p); }; DisplayObject.prototype.localToScreenPoints = function localToScreenPoints(...points) { return points.map((p) => this.toGlobal(p)); }; DisplayObject.prototype.screenToLocalPoint = function screenToLocalPoint(p) { return this.toLocal(p); }; DisplayObject.prototype.screenToLocalPoints = function screenToLocalPoints(...points) { return points.map((p) => this.toLocal(p)); }; DisplayObject.prototype.localBoundsToCanvasPoints = function localBoundsToCanvasPoints() { const rect = this.getLocalBounds(); const pps = convertRectangleToPolygonPoints(rect); return this.localToCanvasPoints(...pps); }; // 扩展pixijs图形对象,添加自定义绘制贝塞尔曲线,可自定义分段数 Graphics.prototype.drawBezierCurve = function drawBezierCurve(p1, p2, cp1, cp2, segmentsCount) { const fromX = p1.x; const fromY = p1.y; const n = segmentsCount; let dt = 0; let dt2 = 0; let dt3 = 0; let t2 = 0; let t3 = 0; const cpX = cp1.x; const cpY = cp1.y; const cpX2 = cp2.x; const cpY2 = cp2.y; const toX = p2.x; const toY = p2.y; this.moveTo(p1.x, p1.y); for (let i = 1, j = 0; i <= n; ++i) { j = i / n; dt = 1 - j; dt2 = dt * dt; dt3 = dt2 * dt; t2 = j * j; t3 = t2 * j; const px = dt3 * fromX + 3 * dt2 * j * cpX + 3 * dt * t2 * cpX2 + t3 * toX; const py = dt3 * fromY + 3 * dt2 * j * cpY + 3 * dt * t2 * cpY2 + t3 * toY; this.lineTo(px, py); } return this; }; /** * 图形变换数据 */ class GraphicTransform { position; scale; rotation; skew; constructor(position, scale, rotation, skew) { this.position = position; this.scale = scale; this.rotation = rotation; this.skew = skew; } static default() { return new GraphicTransform(new Point(0, 0), new Point(1, 1), 0, new Point(0, 0)); } static fromObject(obj) { return new GraphicTransform(obj.position.clone(), obj.scale.clone(), obj.rotation, obj.skew.clone()); } static from(transform) { if (transform) { return new GraphicTransform(new Point(transform.position.x, transform.position.y), new Point(transform.scale.x, transform.scale.y), transform.rotation, new Point(transform.skew.x, transform.skew.y)); } return GraphicTransform.default(); } } /** * 图形子元素变换 */ class ChildTransform { name; transform; constructor(name, transform) { this.name = name; this.transform = transform; } static fromChild(child) { if (child.name == null || child.name == undefined || child.name.trim() == '') { throw new Error(`图形type=${child.getGraphic()?.type}的子元素${child}name为空,但设置为需要保存变换数据`); } return new ChildTransform(child.name, GraphicTransform.fromObject(child)); } static from(ct) { return new ChildTransform(ct.name, GraphicTransform.from(ct.transform)); } } class GraphicAnimation { options; _running; /** * 倍速 */ _xSpeed; constructor(options) { this.options = options; this._running = false; this._xSpeed = 1; } static init(options) { return new GraphicAnimation(options); } pause() { this._running = false; return this; } resume() { this._running = true; return this; } get name() { return this.options.name; } get running() { return this._running; } get xSpeed() { return this._xSpeed; } set xSpeed(v) { this._xSpeed = v; } run(dt) { if (this.options.run) { this.options.run(dt * this.xSpeed); } return this; } } /** * 图形对象基类 */ class JlGraphic extends Container { __JlGraphic = true; type; // 图形类型 _id = 0; // 图形的唯一标识,不具有业务意义,唯一,不可重复,可用做图形数据关联。 _code = ''; // 业务编号/名称,用于标识图形对象,具有业务意义 _datas; // 图形数据 _states; // 图形状态数据 _relationManage; // 图形关系管理 _queryStore; // 图形对象查询仓库 constructor(type) { super(); this.type = type; this.draggable = false; this.filters; } /** * 添加图形动画,只有在画布上才能添加 * @param animation */ addAnimation(animation) { const app = this.getGraphicApp(); app.animationManager.registerAnimation(this, animation); } removeAnimation(name) { const app = this.getGraphicApp(); app.animationManager.unregisterAnimation(this, name); } animation(name) { const app = this.getGraphicApp(); return app.animationManager.animation(this, name); } removeAllAnimation() { const app = this.getGraphicApp(); app.animationManager.unregisterGraphicAnimations(this); } /** * 更新选中状态 * @param selected * @returns 是否更新 */ updateSelected(selected) { if (this.selected !== selected) { this.selected = selected; this.fireSelected(); return true; } return false; } invertSelected() { this.selected = !this.selected; this.fireSelected(); } fireSelected() { if (this.selected) { this.emit('selected', this); } else { this.exitChildEdit(); this.removeAllChildSelected(); this.emit('unselected', this); } const app = this.getGraphicApp(); if (app) { app.emit('graphicselectedchange', this, this.selected); } } hasSelectedChilds() { return (recursiveFindChild(this, (child) => { if (child.selected) { return true; } return false; }) != null); } setChildSelected(child) { if (child.isGraphicChild() && child.selectable) { this.removeAllChildSelected(); child.selected = true; this.fireChildSelected(child); } return false; } invertChildSelected(child) { if (child.isGraphicChild() && child.selectable) { child.selected = !child.selected; this.fireChildSelected(child); } return false; } removeAllChildSelected() { recursiveChildren(this, (child) => { if (child.selected) { child.selected = false; this.fireChildSelected(child); } }); } fireChildSelected(child) { const selected = child.selected; if (selected) { this.emit('childselected', child); } else { this.emit('childunselected', child); } const app = this.getGraphicApp(); if (app) { app.emit('graphicchildselectedchange', child, selected); } } exitChildEdit() { this.childEdit = false; this.removeAllChildSelected(); } /** * 是否此对象id/code */ isIdOrCode(s) { return this.id === s || this.code === s; } /** * 获取图形id,如果图形数据对象存在,则返回图形数据对象id */ get id() { if (this._datas) { return this._datas.id; } return this._id; } /** * 设置图形id,如果图形数据存在,则同时设置图形数据id */ set id(v) { this._id = v; if (this._datas) { this._datas.id = v; } } /** * 获取图形业务code,如果业务code在图形数据或图形状态中,则需要重写此方法 */ get code() { return this._code; } /** * 设置图形业务code,如果业务code在图形数据或图形状态中,则需要重写此方法 */ set code(v) { this._code = v; } getDatas() { if (this._datas) { return this._datas; } throw new Error(`id=${this.id},type=${this.type}的图形没有数据`); } getStates() { if (this._states) { return this._states; } throw new Error(`id=${this.id},type=${this.type}的的图形没有状态`); } get queryStore() { if (this._queryStore) { return this._queryStore; } throw new Error(`type=${this.type}的图形没有QueryStore`); } set queryStore(v) { this._queryStore = v; } get relationManage() { if (this._relationManage) { return this._relationManage; } throw new Error(`type=${this.type}的图形没有关系管理`); } set relationManage(v) { this._relationManage = v; } /** * 构建图形关系 * @param g */ buildRelation() { } /** * 从数据加载恢复图形关系 */ loadRelations() { } /** * 获取当前图形的所有图形关系 * @returns */ getAllRelations() { return this.relationManage.getRelationsOfGraphic(this); } /** * 获取当前图形的所有指定类型图形关系 * @param type * @returns */ queryRelationByType(type) { return this.relationManage.getRelationsOfGraphicAndOtherType(this, type); } /** * 删除当前图形关联的指定类型的关系 * @param type */ deleteRelationByType(type) { this.relationManage.deleteRelationOfGraphicAndOtherType(this, type); } /** * 构建并保存关系数据到datas中 */ saveRelations() { } /** * 保存数据,复制,非原始数据 * @returns */ saveData() { this.getDatas().graphicType = this.type; this.getDatas().transform = GraphicTransform.fromObject(this); this.getDatas().childTransforms = this.buildChildTransforms(); this.saveRelations(); return this.getDatas().clone(); } /** * 构建子元素变换列表 * @returns */ buildChildTransforms() { const childTransforms = []; recursiveChildren(this, (child) => { if (child.transformSave) { childTransforms.push(ChildTransform.fromChild(child)); } }); return childTransforms; } /** * 加载数据 * @param data */ loadData(data) { if (data.graphicType !== this.type) { throw new Error(`不同的图形类型,请检查数据是否正常: data.graphicType="${data.graphicType}, type="${this.type}`); } this._datas = data; this.loadTransformFrom(data); } loadTransformFrom(data) { if (data.transform) { this.loadTransform(data.transform); } if (data.childTransforms) { data.childTransforms.forEach((ct) => { const child = this.getChildByName(ct.name, true); if (child) { child.loadTransform(ct.transform); } }); } } /** * 更新图形数据 * @param data * @returns */ updateData(data) { let update = false; if (!this.getDatas().eq(data)) { update = true; const old = this.getDatas().clone(); this.getDatas().copyFrom(data); this.onDataChange(data, old); this.loadTransformFrom(data); this.emit('dataupdate', this.getDatas(), old); this.repaint(); } return update; } /** * 图形数据更新 */ onDataChange(newVal, old) { } /** * 加载状态 * @param state */ loadState(state) { if (state.graphicType !== this.type) { throw new Error(`不同的图形类型,请检查数据是否正常: state.graphicType="${state.graphicType}, type="${this.type}`); } this._states = state; } /** * 更新状态 * @param state * @returns */ updateStates(state) { let stateChange = false; if (!this.getStates().eq(state)) { // 判断并更新状态,默认状态 const old = this.getStates().clone(); this.getStates().copyFrom(state); this.onStateChange(state, old); stateChange = true; this.emit('stateupdate', this.getStates(), old); // this.repaint(); } return stateChange; } /** * 图形状态更新处理 */ onStateChange(newVal, old) { } repaint() { try { this.doRepaint(); this.emit('repaint', this); } catch (e) { console.error(`设备id=${this.id},type=${this.type}重绘逻辑异常`, e); } } /** * 处理删除逻辑 */ onDelete() { this.removeAllAssistantAppend(); this.removeAllListeners(); recursiveChildren(this, (child) => child.removeAllAssistantAppend()); } /** * 框选检测,默认取图形包围盒判定,若需要精细判定-子类重写此方法 * @param box * @returns */ boxIntersectCheck(box) { return box.intersects(this.getLocalBounds(), this.localTransform); } } /** * 图形对象模板 */ class JlGraphicTemplate { type; options; constructor(type, options) { this.type = type; this.options = options; } get datas() { if (this.options.dataTemplate) { return this.options.dataTemplate.clone(); } throw new Error(`type=${this.type}的图形模板没有数据模板`); } get states() { if (this.options.stateTemplate) { return this.options.stateTemplate.clone(); } throw new Error(`type=${this.type}的图形模板没有状态模板`); } /** * 加载图形对象需要用到的资源 */ async loadAssets() { } /** * 克隆图形对象 * @param graphic * @returns */ clone(graphic) { const g = this.new(); if (graphic._datas) { // 数据克隆 const datas = graphic.saveData(); g.loadData(datas); } if (graphic._states) { // 状态克隆 const state = graphic.getStates().clone(); g.loadState(state); } g.id = GraphicIdGenerator.next(); return g; } } /* eslint-disable @typescript-eslint/no-explicit-any */ class MessageClient extends EventEmitter { options; subClients = []; // 订阅客户端 constructor(options) { super(); this.options = options; } unsubscribe(destination) { this.unsubscribe0(destination); const idx = this.subClients.findIndex((cli) => cli.destination === destination); if (idx >= 0) { this.subClients.splice(idx, 1); } } getOrNewSubClient(destination) { let cli = this.subClients.find((cli) => cli.destination === destination); if (!cli) { // 不存在,新建 cli = new SubscriptionClient(this, destination, this.options.protocol); this.subClients.push(cli); } return cli; } addSubscription(destination, handler) { const cli = this.getOrNewSubClient(destination); cli.addHandler(handler); } removeSubscription(destination, handle) { this.getOrNewSubClient(destination).removeHandler(handle); } publishMessage(destination, message) { const cli = this.getOrNewSubClient(destination); cli.publishMessage(destination, message); } } class SubscriptionClient { mc; destination; protocol; handlers = []; subscripted = false; constructor(mc, destination, protocal) { this.mc = mc; this.destination = destination; this.protocol = protocal; this.mc.on('disconnected', this.onDisconnect, this); this.mc.on('connected', this.trySubscribe, this); } addHandler(handler) { if (this.handlers.filter((h) => h.App === handler.App).length == 0) { this.handlers.push(handler); } if (!this.subscripted) { this.trySubscribe(); } } removeHandler(handler) { const idx = this.handlers.findIndex((h) => h.App === handler.App); if (idx >= 0) { this.handlers.splice(idx, 1); } if (this.handlers.length == 0) { console.log(`订阅${this.destination}没有消息监听处理,移除订阅`); this.unsubscribe(); } } trySubscribe() { if (this.mc.connected) { this.subscripted = this.mc.subscribe(this.destination, (data) => { this.handleMessage(data); }); } } unsubscribe() { this.mc.unsubscribe(this.destination); } publishMessage(destination, message) { if (this.mc.connected) { this.mc.publishMessage(destination, message); } } handleMessage(data) { if (this.protocol === 'json') { console.debug('收到消息:', data); } this.handlers.forEach((handler) => { try { handler.handle(data); } catch (error) { console.error('图形应用状态消息处理异常', error); } }); } onDisconnect() { this.subscripted = false; } } const ReconnectDelay = 3000; const HeartbeatIncoming = 30000; const HeartbeatOutgoing = 30000; class StompMessagingClient extends MessageClient { cli; constructor(options) { super(options); this.options = options; this.cli = new Client({ brokerURL: options.wsUrl, connectHeaders: { Authorization: options.token ? options.token : '', }, reconnectDelay: ReconnectDelay, heartbeatIncoming: HeartbeatIncoming, heartbeatOutgoing: HeartbeatOutgoing, }); this.cli.onConnect = () => { // this.subClients.forEach((cli) => { // this.subscribe(cli.destination, cli.handleMessage); // }); this.emit('connected', ''); }; this.cli.onStompError = (frame) => { const errMsg = frame.headers['message']; if (errMsg === '401') { console.warn('认证失败,断开WebSocket连接'); this.cli.deactivate(); } else { console.error('收到Stomp错误消息', frame); } }; this.cli.onDisconnect = (frame) => { console.log('Stomp 断开连接', frame); this.emit('disconnected', frame); }; // websocket错误处理 this.cli.onWebSocketError = (err) => { console.error('websocket错误', err); this.emit('disconnected', ''); }; this.cli.activate(); } get connected() { return this.cli.connected; } subscribe(destination, handle) { this.cli.subscribe(destination, (frame) => { if (this.options.protocol === 'json') { const data = JSON.parse(frame.body); handle(data); } else { handle(frame.binaryBody); } }, { id: destination, }); return true; } unsubscribe0(destination) { this.cli.unsubscribe(destination); } publishMessage(destination, message) { console.debug('MQTT发布消息:未实现'); } close() { this.cli.deactivate(); } } class MqttMsgClient extends MessageClient { cli; retryTimes = 0; subMsgHandler = new Map(); constructor(options) { super(options); this.options = options; try { this.cli = mqtt.connect(options.wsUrl, { protocolVersion: 5, clean: true, resubscribe: false, keepalive: options.heartbeat, //ms,心跳 connectTimeout: options.connectTimeout, // ms,连接超时 reconnectPeriod: options.retryPeriod, // ms,重连间隔 username: options.clientName || '', password: options.token, }); this.cli.on('connect', (packet) => { console.log('MQTT 连接成功!'); this.retryTimes = 0; // 连接成功,重置 this.emit('connected', packet); }); this.cli.on('disconnect', (packet) => { console.log('MQTT 连接断开!'); this.emit('disconnected', packet); }); this.cli.on('close', () => { console.log('MQTT 关闭!'); this.emit('disconnected', 'close'); }); this.cli.on('reconnect', () => { this.retryTimes += 1; console.log(`MQTT第 ${this.retryTimes} 次尝试重连`); if (this.retryTimes > options.retryTimes) { try { this.cli.end(); console.error('MQTT 达到重连最大尝试次数,停止重试'); } catch (error) { console.error(error); } } }); this.cli.on('error', (error) => { console.log('MQTT 出现错误', error); console.warn(error); this.emit('error', error); }); this.cli.on('message', (topic, message) => { const handle = this.subMsgHandler.get(topic); if (handle) { if (this.options.protocol === 'json') { const data = JSON.parse(message.toString()); handle(data); } else { // 字节流 handle(message); } } else { console.warn('未找到订阅的消息处理器', topic); } }); } catch (err) { console.error('MQTT connect error', err); this.emit('error', err); throw err; } } subscribe(destination, handle) { console.debug('MQTT订阅执行', destination); this.cli.subscribe(destination, { qos: 0 }, (error, res) => { if (error) { console.warn('MQTT 订阅失败', error); return; } console.debug('MQTT 订阅成功', res); return false; }); this.subMsgHandler.set(destination, handle); return true; } unsubscribe0(destination) { console.debug('MQTT取消订阅执行', destination); // 删除指定订阅的消息处理器 this.subMsgHandler.delete(destination); // 通知服务器取消订阅 this.cli.unsubscribe(destination, (error, packet) => { if (error) { console.warn('MQTT 取消订阅失败', error); } else { console.debug('MQTT 取消订阅成功', packet); } }); } get connected() { return this.cli.connected; } close() { try { console.debug('MQTT关闭执行'); this.cli.end(true, () => { console.debug('MQTT 消息客户端关闭成功'); this.subMsgHandler.clear(); }); } catch (error) { console.warn('MQTT 消息客户端关闭失败', error); } } publishMessage(destination, message) { console.debug('MQTT发布消息'); if (this.connected) { this.cli.publish(destination, message); } else { console.warn('MQTT 未连接,消息发布失败'); } } } var ClientEngine; (function (ClientEngine) { ClientEngine[ClientEngine["Stomp"] = 0] = "Stomp"; // Centrifugo, ClientEngine[ClientEngine["MQTT"] = 1] = "MQTT"; })(ClientEngine || (ClientEngine = {})); const DefaultStompOption = { engine: ClientEngine.Stomp, protocol: 'protobuf', wsUrl: '', token: '', connectTimeout: 30 * 1000, heartbeat: 60, retryPeriod: 2 * 1000, retryTimes: 100, }; class WsMsgCli { static client; static options; static appMsgBroker = []; static new(options) { if (WsMsgCli.client) { // 已经创建 return; } WsMsgCli.options = Object.assign({}, DefaultStompOption, options); switch (WsMsgCli.options.engine) { // case ClientEngine.Centrifugo: { // WsMsgCli.client = new CentrifugeMessagingClient(WsMsgCli.options); // break; // } case ClientEngine.MQTT: { WsMsgCli.client = new MqttMsgClient(WsMsgCli.options); break; } case ClientEngine.Stomp: { WsMsgCli.client = new StompMessagingClient(WsMsgCli.options); break; } default: { throw new Error('未知的消息客户端引擎类型', WsMsgCli.options.engine); } } const cli = WsMsgCli.client; cli.on('connected', () => { WsMsgCli.emitConnectStateChangeEvent(true); }); cli.on('disconnected', () => { WsMsgCli.emitConnectStateChangeEvent(false); }); cli.on('error', (err) => { console.warn('websocket 客户端错误消息发布', err); WsMsgCli.appMsgBroker.forEach((broker) => { broker.app.emit('websocket-error', err); }); }); } static isInitiated() { return !!WsMsgCli.client; } static emitConnectStateChangeEvent(connected) { WsMsgCli.appMsgBroker.forEach((broker) => { broker.app.emit('websocket-connect-state-change', connected); }); } static isConnected() { return WsMsgCli.client && WsMsgCli.client.connected; } static registerSubscription(destination, handler) { WsMsgCli.client.addSubscription(destination, handler); } static unregisterSubscription(destination, handler) { WsMsgCli.client.removeSubscription(destination, handler); } static registerAppMsgBroker(broker) { WsMsgCli.appMsgBroker.push(broker); } static publishMessage(destination, message) { WsMsgCli.client.publishMessage(destination, message); } static removeAppMsgBroker(broker) { const index = WsMsgCli.appMsgBroker.findIndex((mb) => mb == broker); if (index >= 0) { WsMsgCli.appMsgBroker.splice(index, 1); } } static hasAppMsgBroker() { return WsMsgCli.appMsgBroker.length > 0; } /** * 关闭websocket连接 */ static close() { if (WsMsgCli.client) { WsMsgCli.client.close(); } } } class AppMessageHandler { app; sub; constructor(app, subOptions) { this.app = app; if (!subOptions.messageConverter && !subOptions.messageHandle) { throw new Error(`没有消息处理器或图形状态消息转换器: ${subOptions}`); } this.sub = subOptions; } get App() { return this.app; } handle(data) { const sub = this.sub; if (sub.messageConverter) { const graphicStates = sub.messageConverter(data); this.app.handleGraphicStates(graphicStates, sub.graphicQueryer, sub.createOnNotFound); } else if (sub.messageHandle) { sub.messageHandle(data); } } } /** * 图形APP的websocket消息代理 */ class AppWsMsgBroker { app; subscriptions = new Map(); constructor(app) { this.app = app; WsMsgCli.registerAppMsgBroker(this); } subscribe(sub) { const handler = new AppMessageHandler(this.app, sub); WsMsgCli.registerSubscription(sub.destination, handler); this.subscriptions.set(sub.destination, handler); } unsbuscribe(destination) { const oldSub = this.subscriptions.get(destination); if (oldSub) { WsMsgCli.unregisterSubscription(destination, oldSub); } } unsubscribeAll() { this.subscriptions.forEach((record, destination) => { this.unsbuscribe(destination); }); } resubscribeAll() { this.subscriptions.forEach((handler, destination) => { WsMsgCli.registerSubscription(destination, handler); }); } publishMessage(destination, message) { WsMsgCli.publishMessage(destination, message); } /** * 取消所有订阅,从通用Stomp客户端移除此消息代理 */ close() { WsMsgCli.removeAppMsgBroker(this); this.unsubscribeAll(); } } /** * 默认的白色样式 */ const DefaultWhiteStyleOptions = { titleStyle: { fontSize: 16, fontColor: '#000000', padding: [5, 15], }, backgroundColor: '#ffffff', border: true, borderWidth: 1, borderColor: '#4C4C4C', /** * 默认圆角 */ borderRoundRadius: 5, itemStyle: { fontSize: 16, fontColor: '#000000', padding: [5, 25], hoverColor: '#1E78DB', disabledFontColor: '#9D9D9D', }, }; /** * 默认的白色菜单配置 */ const DefaultWhiteMenuOptions = { name: '', style: DefaultWhiteStyleOptions, groups: [], }; class ContextMenuPlugin { app; contextMenuMap = new Map(); constructor(app) { this.app = app; const canvas = this.app.canvas; canvas.on('pointerdown', () => { this.contextMenuMap.forEach((menu) => { menu.close(); }); }); } registerMenu(menu) { this.contextMenuMap.set(menu.menuName, menu); menu.plugin = this; } /** * 获取视口屏幕宽度 */ get screenWidth() { return this.app.viewport.screenWidth; } /** * 获取视口屏幕高度 */ get screenHeight() { return this.app.viewport.screenHeight; } /** * 打开菜单 * @param menu * @param global */ open(menu, global) { if (!menu.opened) { menu.opened = true; this.app.pixi.stage.addChild(menu); } // 处理超出显示范围 const screenHeight = this.screenHeight; const bottomY = global.y + menu.height; let oob = this.oob(menu, global); const pos = global.clone(); if (oob.right) { pos.x = global.x - menu.width; } if (oob.bottom) { const py = global.y - menu.height; if (py > 0) { pos.y = py; } else { pos.y = global.y - (bottomY - screenHeight); } } // 移动后是否左上超出 oob = this.oob(menu, pos); if (oob.left) { pos.x = 0; } if (oob.top) { pos.y = 0; } menu.position.copyFrom(pos); } /** * 关闭菜单 * @param menu */ close(menu) { if (menu.opened) { menu.opened = false; this.app.pixi.stage.removeChild(menu); } } /** * 关闭所有菜单 */ closeAll() { this.contextMenuMap.forEach((cm) => { this.close(cm); }); } /** * 越界检查 * @param menu * @param global * @returns */ oob(menu, global) { const screenWidth = this.screenWidth; const screenHeight = this.screenHeight; const bound = new Rectangle(0, 0, screenWidth, screenHeight); const menuRect = new Rectangle(global.x, global.y, menu.width, menu.height); return OutOfBound.check(menuRect, bound); } } /** * 上下文菜单,多级嵌套 */ class ContextMenu extends Container { _plugin; parentMenuItem; openedSubMenu; menuOptions; opened = false; bg; title; groups; padding = 5; _active = false; // 激活状态 timeoutCloseHandle; constructor(menuOptions, parentMenuItem) { super(); this.menuOptions = Object.assign({}, DefaultWhiteMenuOptions, menuOptions); this.name = this.menuOptions.name; this.bg = new Graphics(); this.addChild(this.bg); this.groups = []; this.init(); this.parentMenuItem = parentMenuItem; } static init(options) { return new ContextMenu(options); } get style() { return this.menuOptions.style; } /** * 父级菜单 */ get parentMenu() { return this.parentMenuItem?.menu; } /** * 最顶级菜单 */ get rootMenu() { if (this.parentMenu) { return this.parentMenu.rootMenu; } return this; } /** * 是否存在激活的菜单项 * @returns */ hasActiveItem() { for (let i = 0; i < this.groups.length; i++) { const group = this.groups[i]; if (group.hasActiveItem()) { return true; } } return false; } get active() { return (this._active || this.hasActiveItem() || (this.parentMenuItem != undefined && this.parentMenuItem._active)); } set active(v) { this._active = v; this.onActiveChanged(); } onActiveChanged() { if (this.parentMenuItem) { this.parentMenuItem.onActiveChanged(); if (!this.active) { this.timeoutCloseHandle = setTimeout(() => { this.close(); }, 500); } else { if (this.timeoutCloseHandle) { clearTimeout(this.timeoutCloseHandle); } } } } setOptions(menuOptions) { this.menuOptions = Object.assign({}, DefaultWhiteMenuOptions, menuOptions); this.init(); } /** * 初始化 */ init() { // this.initTitle(); this.groups = []; const options = this.menuOptions; options.groups.forEach((group) => { const menuGroup = new MenuGroup(this, group); this.groups.push(menuGroup); }); const { borderHeight, maxItemWidth } = this.calculateBorderInfo(); const splitLineWidth = 1; const bgWidth = maxItemWidth + this.padding * 2; const bgHeight = borderHeight + this.groups.length * this.padding * 2 + (this.groups.length - 1) * splitLineWidth; if (options.style.border) { this.bg.lineStyle(options.style.borderWidth, new Color(options.style.borderColor)); } this.bg.beginFill(new Color(options.style.backgroundColor)); if (options.style.borderRoundRadius > 0) { this.bg.drawRoundedRect(0, 0, bgWidth, bgHeight, options.style.borderRoundRadius); } else { this.bg.drawRect(0, 0, bgWidth, bgHeight); } this.bg.endFill(); let groupHeight = 0; this.bg.lineStyle(splitLineWidth, new Color(options.style.borderColor)); for (let i = 0; i < this.groups.length; i++) { const group = this.groups[i]; group.updateItemBox(maxItemWidth); group.position.set(this.padding, groupHeight + this.padding); if (i === this.groups.length - 1) { // 最后一个 break; } const splitLineY = groupHeight + group.height + this.padding * 2; this.bg.moveTo(0, splitLineY); this.bg.lineTo(bgWidth, splitLineY); groupHeight = splitLineY + splitLineWidth; } this.addChild(...this.groups); this.eventMode = 'static'; this.on('pointerover', () => { this.active = true; }); this.on('pointerout', () => { this.active = false; }); } initGroups() { this.groups = []; const options = this.menuOptions; options.groups.forEach((group) => { const menuGroup = new MenuGroup(this, group); this.groups.push(menuGroup); }); this.addChild(...this.groups); } initTitle() { if (this.menuOptions.title) { this.title = new Text(this.menuOptions.title, { align: 'left' }); } } calculateBorderInfo() { let maxItemNameWidth = 0; let maxShortcutWidth = 0; let maxGutter = 0; let borderHeight = 0; this.groups.forEach((menuGroup) => { borderHeight += menuGroup.totalHeight; const maxInw = menuGroup.maxItemNameWidth; if (maxInw > maxItemNameWidth) { maxItemNameWidth = maxInw; } const maxSw = menuGroup.maxShortcutWidth; if (maxSw > maxShortcutWidth) { maxShortcutWidth = maxSw; } const gutter = menuGroup.totalGutter; if (gutter > maxGutter) { maxGutter = gutter; } }); const maxItemWidth = maxItemNameWidth + maxShortcutWidth + maxGutter; return { borderHeight, maxItemWidth }; } updateBg() { this.bg.clear(); const options = this.menuOptions; const { borderHeight, maxItemWidth } = this.calculateBorderInfo(); const splitLineWidth = 1; const bgWidth = maxItemWidth + this.padding * 2; const bgHeight = borderHeight + this.groups.length * this.padding * 2 + (this.groups.length - 1) * splitLineWidth; if (options.style.border) { this.bg.lineStyle(options.style.borderWidth, new Color(options.style.borderColor)); } this.bg.beginFill(new Color(options.style.backgroundColor)); if (options.style.borderRoundRadius > 0) { this.bg.drawRoundedRect(0, 0, bgWidth, bgHeight, options.style.borderRoundRadius); } else { this.bg.drawRect(0, 0, bgWidth, bgHeight); } this.bg.endFill(); let groupHeight = 0; this.bg.lineStyle(splitLineWidth, new Color(options.style.borderColor)); for (let i = 0; i < this.groups.length; i++) { const group = this.groups[i]; group.updateItemBox(maxItemWidth); group.position.set(this.padding, groupHeight + this.padding); if (i === this.groups.length - 1) { // 最后一个 break; } const splitLineY = groupHeight + group.height + this.padding * 2; this.bg.moveTo(0, splitLineY); this.bg.lineTo(bgWidth, splitLineY); groupHeight = splitLineY + splitLineWidth; } } update() { if (this.menuOptions.groups.length !== this.groups.length) { this.init(); } else { this.groups.forEach((group) => group.update()); this.updateBg(); } } get menuName() { return this.menuOptions.name; } get plugin() { if (this.parentMenu) { return this.parentMenu.plugin; } if (this._plugin) { return this._plugin; } throw new Error(`上下文菜单name=${this.menuOptions.name}没有添加到插件中`); } set plugin(v) { this._plugin = v; } /** * 显示菜单 */ open(global) { if (this.parentMenu) { this.parentMenu.openSub(this, global); } else { this.update(); this.plugin.open(this, global); } } /** * 关闭菜单 */ close() { if (this.openedSubMenu) { this.openedSubMenu.close(); this.openedSubMenu = undefined; } this.plugin.close(this); } /** * 打开子菜单 * @param subMenu * @param global */ openSub(subMenu, global) { if (this.openedSubMenu) { this.openedSubMenu.close(); } const pos = global.clone(); const oob = this.plugin.oob(subMenu, global); if (oob.right) { pos.x = this.position.x - subMenu.width + this.padding; } if (oob.bottom) { pos.y = this.plugin.screenHeight - subMenu.height - this.padding; } this.plugin.open(subMenu, pos); this.openedSubMenu = subMenu; } } class MenuGroup extends Container { gutter = 3; // 名称、快捷键、箭头文本间隙 config; menu; items = []; constructor(menu, config) { super(); this.config = config; this.menu = menu; this.init(); } init() { this.items = []; // 清空 this.config.items.forEach((item) => { this.items.push(new ContextMenuItem(this.menu, item)); }); if (this.items.length === 0) { console.error('菜单group为空', this.config, this.menu); throw new Error(`{name=${this.menu.name}}的菜单的group为{name=${this.config.name}}的条目为空!`); } this.addChild(...this.items); } hasActiveItem() { for (let i = 0; i < this.items.length; i++) { const item = this.items[i]; if (item.active) { return true; } } return false; } get maxItemNameWidth() { const maxNameWidth = this.items .map((item) => item.nameBounds.width) .sort((a, b) => a - b) .reverse()[0]; return maxNameWidth; } get maxShortcutWidth() { const maxShortcutWidth = this.items .map((item) => item.shortcutKeyBounds.width) .sort((a, b) => a - b) .reverse()[0]; return maxShortcutWidth; } get totalGutter() { return this.gutter + this.items[0].paddingLeft + this.items[0].paddingRight; } get totalHeight() { let total = 0; this.items.forEach((item) => (total += item.totalHeight)); return total; } update() { if (this.items.length !== this.config.items.length) { this.init(); } else { let i = 0; this.items.forEach((item) => { item.update(); if (item.visible) { item.position.y = i * item.totalHeight; i++; } }); } } updateItemBox(maxItemWidth) { this.items.forEach((item) => item.updateBox(maxItemWidth, item.totalHeight)); } } /** * 菜单项 */ class ContextMenuItem extends Container { menu; config; /** * 名称文本 */ nameText; /** * 快捷键文本 */ shortcutKeyText; gutter = 3; // 名称、快捷键、箭头文本间隙 arrowText; box; subMenu; _active = false; // 激活状态 constructor(menu, config) { super(); this.menu = menu; this.config = config; this.box = new Graphics(); this.addChild(this.box); this.nameText = new Text(this.config.name, { fontSize: this.fontSize, fill: this.fontColor, }); this.addChild(this.nameText); this.initShortcutKeyText(); this.initSubMenu(); } registerEventHandler() { this.eventMode = 'static'; this.cursor = 'pointer'; this.on('pointerover', () => { this.active = true; if (this.config.disabled) { this.cursor = 'not-allowed'; } else { this.cursor = 'pointer'; } if (this.subMenu) { const p = this.toGlobal(new Point(this.width)); this.subMenu.open(p); } }); this.on('pointerout', () => { this.active = false; }); this.on('pointertap', () => { if (this.config.disabled) { // 禁用,不处理 return; } if (this.config.handler) { this.menu.plugin.app.emit('pre-menu-handle', this.config); this.config.handler(); this.menu.plugin.app.emit('post-menu-handle', this.config); } if (!this.config.subMenu || this.config.subMenu.groups.length === 0) { this.active = false; this.menu.active = false; this.menu.rootMenu.close(); } }); } get active() { return this._active || (this.subMenu != undefined && this.subMenu.active); } set active(v) { this._active = v; if (this.subMenu) { this.subMenu.onActiveChanged(); } this.onActiveChanged(); } onActiveChanged() { if (this.active) { this.box.alpha = 1; } else { this.box.alpha = 0; } } get textWidth() { return this.nameBounds.width + this.shortcutKeyBounds.width + this.gutter; } get nameGraphic() { if (this.nameText) { return this.nameText; } throw new Error(`菜单项name=${this.config.name}没有初始化名称图形对象`); } get totalHeight() { if (this.config.visible === false) { return 0; } else { return this.paddingTop + this.paddingBottom + this.nameGraphic.height; } } get nameBounds() { return this.nameGraphic.getLocalBounds(); } get shortcutKeyBounds() { if (this.shortcutKeyText) { return this.shortcutKeyText.getLocalBounds(); } else { return new Rectangle(0, 0, 0, 0); } } get style() { return this.menu.style.itemStyle; } checkPadding(padding) { if (Array.isArray(padding)) { if (padding.length !== 2 && padding.length !== 4) { throw new Error('错误的padding数据'); } } } toWholePadding(padding) { this.checkPadding(padding); if (Array.isArray(padding)) { if (padding.length == 2) { return [padding[0], padding[1], padding[0], padding[1]]; } else { return padding; } } else { return [padding, padding, padding, padding]; } } get paddingTop() { return this.toWholePadding(this.menu.style.itemStyle.padding)[0]; } get paddingBottom() { return this.toWholePadding(this.menu.style.itemStyle.padding)[2]; } get paddingLeft() { return this.toWholePadding(this.menu.style.itemStyle.padding)[3]; } get paddingRight() { return this.toWholePadding(this.menu.style.itemStyle.padding)[1]; } get hoverColor() { return this.style.hoverColor; } get fontSize() { return this.style.fontSize; } get fontColor() { if (this.config.disabled) { return this.style.disabledFontColor; } else if (this.config.fontColor) { return this.config.fontColor; } return this.style.fontColor; } initShortcutKeyText() { if (this.config.shortcutKeys && this.config.shortcutKeys.length > 0) { this.shortcutKeyText = new Text(this.config.shortcutKeys.join('+'), { fontSize: this.fontSize, fill: this.fontColor, }); this.addChild(this.shortcutKeyText); return this.shortcutKeyText; } return undefined; } initSubMenu() { if (this.config.subMenu && this.config.subMenu.groups.length > 0) { this.arrowText = new Text('>', { fontSize: this.fontSize, fill: this.fontColor, }); this.addChild(this.arrowText); this.subMenu = new ContextMenu(this.config.subMenu, this); } } updateBackground(width, height) { this.box.clear(); const box = this.box; const style = this.menu.style; if (!style.itemStyle.hoverColor) { throw new Error('未设置菜单项的hoverColor'); } let hoverColor = style.itemStyle.hoverColor; if (this.style && this.style.hoverColor) { hoverColor = this.style.hoverColor; } box.beginFill(new Color(hoverColor)); if (style.borderRoundRadius > 0) { box.drawRoundedRect(0, 0, width, height, style.borderRoundRadius); } else { box.drawRect(0, 0, width, height); } box.endFill(); box.alpha = 0; } updateBox(width, height) { this.removeAllListeners(); this.updateBackground(width, height); this.nameText?.position.set(this.paddingLeft, this.paddingTop); if (this.shortcutKeyText) { const skTextWidth = this.shortcutKeyBounds.width; this.shortcutKeyText.position.set(width - skTextWidth - this.paddingRight, this.paddingTop); } if (this.arrowText) { this.arrowText.position.set(width - this.paddingRight - this.gutter, this.paddingTop); } // 事件监听 this.hitArea = new Rectangle(0, 0, width, height); this.registerEventHandler(); } update() { if (this.config.visible === false) { this.visible = false; return; } this.visible = true; this.nameText.text = this.config.name; this.nameText.style.fontSize = this.fontSize; this.nameText.style.fill = this.fontColor; if (this.shortcutKeyText) { if (this.config.shortcutKeys && this.config.shortcutKeys.length > 0) { this.shortcutKeyText.text = this.config.shortcutKeys.join('+'); this.shortcutKeyText.style.fontSize = this.fontSize; this.shortcutKeyText.style.fill = this.fontColor; } else { this.shortcutKeyText.visible = false; } } else { this.initShortcutKeyText(); } if (this.config.subMenu == undefined && this.subMenu) { this.subMenu = undefined; } else if (this.config.subMenu && this.subMenu == undefined) { this.initSubMenu(); } if (this.subMenu) { this.subMenu.update(); } } } const AppConsts = { viewportname: '__viewport', canvasname: '__jlcanvas', AssistantAppendsName: '__assistantAppends', // 辅助元素默认颜色 assistantElementColor: '#1976d2', }; class CanvasData { width; height; backgroundColor; viewportTransform; gridBackground; constructor(properties = { width: 1920, height: 1080, backgroundColor: '#ffffff', viewportTransform: GraphicTransform.default(), gridBackground: undefined, }) { this.width = properties.width; this.height = properties.height; this.backgroundColor = properties.backgroundColor; this.viewportTransform = properties.viewportTransform; this.gridBackground = properties.gridBackground; } copyFrom(properties) { let sizeChange = false; if (properties.width <= 0 || properties.height <= 0) { console.error('画布宽度/高度不能小于等于0'); } else { const width = Math.floor(properties.width); const height = Math.floor(properties.height); if (this.width != width) { this.width = width; sizeChange = true; } if (this.height != height) { this.height = height; sizeChange = true; } } this.backgroundColor = properties.backgroundColor; this.viewportTransform = properties.viewportTransform; if (properties.gridBackground) { this.gridBackground = properties.gridBackground; } return sizeChange; } clone() { const cp = new CanvasData(this); return cp; } } class JlCanvas extends Container { __JlCanvas = true; type = 'Canvas'; scene; _properties; bg = new Graphics(); // 背景 gridBackgroundLine = new Container(); //网格背景 nonInteractiveContainer; // 无交互对象容器 assistantAppendContainer; // 辅助附加容器 constructor(scene, properties = new CanvasData()) { super(); this.scene = scene; this._properties = properties; this.eventMode = 'static'; this.nonInteractiveContainer = new Container(); this.nonInteractiveContainer.name = 'non-interactives'; this.nonInteractiveContainer.eventMode = 'none'; this.addChild(this.bg); this.addChild(this.gridBackgroundLine); this.addChild(this.nonInteractiveContainer); this.sortableChildren = true; this.assistantAppendContainer = new Container(); this.assistantAppendContainer.eventMode = 'static'; this.assistantAppendContainer.name = AppConsts.AssistantAppendsName; this.assistantAppendContainer.zIndex = 999; this.assistantAppendContainer.sortableChildren = true; this.addChild(this.assistantAppendContainer); this.repaint(); } /** * 图形重绘(数据/状态变化时触发) */ repaint() { this.doRepaint(); } get width() { return this._properties.width; } get height() { return this._properties.height; } get backgroundColor() { return this._properties.backgroundColor; } get gridBackground() { return this._properties.gridBackground; } doRepaint() { this.bg.clear(); this.bg .beginFill(new Color(this.backgroundColor)) .drawRect(0, 0, this._properties.width, this._properties.height) .endFill(); this.gridBackgroundLine.children.forEach((g) => { g.clear(); }); if (this.gridBackground && this.gridBackground.hasGrid && this.gridBackground.space > 0) { const horizontalAmount = this.height / this.gridBackground.space; for (let i = 1; i < horizontalAmount; i++) { const lineGraphic = new Graphics(); const posY = i * this.gridBackground.space; lineGraphic .clear() .lineStyle(1, new Color(this.gridBackground.lineColor)) .moveTo(0, posY) .lineTo(this.width, posY); this.gridBackgroundLine.addChild(lineGraphic); } const verticalAmount = this.width / this.gridBackground.space; for (let i = 1; i < verticalAmount; i++) { const lineGraphic = new Graphics(); const posX = i * this.gridBackground.space; lineGraphic .clear() .lineStyle(1, new Color(this.gridBackground.lineColor)) .moveTo(posX, 0) .lineTo(posX, this.height); this.gridBackgroundLine.addChild(lineGraphic); } } } get properties() { return this._properties; } saveData() { const vp = this.getViewport(); this.properties.viewportTransform = vp.saveTransform(); return this.properties.clone(); } update(properties) { // 更新画布 const old = this.properties.clone(); this._properties.copyFrom(properties); this.repaint(); const vp = this.getViewport(); vp.loadTransform(properties.viewportTransform); this.emit('dataupdate', this.properties, old); } addChild(...children) { const rt = super.addChild(...children); children.forEach((g) => { g.onAddToCanvas(this); recursiveChildren(g, (child) => child.onAddToCanvas(this)); }); return rt; } removeChild(...children) { children.forEach((g) => { g.onRemoveFromCanvas(this); recursiveChildren(g, (child) => child.onRemoveFromCanvas(this)); }); return super.removeChild(...children); } /** * 添加无交互Child */ addNonInteractiveChild(...obj) { this.nonInteractiveContainer.addChild(...obj); obj.forEach((g) => { g.onAddToCanvas(this); recursiveChildren(g, (child) => child.onAddToCanvas(this)); }); } removeGraphic(...obj) { obj.forEach((g) => { g.onRemoveFromCanvas(this); recursiveChildren(g, (child) => child.onRemoveFromCanvas(this)); }); this.removeChild(...obj); this.nonInteractiveContainer.removeChild(...obj); } /** * 移除无交互Child */ removeNonInteractiveChild(...obj) { obj.forEach((g) => { g.onRemoveFromCanvas(this); recursiveChildren(g, (child) => child.onRemoveFromCanvas(this)); }); this.nonInteractiveContainer.removeChild(...obj); } addAssistantAppends(...appends) { this.assistantAppendContainer.addChild(...appends); appends.forEach((g) => { g.onAddToCanvas(this); recursiveChildren(g, (child) => child.onAddToCanvas(this)); }); } removeAssistantAppends(...appends) { appends.forEach((g) => { g.onRemoveFromCanvas(this); recursiveChildren(g, (child) => child.onAddToCanvas(this)); }); this.assistantAppendContainer.removeChild(...appends); } /** * 暂停所有可交互对象 */ pauseInteractiveChildren() { this.interactiveChildren = false; } /** * 恢复所有可交互对象 */ resumeInteractiveChildren() { this.interactiveChildren = true; } } class GraphicSceneBase extends EventEmitter { graphicStore; _options; pixi; // Pixi 渲染器 viewport; // 视口 canvas; // 画布 _loaded = false; // 是否已经加载 _dom; // 场景绑定到的dom节点 _viewportResizer; // 自动根据dom大小变化调整视口尺寸 graphicTemplateMap = new Map(); // 图形对象模板 interactionPluginMap = new Map(); // 交互插件 graphicCopyPlugin; // 图形复制操作插件 animationManager; // 动画管理组件 menuPlugin; // 菜单插件 debounceEmitFunc; wsMsgBroker; // websocket消息代理 constructor(options) { super(); this.graphicStore = new GraphicStore(); this._options = options; // 创建pixi渲染app this.pixi = new Application({ antialias: true, }); // 创建画布 this.canvas = new JlCanvas(this); this.canvas.name = AppConsts.canvasname; // 创建相机 this.viewport = new Viewport({ screenWidth: window.innerWidth, screenHeight: window.innerHeight, worldWidth: this.canvas._properties.width, worldHeight: this.canvas._properties.height, passiveWheel: true, events: this.pixi.renderer.events, disableOnContextMenu: true, }); // 设置视口操作方式 this.viewport .wheel({ percent: 1, }) .pinch() .clampZoom({ minScale: 0.1, maxScale: 8, }) .clamp({ direction: 'all', }); this.viewport.name = AppConsts.viewportname; this.viewport.interactiveChildren = true; // 添加视口到渲染器舞台 this.pixi.stage.addChild(this.viewport); // 将画布置于视口 this.viewport.addChild(this.canvas); // 监听并通知缩放变化事件 this.viewport.on('zoomed-end', () => { this.emit('viewport-scaled', this.viewport); }); this.graphicCopyPlugin = new GraphicCopyPlugin(this); // 添加通用交互插件 CommonMouseTool.new(this).resume(); // drag插件 DragPlugin.new(this).resume(); // 视口移动插件 ViewportMovePlugin.new(this); // 图形变换插件 GraphicTransformPlugin.new(this).resume(); // 动画管理 this.animationManager = new AnimationManager(this); this.menuPlugin = new ContextMenuPlugin(this); this.wsMsgBroker = new AppWsMsgBroker(this); this.debounceEmitFunc = debounce(this.doEmitAppGraphicSelected, 50); this.on('graphicselectedchange', () => { this.debounceEmitFunc(this); }); // 发布选项更新事件 this.emit('options-update', this._options); } get appOptions() { return this._options; } get dom() { return this._dom; } get queryStore() { return this.graphicStore; } get selectedGraphics() { return this.queryStore.getAllGraphics().filter((g) => g.selected); } async load() { if (this._options.dataLoader) { const storage = await this._options.dataLoader(); if (storage.canvasProperty) { this.canvas.update(storage.canvasProperty); } if (storage.datas) { await this.loadGraphic(storage.datas); } } this._loaded = true; } /** * 重新加载数据 */ async reload() { if (!this._loaded) { this.graphicStore.clear(); await this.load(); } else { console.debug('场景已经加载过数据,不重新加载', this); } } async forceReload() { console.debug('场景强制重新加载数据', this); this.graphicStore.clear(); await this.load(); } /** * 更新选项 * @param options */ setOptions(options) { if (this._options) { this._options = Object.assign(this._options, options); } else { this._options = options; } this.emit('options-update', options); } toCanvasCoordinates(p) { return this.viewport.toWorld(p); } /** * 注册菜单 * @param menu */ registerMenu(menu) { this.menuPlugin.registerMenu(menu); } /** * 注册图形对象模板 * @param graphicTemplates */ registerGraphicTemplates(...graphicTemplates) { graphicTemplates.forEach((graphicTemplate) => { this.graphicTemplateMap.set(graphicTemplate.type, graphicTemplate); }); } getGraphicTemplatesByType(type) { const template = this.graphicTemplateMap.get(type); if (!template) { throw new Error(`不存在type=${type}的图形对象模板`); } return template; } updateViewport(domWidth, domHeight) { let screenWidth = this.viewport.screenWidth; let screenHeight = this.viewport.screenHeight; if (domWidth) { screenWidth = domWidth; } if (domHeight) { screenHeight = domHeight; } const worldWidth = this.canvas._properties.width; const worldHeight = this.canvas._properties.height; this.pixi.resize(); this.viewport.resize(screenWidth, screenHeight, worldWidth, worldHeight); if (this.viewport.OOB().right) { this.viewport.right = this.viewport.right + 1; } else if (this.viewport.OOB().left) { this.viewport.left = this.viewport.left - 1; } else if (this.viewport.OOB().top) { this.viewport.top = this.viewport.top - 1; } else if (this.viewport.OOB().bottom) { this.viewport.bottom = this.viewport.bottom + 1; } } /** * 暂停 */ pause() { // 暂停动画 this.animationManager.pause(); // 取消消息订阅 this.wsMsgBroker.unsubscribeAll(); // 关闭所有上下文菜单 this.menuPlugin.closeAll(); } /** * 恢复 */ resume() { // 恢复动画 this.animationManager.resume(); // 重新订阅 this.wsMsgBroker.resubscribeAll(); } bindDom(dom) { this._dom = dom; this.pixi.resizeTo = dom; dom.appendChild(this.pixi.view); // 绑定dom后,先调整视口 this.updateViewport(dom.clientWidth, dom.clientHeight); // 监听dom大小变化 this._viewportResizer = setInterval(() => { this.updateViewport(dom.clientWidth, dom.clientHeight); }, 1000); // 恢复 this.resume(); } unbindDom() { if (this._dom) { clearInterval(this._viewportResizer); this._dom.removeChild(this.pixi.view); this._dom = undefined; // 暂停 this.pause(); } } /** * 加载图形,GraphicApp默认添加到无交互容器,DrawApp默认添加到交互容器,如需定制,提供选项配置 * @param protos * @param options 添加到可交互/不可交互容器选项配置 */ async loadGraphic(protos) { for (const item of this.graphicTemplateMap) { await item[1].loadAssets(); } // 加载数据到图形存储 protos.forEach((proto) => { const template = this.getGraphicTemplatesByType(proto.graphicType); const g = template.new(); g.loadData(proto); this.addGraphics(g); }); // 加载数据关系 this.queryStore.getAllGraphics().forEach((g) => g.loadRelations()); // 更新id生成器 const max = this.queryStore .getAllGraphics() .map((g) => g.id) .sort((a, b) => a - b) .pop() ?? 0; GraphicIdGenerator.initSerial(max); // 数据加载完成后 this.emit('postdataloaded'); // 加载完成通知 this.emit('loadfinish'); } /** * 添加图形前处理 * @param graphic */ beforeGraphicStore(graphic) { const interactiveGraphicTypeIncludes = this._options.interactiveGraphicTypeIncludes || []; const interactiveGraphicTypeExcludes = this._options.interactiveGraphicTypeExcludes || []; // 默认无交互 graphic.eventMode = 'auto'; if (interactiveGraphicTypeIncludes.findIndex((type) => type === graphic.type) >= 0) { graphic.eventMode = 'static'; } else if (interactiveGraphicTypeExcludes.findIndex((type) => type === graphic.type) < 0) { graphic.eventMode = 'static'; } } /** * 执行添加图形对象 * @param graphic */ doAddGraphics(graphic) { this.beforeGraphicStore(graphic); if (this.graphicStore.storeGraphics(graphic)) { // cullable,默认设置剪裁,如果图形包围框不在屏幕内,则不渲染,增加效率用 if (!this._options || this._options.cullable !== false) { graphic.cullable = true; } if (graphic.eventMode == 'static' || graphic.eventMode == 'dynamic') { // 添加为可交互 this.canvas.addChild(graphic); } else { // 添加到不可交互容器 this.canvas.addNonInteractiveChild(graphic); } graphic.repaint(); this.emit('graphicstored', graphic); graphic.on('childselected', (child) => { this.emit('graphicchildselectedchange', child, true); }); graphic.on('childunselected', (child) => { this.emit('graphicchildselectedchange', child, false); }); } } doDeleteGraphics(graphic) { // 从store中删除 const g = this.graphicStore.deleteGraphics(graphic); if (g) { // 清除选中 g.updateSelected(false); // 从画布移除 this.canvas.removeGraphic(g); // 对象删除处理 g.onDelete(); this.emit('graphicdeleted', g); } } /** * 存储图形 * @param graphics 图形对象 */ addGraphics(...graphics) { graphics.forEach((g) => this.doAddGraphics(g)); } /** * 删除图形 * @param graphics 图形对象 */ deleteGraphics(...graphics) { const dels = graphics.filter((g) => { if (this._options?.isSupportDeletion == undefined || this._options.isSupportDeletion(g)) { this.doDeleteGraphics(g); return true; } console.debug(`type=${g.type},id=${g.id}的图形不支持删除`); return false; }); return dels; } /** * 检测并构建关系 */ detectRelations() { this.queryStore.getAllGraphics().forEach((g) => g.buildRelation()); } /** * 全选 */ selectAllGraphics(filter) { if (filter == undefined) { filter = (g) => g.visible; } this.updateSelected(...this.queryStore.getAllGraphics().filter(filter)); } /** * 更新选中 */ updateSelected(...graphics) { this.selectedGraphics.forEach((graphic) => { if (graphics.findIndex((g) => g.id === graphic.id) >= 0) { return; } if (graphic.selected) { graphic.updateSelected(false); } }); graphics.forEach((graphic) => { graphic.updateSelected(true); }); } doEmitAppGraphicSelected() { // 场景发布图形选中 this.emit('graphicselected', this.selectedGraphics); // this.app.emit('graphicselected', this.selectedGraphics); } /** * 更新画布 * @param param */ updateCanvas(param) { this.canvas.update(param); } /** * 使图形居中显示(所有图形的外包围盒) */ makeGraphicCenterShow(...group) { if (group.length > 0) { const bounds0 = group[0].getBounds(); let lx = bounds0.x; let ly = bounds0.y; let rx = bounds0.x + bounds0.width; let ry = bounds0.y + bounds0.height; if (group.length > 1) { for (let i = 1; i < group.length; i++) { const g = group[i]; const bound = g.getBounds(); if (bound.x < lx) { lx = bound.x; } if (bound.y < ly) { ly = bound.y; } const brx = bound.x + bound.width; if (brx > rx) { rx = brx; } const bry = bound.y + bound.height; if (bry > ry) { ry = bry; } } } const { x, y } = getRectangleCenter(new Rectangle(lx, ly, rx - lx, ry - ly)); const p = this.viewport.toWorld(x, y); this.viewport.moveCenter(p.x, p.y); } } /** * 注册交互插件,会替换旧的 */ registerInteractionPlugin(...plugins) { plugins.forEach((plugin) => { const old = this.interactionPluginMap.get(plugin.name); if (old) { console.warn(`已经存在name=${plugin.name}的交互插件,忽略此插件注册!`); return; } this.interactionPluginMap.set(plugin.name, plugin); }); } /** * 根据名称获取交互插件 * @param name * @returns */ interactionPlugin(name) { const plugin = this.interactionPluginMap.get(name); if (!plugin) { throw new Error(`未找到name=${name}的交互插件`); } return plugin; } /** * 停止应用交互插件 */ pauseAppInteractionPlugins() { this.interactionPluginMap.forEach((plugin) => { if (plugin.isActive() && plugin._type === InteractionPluginType.App) { this.doPauseInteractionPlugin(plugin); } }); } doPauseInteractionPlugin(plugin) { if (plugin && plugin.isActive()) { plugin.pause(); this.emit('interaction-plugin-pause', plugin); } } /** * 移除交互插件 */ removeInteractionPlugin(plugin) { this.interactionPluginMap.delete(plugin.name); } checkWsMsgCli() { if (!WsMsgCli.isInitiated()) { throw new Error('订阅消息需先启动消息代理, 执行app.enableWebsocket()'); } } /** * 订阅websocket消息 */ subscribe(sub) { this.checkWsMsgCli(); this.wsMsgBroker.subscribe(sub); } /** * 取消websocket订阅 */ unsubscribe(destination) { this.checkWsMsgCli(); this.wsMsgBroker.unsbuscribe(destination); } /** * 发布websocket消息 */ publishMessage(destination, message) { this.checkWsMsgCli(); this.wsMsgBroker.publishMessage(destination, message); } /** * 处理图形状态 * @param graphicStates */ handleGraphicStates(graphicStates, queryer, createOnNotFound) { graphicStates.forEach((state) => { let g; if (queryer) { g = queryer(state, this.queryStore); } else { g = this.queryStore.queryByCodeAndType(state.code, state.graphicType); } try { if (!g) { // 未找到图形对象 if (!state.remove && createOnNotFound && createOnNotFound.graphicTypes && createOnNotFound.graphicTypes.findIndex((v) => v === state.graphicType) >= 0) { const template = this.getGraphicTemplatesByType(state.graphicType); const g = template.new(); g.loadState(state); this.addGraphics(g); } } else { // 找到 if (state.remove) { this.deleteGraphics(g); g.destroy({ children: true }); } else if (g.updateStates(state)) { g.repaint(); } } } catch (err) { console.error('图形状态处理异常', g, state, err); // throw err; } }); } /** * 销毁 */ destroy() { console.debug('场景销毁', this); this.unbindDom(); if (this.wsMsgBroker) { this.wsMsgBroker.close(); } this.interactionPluginMap.forEach((plugin) => { plugin.pause(); }); this.animationManager.destroy(); this.canvas.destroy(true); this.viewport.destroy(); this.pixi.destroy(true, true); } } /** * 图形app基类 */ class GraphicApp extends GraphicSceneBase { /** * 场景列表 */ scenes = new Map(); opRecord; // 操作记录 keyboardPlugin; // 键盘操作处理插件 constructor(options) { super(options); this.opRecord = new OperationRecord(); // 绑定键盘监听 this.keyboardPlugin = new JlGraphicAppKeyboardPlugin(this); } get app() { return this; } setOptions(options) { if (options.maxOperationRecords && options.maxOperationRecords > 0) { this.opRecord.setMaxLen(options.maxOperationRecords); } super.setOptions(options); } addGraphicAndRecord(...graphics) { this.addGraphics(...graphics); this.opRecord.record(new GraphicCreateOperation(this, graphics)); } deleteGraphicAndRecord(...graphics) { this.deleteGraphics(...graphics); this.opRecord.record(new GraphicDeleteOperation(this, graphics)); } /** * 实例化一个场景 * @param code 场景标识 * @returns */ initScene(code, options) { let scene = this.scenes.get(code); if (!scene) { scene = new JlScene(this, code, options); this.scenes.set(code, scene); } return scene; } /** * 获取场景 * @param code * @returns */ getScene(code) { const scene = this.scenes.get(code); if (!scene) { throw new Error(`不存在code=${code}的场景`); } return scene; } switchScene(code, dom) { const scene = this.getScene(code); // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const [_code, pre] of this.scenes) { if (pre.dom === dom) { pre.unbindDom(); break; } } scene.bindDom(dom); } removeScene(code) { const scene = this.scenes.get(code); if (scene) { this.scenes.delete(code); scene.destroy(); } } /** * 启动websocket消息客户端 */ enableWsMassaging(options) { WsMsgCli.new(options); this.wsMsgBroker = new AppWsMsgBroker(this); } /** * 添加键盘监听器,如果是相同的按键,新注册的会覆盖旧的,当移除新的时,旧的自动生效 * @param keyListeners */ addKeyboardListener(...keyListeners) { keyListeners.forEach((keyListener) => this.keyboardPlugin.addKeyListener(keyListener)); } /** * 移除键盘监听器 * @param keyListeners */ removeKeyboardListener(...keyListeners) { keyListeners.forEach((keyListener) => this.keyboardPlugin.removeKeyListener(keyListener)); } /** * 销毁 */ destroy() { console.debug('图形应用销毁', this); this.emit('destroy', this); super.destroy(); this.scenes.forEach((scene) => scene.destroy()); } } /** * 场景 */ class JlScene extends GraphicSceneBase { code; app; constructor(app, code, options) { super(options); this.code = code; this.app = app; } } /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-empty-function */ /** * 图形绘制助手 */ class GraphicDrawAssistant extends AppInteractionPlugin { __GraphicDrawAssistant = true; app; type; // 图形对象类型 description; // 描述 icon; // 界面显示的图标 container = new Container(); graphicTemplate; escListener = new KeyListener({ value: 'Escape', onRelease: () => { this.onEsc(); }, }); onEsc() { this.createAndStore(true); } constructor(graphicApp, graphicTemplate, icon, description) { super(graphicTemplate.type, graphicApp); this.app = graphicApp; this.type = graphicTemplate.type; this.graphicTemplate = graphicTemplate; this.icon = icon; this.description = description; this.app.registerGraphicTemplates(this.graphicTemplate); } get canvas() { return this.app.canvas; } bind() { this.app.drawing = true; const canvas = this.canvas; canvas.addChild(this.container); canvas.on('mousedown', this.onLeftDown, this); canvas.on('mousemove', this.onMouseMove, this); canvas.on('mouseup', this.onLeftUp, this); canvas.on('rightdown', this.onRightDown, this); canvas.on('rightup', this.onRightUp, this); canvas.on('_rightclick', this.onRightClick, this); this.app.viewport.wheel({ percent: 0.01, }); this.app.addKeyboardListener(this.escListener); this.app.viewport.drag({ mouseButtons: 'right', }); } unbind() { this.clearCache(); const canvas = this.canvas; if (this.container?.parent) { canvas.removeChild(this.container); } canvas.off('mousedown', this.onLeftDown, this); canvas.off('mousemove', this.onMouseMove, this); canvas.off('mouseup', this.onLeftUp, this); canvas.off('rightdown', this.onRightDown, this); canvas.off('rightup', this.onRightUp, this); this.app.viewport.plugins.remove('wheel'); this.app.removeKeyboardListener(this.escListener); this.app.viewport.plugins.remove('drag'); this.app.drawing = false; } onLeftDown(e) { } onMouseMove(e) { this.redraw(this.toCanvasCoordinates(e.global)); } onLeftUp(e) { } onRightDown(e) { } onRightUp(e) { } onRightClick(e) { this.finish(); } /** * 获取下一个id */ nextId() { return GraphicIdGenerator.next(); } clearCache() { } toCanvasCoordinates(p) { return this.app.toCanvasCoordinates(p); } /** * 保存创建的图形对象 */ storeGraphic(...graphics) { this.app.addGraphicAndRecord(...graphics); } /** * 创建并添加到图形App */ createAndStore(finish) { const data = this.graphicTemplate.datas; data.id = this.nextId(); data.graphicType = this.graphicTemplate.type; if (!this.prepareData(data)) { if (finish) { this.finish(); } return null; } const template = this.graphicTemplate; const g = template.new(); g.loadData(data); this.storeGraphic(g); if (finish) { this.finish(g); } return g; } /** * 绘制完成 */ finish(...graphics) { this.clearCache(); this.app.interactionPlugin(CommonMouseTool.Name).resume(); this.app.updateSelected(...graphics); } } /** * 绘制应用 */ class JlDrawApp extends GraphicApp { font = BitmapFont.from('coordinates', { fontFamily: 'Roboto', fontSize: 16, fill: '#ff2700', }, { chars: ['画布坐标:() 屏幕坐标:() 缩放:.,', ['0', '9']] }); coordinates = new BitmapText('画布坐标: (0, 0) 屏幕坐标:(0, 0)', { fontName: 'coordinates', }); scaleText = new BitmapText('缩放: 1', { fontName: 'coordinates', }); drawAssistants = []; _drawing = false; debouncedFormDataUpdator; get drawing() { return this._drawing; } set drawing(value) { this._drawing = value; } constructor(options) { super(options); this.appendDrawStatesDisplay(); // 拖拽操作记录 this.appOperationRecord(); // 绑定通用键盘操作 this.bindKeyboardOperation(); this.formDataSyncListen(); this.debouncedFormDataUpdator = debounce(this.doFormDataUpdate, 60); } setOptions(options) { super.setOptions(options); } registerInteractionPlugin(...plugins) { plugins.forEach((plugin) => { if (plugin instanceof GraphicDrawAssistant) { this.drawAssistants.push(plugin); } super.registerInteractionPlugin(plugin); }); } getDrawAssistant(graphicType) { const sda = this.drawAssistants .filter((da) => da.type === graphicType) .pop(); if (!sda) { throw new Error(`未找到图形绘制助手: ${graphicType}`); } return sda; } appOperationRecord() { let dragStartDatas = []; this.on('drag_op_start', (e) => { // 图形拖拽,记录初始数据 if (!e.target.isCanvas()) { dragStartDatas = this.selectedGraphics.map((g) => g.saveData()); } }); // 图形拖拽操作监听 this.on('drag_op_end', () => { // 图形拖拽,记录操作 if (dragStartDatas.length > 0) { const newData = this.selectedGraphics.map((g) => g.saveData()); this.opRecord.record(new GraphicDataUpdateOperation(this, this.selectedGraphics, dragStartDatas, newData)); dragStartDatas = []; } }); // 菜单操作 let preMenuHandleDatas = []; this.on('pre-menu-handle', (menu) => { if (menu.name === '撤销' || menu.name === '重做') { return; } preMenuHandleDatas = this.selectedGraphics.map((g) => g.saveData()); }); this.on('post-menu-handle', () => { if (preMenuHandleDatas.length > 0) { const newData = this.selectedGraphics.map((g) => g.saveData()); this.opRecord.record(new GraphicDataUpdateOperation(this, this.selectedGraphics, preMenuHandleDatas, newData)); preMenuHandleDatas = []; } }); } /** * 绘制状态信息显示 */ appendDrawStatesDisplay() { this.pixi.stage.addChild(this.coordinates); this.pixi.stage.addChild(this.scaleText); const bound = this.coordinates.getLocalBounds(); this.scaleText.position.set(bound.width + 10, 0); this.canvas.on('mousemove', (e) => { if (e.target) { const { x, y } = this.toCanvasCoordinates(e.global); const cpTxt = `(${x}, ${y})`; const tp = e.global; const tpTxt = `(${tp.x}, ${tp.y})`; this.coordinates.text = `画布坐标:${cpTxt} 屏幕坐标:${tpTxt}`; const bound = this.coordinates.getLocalBounds(); this.scaleText.position.set(bound.width + 10, 0); } }); this.viewport.on('zoomed-end', () => { this.scaleText.text = `缩放: ${this.viewport.scaled}`; }); } bindKeyboardOperation() { this.addKeyboardListener( // Ctrl + A new KeyListener({ value: 'KeyA', combinations: [CombinationKey.Ctrl], onPress: (e, app) => { if (e.ctrlKey) { app.selectAllGraphics(); } }, })); // 复制功能 this.addKeyboardListener(new KeyListener({ value: 'KeyD', combinations: [CombinationKey.Shift], onPress: (e, app) => { this.graphicCopyPlugin.init(); }, })); this.addKeyboardListener(new KeyListener({ // Ctrl + Z value: 'KeyZ', global: true, combinations: [CombinationKey.Ctrl], onPress: (e, app) => { app.opRecord.undo(); }, })); this.addKeyboardListener(new KeyListener({ // Ctrl + Shift + Z value: 'KeyZ', global: true, combinations: [CombinationKey.Ctrl, CombinationKey.Shift], onPress: (e, app) => { app.opRecord.redo(); }, })); this.addKeyboardListener(new KeyListener({ value: 'Delete', onPress: (e, app) => { app.deleteGraphicAndRecord(...app.selectedGraphics); }, })); this.addKeyboardListener(new KeyListener({ value: 'ArrowUp', pressTriggerAsOriginalEvent: true, onPress: (e, app) => { updateGraphicPositionOnKeyboardEvent(app, UP); }, onRelease: (e, app) => { recordGraphicMoveOperation(app); }, })); this.addKeyboardListener(new KeyListener({ value: 'ArrowDown', pressTriggerAsOriginalEvent: true, onPress: (e, app) => { updateGraphicPositionOnKeyboardEvent(app, DOWN); }, onRelease: (e, app) => { recordGraphicMoveOperation(app); }, })); this.addKeyboardListener(new KeyListener({ value: 'ArrowLeft', pressTriggerAsOriginalEvent: true, onPress: (e, app) => { updateGraphicPositionOnKeyboardEvent(app, LEFT); }, onRelease: (e, app) => { recordGraphicMoveOperation(app); }, })); this.addKeyboardListener(new KeyListener({ value: 'ArrowRight', pressTriggerAsOriginalEvent: true, onPress: (e, app) => { updateGraphicPositionOnKeyboardEvent(app, RIGHT); }, onRelease: (e, app) => { recordGraphicMoveOperation(app); }, })); } /** * 图形对象存储处理,默认添加图形交互 * @param graphic */ beforeGraphicStore(graphic) { graphic.eventMode = 'static'; graphic.selectable = true; graphic.draggable = true; graphic.on('repaint', () => { this.handleFormDataUpdate(graphic); }); graphic.on('transformend', () => { this.handleFormDataUpdate(graphic); }); } formData = undefined; /** * 绑定form表单对象 * @param form */ bindFormData(form) { this.formData = form; if (this.formData && this.selectedGraphics.length == 1) { if (this.formData.graphicType == this.selectedGraphics[0].type) { this.formData.copyFrom(this.selectedGraphics[0].saveData()); } else { this.formData = undefined; } } } /** * 移除form绑定 * @param form */ unbindFormData(form) { if (this.formData == form) { this.formData = undefined; } } formDataSyncListen() { this.on('graphicselected', () => { if (this.selectedGraphics.length == 1) { this.handleFormDataUpdate(this.selectedGraphics[0]); } }); } /** * 处理表单数据更新(使用debounce限流) */ handleFormDataUpdate(g) { this.debouncedFormDataUpdator(this, g); } doFormDataUpdate(g) { if (this.selectedGraphics.length > 1) return; if (this.formData && g.type === this.formData.graphicType) { this.formData.copyFrom(g.saveData()); } } updateCanvasAndRecord(data) { const old = this.canvas.properties.clone(); this.canvas.update(data); const newVal = this.canvas.properties.clone(); this.opRecord.record(new UpdateCanvasOperation(this, this.canvas, old, newVal)); } updateGraphicAndRecord(g, data) { const old = g.saveData(); g.updateData(data); const newVal = g.saveData(); this.opRecord.record(new GraphicDataUpdateOperation(this, [g], [old], [newVal])); } } let dragStartDatas = []; function handleArrowKeyMoveGraphics(app, handler) { if (app.selectedGraphics.length === 1 && app.selectedGraphics[0].hasSelectedChilds()) { recursiveChildren(app.selectedGraphics[0], (child) => { if (child.selected && child.draggable) { handler(child); } }); } else { app.selectedGraphics.forEach((g) => { handler(g); }); } } function updateGraphicPositionOnKeyboardEvent(app, dp) { let dragStart = false; if (dragStartDatas.length === 0) { dragStartDatas = app.selectedGraphics.map((g) => g.saveData()); dragStart = true; } handleArrowKeyMoveGraphics(app, (g) => { if (dragStart) { g.shiftStartPoint = g.position.clone(); g.emit('transformstart', GraphicTransformEvent.shift(g, ShiftData.new(g.shiftStartPoint))); } else { g.shiftLastPoint = g.position.clone(); } g.position.x += dp.x; g.position.y += dp.y; if (!dragStart) { if (g.shiftStartPoint && g.shiftLastPoint) { g.emit('transforming', GraphicTransformEvent.shift(g, ShiftData.new(g.shiftStartPoint, g.position.clone(), g.shiftLastPoint))); } } }); } function recordGraphicMoveOperation(app) { if (dragStartDatas.length > 0 && dragStartDatas.length === app.selectedGraphics.length) { const newData = app.selectedGraphics.map((g) => g.saveData()); app.opRecord.record(new GraphicDataUpdateOperation(app, app.selectedGraphics, dragStartDatas, newData)); handleArrowKeyMoveGraphics(app, (g) => { if (g.shiftStartPoint) { g.emit('transformend', GraphicTransformEvent.shift(g, ShiftData.new(g.shiftStartPoint, g.position.clone()))); } }); } dragStartDatas = []; } /** * 实例化图形app * @param options * @returns */ function newGraphicApp(options) { return new GraphicApp(options); } /** * 实例化绘图app * @param options * @returns */ function newDrawApp(options) { return new JlDrawApp(options); } export { AbsorbableCircle, AbsorbableLine, AbsorbablePoint, AbsorbablePointParam, AnimationManager, AppConsts, AppDragEvent, AppInteractionPlugin, AppWsMsgBroker, BezierCurveEditPlugin, BoundsGraphic, ChildTransform, ClientEngine, CombinationKey, CommonMouseTool, ContextMenu, ContextMenuPlugin, DOWN, DashedLine, DefaultWhiteMenuOptions, DefaultWhiteStyleOptions, DragPlugin, DraggablePoint, DraggablePointParam, GlobalKeyboardHelper, GraphicAnimation, GraphicCopyPlugin, GraphicDataUpdateOperation, GraphicDrawAssistant, GraphicEditPlugin, GraphicIdGenerator, GraphicInteractionPlugin, GraphicRelation, GraphicRelationParam, GraphicStore, GraphicTransform, GraphicTransformEvent, GraphicTransformPlugin, IdGenerator, InteractionPluginBase, InteractionPluginType, JlGraphic, JlGraphicAppKeyboardPlugin, JlGraphicTemplate, JlOperation, KeyListener, LEFT, LineEditPlugin, MessageClient, MqttMsgClient, OperationRecord, OtherInteractionPlugin, OutOfBound, PolylineEditPlugin, RIGHT, RelationManage, ScaleData, ShiftData, StompMessagingClient, SubscriptionClient, TransformPoints, UP, Vector2, VectorGraphicUtil, VectorText, ViewportMovePlugin, WsMsgCli, addBezierWayPoint, addLineWayPoint, addPolygonSegmentingPoint, addWayPoint, angleOfIncludedAngle, angleToAxisx, assertBezierPoints, calculateBezierPoints, calculateDistanceFromPointToLine, calculateFootPointFromPointToLine, calculateIntersectionPointOfCircleAndLine, calculateIntersectionPointOfCircleAndPoint, calculateLineMidpoint, calculateLineSegmentingPoint, calculateMirrorPoint, calculateMirrorPointBasedOnAxis, calculateOneBezierPoints, circlePoint, circlePoint2, clearWayPoint, convertLineToPolygonPoints, convertRectangleToPolygonPoints, convertToBezierParams, debounce, deserializeTransformInto, distance, distance2, epsilon, floatEquals, getCenterOfTwoRectangle, getIntersectionPoint, getNormalVector, getParallelOfPolyline, getRectangleCenter, getWayLineIndex, getWaypointRangeIndex, isLineContainOther, isParallelLines, isPointOnLine, isZero, lineBox, lineLine, linePoint, linePolygon, movePointAlongNormal, newDrawApp, newGraphicApp, pointBox, pointPoint, pointPoint2, pointPolygon, polylineBox, polylinePoint, polylinePolygon, recursiveChildren, recursiveFindChild, recursiveFindParent, recursiveParents, removeBezierWayPoint, removeLineWayPoint, removeWayPoint, serializeTransform, splitLineEvenly, splitPolyline };