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