graphic-pixi/lib/index.js
joylink_fanyuhong 2a2a4ce488 打包
2024-10-14 09:40:45 +08:00

7815 lines
234 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<key.code|key.key|key.keyCode, Map<KeyListener.identifier, KeyListener>>
*/
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 支持keyCodekeycode三种值
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 };