2023-12-13 10:39:56 +08:00
|
|
|
|
import * as graphicsExtras from '@pixi/graphics-extras';
|
|
|
|
|
export { graphicsExtras as GraphicsExtras };
|
2023-12-13 10:29:16 +08:00
|
|
|
|
import { Graphics, Container, Text, Point, Matrix, Color, Polygon, DisplayObject, Rectangle, Application, BitmapFont, BitmapText } from 'pixi.js';
|
2023-12-12 17:31:07 +08:00
|
|
|
|
import EventEmitter from 'eventemitter3';
|
|
|
|
|
import { Viewport } from 'pixi-viewport';
|
|
|
|
|
import { Client } from '@stomp/stompjs';
|
|
|
|
|
import mqtt from 'mqtt';
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ID生成器
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class IdGenerator {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
serial = 0;
|
|
|
|
|
type;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(type) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.type = type;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
next() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
++this.serial;
|
|
|
|
|
return this.getType() + this.serial;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
getType() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.type;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
initSerial(serial) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.serial = serial;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const GraphicIdGenerator = new IdGenerator('');
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 图形动画管理
|
|
|
|
|
*/
|
|
|
|
|
class AnimationManager {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
app;
|
|
|
|
|
_pause;
|
|
|
|
|
/**
|
|
|
|
|
* key - graphic.id
|
|
|
|
|
*/
|
|
|
|
|
graphicAnimationMap;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-12 17:31:07 +08:00
|
|
|
|
var InteractionPluginType;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
(function (InteractionPluginType) {
|
|
|
|
|
InteractionPluginType["App"] = "app";
|
|
|
|
|
InteractionPluginType["Graphic"] = "graphic";
|
|
|
|
|
InteractionPluginType["Other"] = "other";
|
2023-12-12 17:31:07 +08:00
|
|
|
|
})(InteractionPluginType || (InteractionPluginType = {}));
|
|
|
|
|
class InteractionPluginBase {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
_type;
|
|
|
|
|
name; // 唯一标识
|
|
|
|
|
app;
|
|
|
|
|
_pause;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(app, name, type) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this._type = type;
|
|
|
|
|
this.app = app;
|
|
|
|
|
this.name = name;
|
|
|
|
|
this._pause = true;
|
|
|
|
|
app.registerInteractionPlugin(this);
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 恢复
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
resume() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.bind();
|
|
|
|
|
this._pause = false;
|
|
|
|
|
this.app.emit('interaction-plugin-resume', this);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 停止
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
pause() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.unbind();
|
|
|
|
|
this._pause = true;
|
|
|
|
|
this.app.emit('interaction-plugin-pause', this);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 是否生效
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
isActive() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return !this._pause;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
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 {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
app;
|
|
|
|
|
type;
|
|
|
|
|
target;
|
|
|
|
|
original;
|
|
|
|
|
start; // 画布坐标
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(app, type, target, original, start) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.app = app;
|
|
|
|
|
this.type = type;
|
|
|
|
|
this.target = target;
|
|
|
|
|
this.original = original;
|
|
|
|
|
this.start = start;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 转换为目标对象的位移距离
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
toTargetShiftLen(target) {
|
|
|
|
|
const sl = target.canvasToLocalPoint(this.start);
|
|
|
|
|
const el = target.canvasToLocalPoint(this.end);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return { dx: el.x - sl.x, dy: el.y - sl.y };
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 拖拽操作插件
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class DragPlugin extends OtherInteractionPlugin {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
static Name = '__drag_operation_plugin';
|
|
|
|
|
threshold = 3;
|
|
|
|
|
target = null;
|
|
|
|
|
start = null;
|
|
|
|
|
startClientPoint = null;
|
|
|
|
|
drag = false;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(app) {
|
|
|
|
|
super(app, DragPlugin.Name);
|
|
|
|
|
app.on('options-update', (options) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (options.threshold !== undefined) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.threshold = options.threshold;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
static new(app) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new DragPlugin(app);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
bind() {
|
|
|
|
|
const canvas = this.app.canvas;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
canvas.on('pointerdown', this.onPointerDown, this);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
unbind() {
|
|
|
|
|
const canvas = this.app.canvas;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
canvas.off('pointerdown', this.onPointerDown, this);
|
|
|
|
|
canvas.off('pointerup', this.onPointerUp, this);
|
|
|
|
|
canvas.off('pointerupoutside', this.onPointerUp, this);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onPointerDown(e) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.target = e.target;
|
|
|
|
|
this.start = this.app.toCanvasCoordinates(e.global);
|
|
|
|
|
this.startClientPoint = e.global.clone();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const canvas = this.app.canvas;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
canvas.on('pointermove', this.onPointerMove, this);
|
|
|
|
|
canvas.on('pointerup', this.onPointerUp, this);
|
|
|
|
|
canvas.on('pointerupoutside', this.onPointerUp, this);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onPointerMove(e) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.start && this.startClientPoint) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const current = e.global;
|
|
|
|
|
const sgp = this.startClientPoint;
|
|
|
|
|
const dragStart = Math.abs(current.x - sgp.x) > this.threshold ||
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onPointerUp(e) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const ade = new AppDragEvent(this.app, 'end', this.target, e, this.start);
|
|
|
|
|
const graphic = this.target.getGraphic();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const canvas = this.app.canvas;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
canvas.off('mousemove', this.onPointerMove, this);
|
|
|
|
|
canvas.off('mouseup', this.onPointerUp, this);
|
|
|
|
|
canvas.off('mouseupoutside', this.onPointerUp, this);
|
|
|
|
|
this.clearCache();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 清理缓存
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
clearCache() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.drag = false;
|
|
|
|
|
this.start = null;
|
|
|
|
|
this.startClientPoint = null;
|
|
|
|
|
this.target = null;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 视口移动插件
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class ViewportMovePlugin extends OtherInteractionPlugin {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
static Name = '__viewport_move_plugin';
|
|
|
|
|
static MoveInterval = 20; // 移动间隔,单位ms
|
|
|
|
|
static TriggerRange = 100; // 边界触发范围,单位px
|
|
|
|
|
static DefaultMoveSpeed = 200 / ViewportMovePlugin.MoveInterval; // 默认移动速度
|
|
|
|
|
moveHandler = null;
|
|
|
|
|
moveSpeedx = 0;
|
|
|
|
|
moveSpeedy = 0;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(app) {
|
|
|
|
|
super(app, ViewportMovePlugin.Name);
|
|
|
|
|
}
|
|
|
|
|
static new(app) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new ViewportMovePlugin(app);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
pause() {
|
|
|
|
|
super.pause();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.stopMove();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
bind() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.app.canvas.on('pointermove', this.viewportMove, this);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
unbind() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.app.canvas.off('pointermove', this.viewportMove, this);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
startMove(moveSpeedx, moveSpeedy) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.moveSpeedx = moveSpeedx;
|
|
|
|
|
this.moveSpeedy = moveSpeedy;
|
|
|
|
|
if (this.moveHandler == null) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const viewport = this.app.viewport;
|
|
|
|
|
this.moveHandler = setInterval(() => {
|
|
|
|
|
viewport.moveCorner(viewport.corner.x + this.moveSpeedx, viewport.corner.y + this.moveSpeedy);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}, ViewportMovePlugin.MoveInterval);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
stopMove() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.moveHandler != null) {
|
|
|
|
|
clearInterval(this.moveHandler);
|
|
|
|
|
this.moveHandler = null;
|
|
|
|
|
this.app.canvas.cursor = 'auto';
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
calculateBoundaryMoveSpeed(sp) {
|
|
|
|
|
let moveSpeedx = 0;
|
|
|
|
|
let moveSpeedy = 0;
|
|
|
|
|
const range = ViewportMovePlugin.TriggerRange;
|
|
|
|
|
const viewport = this.app.viewport;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
return { moveSpeedx, moveSpeedy };
|
|
|
|
|
}
|
|
|
|
|
calculateMoveSpeed(dd) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return ((dd / ViewportMovePlugin.TriggerRange) *
|
|
|
|
|
ViewportMovePlugin.DefaultMoveSpeed);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
viewportMove(e) {
|
|
|
|
|
const sp = e.global;
|
|
|
|
|
const { moveSpeedx, moveSpeedy } = this.calculateBoundaryMoveSpeed(sp);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (moveSpeedx == 0 && moveSpeedy == 0) {
|
|
|
|
|
this.app.canvas.cursor = 'auto';
|
|
|
|
|
this.stopMove();
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
this.app.canvas.cursor = 'grab';
|
|
|
|
|
this.startMove(moveSpeedx, moveSpeedy);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 应用交互插件,同时只能生效一个
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class AppInteractionPlugin extends InteractionPluginBase {
|
|
|
|
|
constructor(name, app) {
|
|
|
|
|
super(app, name, InteractionPluginType.App);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 恢复,app交互插件同时只能生效一个
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
resume() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.app.pauseAppInteractionPlugins();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
super.resume();
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 图形交互插件,可同时生效
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class GraphicInteractionPlugin {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
_type = InteractionPluginType.Graphic;
|
|
|
|
|
app;
|
|
|
|
|
name; // 唯一标识
|
|
|
|
|
_pause;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(name, app) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.app = app;
|
|
|
|
|
this.name = name;
|
|
|
|
|
this._pause = true;
|
|
|
|
|
app.registerInteractionPlugin(this);
|
|
|
|
|
this.resume();
|
|
|
|
|
// 新增的图形对象绑定
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.app.on('graphicstored', (g) => {
|
|
|
|
|
if (this.isActive()) {
|
|
|
|
|
this.binds(this.filter(g));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.app.on('graphicdeleted', (g) => {
|
|
|
|
|
if (this.isActive()) {
|
|
|
|
|
this.unbinds(this.filter(g));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
isActive() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return !this._pause;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
isAppPlugin() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return false;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
isOtherPlugin() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return false;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
isGraphicPlugin() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return true;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
resume() {
|
|
|
|
|
const list = this.filter(...this.app.queryStore.getAllGraphics());
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.binds(list);
|
|
|
|
|
this._pause = false;
|
|
|
|
|
this.app.emit('interaction-plugin-resume', this);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
pause() {
|
|
|
|
|
const list = this.filter(...this.app.queryStore.getAllGraphics());
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.unbinds(list);
|
|
|
|
|
this._pause = true;
|
|
|
|
|
this.app.emit('interaction-plugin-pause', this);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
binds(list) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (list) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
list.forEach((g) => this.bind(g));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
unbinds(list) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (list) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
list.forEach((g) => this.unbind(g));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class CompleteMouseToolOptions {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
boxSelect;
|
|
|
|
|
viewportDrag;
|
|
|
|
|
viewportDragLeft;
|
|
|
|
|
wheelZoom;
|
|
|
|
|
selectFilter;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.boxSelect = true;
|
|
|
|
|
this.viewportDrag = true;
|
|
|
|
|
this.wheelZoom = true;
|
|
|
|
|
this.viewportDragLeft = false;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
update(options) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 通用交互工具
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class CommonMouseTool extends AppInteractionPlugin {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
static Name = 'mouse-tool';
|
|
|
|
|
static SelectBox = '__select_box';
|
|
|
|
|
options;
|
|
|
|
|
box;
|
|
|
|
|
leftDownTarget = null;
|
|
|
|
|
drag = false;
|
|
|
|
|
graphicSelect = false;
|
|
|
|
|
rightTarget = null;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (options.mouseToolOptions) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.options.update(options.mouseToolOptions);
|
|
|
|
|
if (this.isActive()) {
|
|
|
|
|
this.pause();
|
|
|
|
|
this.resume();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
static new(app) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new CommonMouseTool(app);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
bind() {
|
|
|
|
|
const canvas = this.app.canvas;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
unbind() {
|
|
|
|
|
const canvas = this.app.canvas;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 确保所有事件取消监听
|
|
|
|
|
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();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onDragStart(event) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.boxSelect && event.target.isCanvas() && event.isLeftButton) {
|
|
|
|
|
this.box.visible = true;
|
|
|
|
|
this.app.interactionPlugin(ViewportMovePlugin.Name).resume();
|
|
|
|
|
}
|
|
|
|
|
this.drag = true;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onDragMove(event) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.boxSelect && event.target.isCanvas()) {
|
|
|
|
|
this.boxSelectDraw(event.start, event.end);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onDragEnd(event) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
setLeftCursor(e) {
|
|
|
|
|
const target = e.target;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.leftDownTarget = target;
|
|
|
|
|
if (target.isCanvas() && this.app.pixi.view.style) {
|
|
|
|
|
this.app.pixi.view.style.cursor = 'grab';
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
resumeLeftCursor() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.leftDownTarget &&
|
|
|
|
|
this.leftDownTarget.isCanvas() &&
|
|
|
|
|
this.app.pixi.view.style) {
|
|
|
|
|
this.app.pixi.view.style.cursor = 'inherit';
|
|
|
|
|
}
|
|
|
|
|
this.leftDownTarget = null;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
setCursor(e) {
|
|
|
|
|
const target = e.target;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.rightTarget = target;
|
|
|
|
|
if (target.isCanvas() && this.app.pixi.view.style) {
|
|
|
|
|
this.app.pixi.view.style.cursor = 'grab';
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
resumeCursor() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.rightTarget &&
|
|
|
|
|
this.rightTarget.isCanvas() &&
|
|
|
|
|
this.app.pixi.view.style) {
|
|
|
|
|
this.app.pixi.view.style.cursor = 'inherit';
|
|
|
|
|
}
|
|
|
|
|
this.rightTarget = null;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onMouseDown(e) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.leftDownTarget = e.target;
|
|
|
|
|
this.graphicSelect = false;
|
|
|
|
|
// 图形
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const graphic = this.leftDownTarget.getGraphic();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (graphic) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const app = this.app;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 图形选中
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 选中处理
|
|
|
|
|
* @param e
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
onMouseUp(e) {
|
|
|
|
|
const app = this.app;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!this.drag) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const target = e.target;
|
|
|
|
|
const graphic = e.target.getGraphic();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 多个图形选中,退出子元素编辑模式
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const selecteds = this.app.selectedGraphics;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (selecteds.length > 1) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
selecteds.forEach((g) => g.exitChildEdit());
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.clearCache();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 清理缓存
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
clearCache() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.drag = false;
|
|
|
|
|
this.leftDownTarget = null;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
get boxSelect() {
|
|
|
|
|
return this.options.boxSelect;
|
|
|
|
|
}
|
|
|
|
|
get selectFilter() {
|
|
|
|
|
return this.options.selectFilter;
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 框选图形绘制并检查
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
boxSelectDraw(start, end) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!this.drag)
|
|
|
|
|
return;
|
|
|
|
|
// 绘制框选矩形框
|
|
|
|
|
this.box.clear();
|
|
|
|
|
this.box.lineStyle({
|
|
|
|
|
width: 2,
|
|
|
|
|
color: this.app.appOptions.assistantElementColor ||
|
|
|
|
|
AppConsts.assistantElementColor,
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const dsx = end.x - start.x;
|
|
|
|
|
const dsy = end.y - start.y;
|
|
|
|
|
let { x, y } = start;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (dsx < 0) {
|
|
|
|
|
x += dsx;
|
|
|
|
|
}
|
|
|
|
|
if (dsy < 0) {
|
|
|
|
|
y += dsy;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const width = Math.abs(dsx);
|
|
|
|
|
const height = Math.abs(dsy);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.box.drawRect(x, y, width, height);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 框选图形判断
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
boxSelectGraphicCheck() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!this.drag)
|
|
|
|
|
return;
|
|
|
|
|
// 遍历筛选
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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))) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 选择过滤器
|
|
|
|
|
if (g.boxIntersectCheck(boxRect)) {
|
|
|
|
|
selects.push(g);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
app.updateSelected(...selects);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
2023-12-12 17:31:07 +08:00
|
|
|
|
let target;
|
|
|
|
|
class GlobalKeyboardHelper {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
appKeyboardPluginMap = [];
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor() {
|
|
|
|
|
window.onkeydown = (e) => {
|
|
|
|
|
this.appKeyboardPluginMap.forEach((plugin) => {
|
|
|
|
|
const listenerMap = plugin.getKeyListener(e);
|
2023-12-13 13:29:51 +08:00
|
|
|
|
listenerMap?.forEach((listener) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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;
|
|
|
|
|
};
|
2023-12-12 17:31:07 +08:00
|
|
|
|
window.onkeyup = (e) => {
|
|
|
|
|
this.appKeyboardPluginMap.forEach((plugin) => {
|
|
|
|
|
const listenerMap = plugin.getKeyListener(e);
|
2023-12-13 13:29:51 +08:00
|
|
|
|
listenerMap?.forEach((listener) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (listener.global) {
|
|
|
|
|
listener.release(e, plugin.app);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
registerGAKPlugin(plugin) {
|
|
|
|
|
if (!this.appKeyboardPluginMap.find((pg) => pg == plugin)) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.appKeyboardPluginMap.push(plugin);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
removeGAKPlugin(plugin) {
|
|
|
|
|
const index = this.appKeyboardPluginMap.findIndex((pg) => pg == plugin);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (index >= 0) {
|
|
|
|
|
this.appKeyboardPluginMap.splice(index, 1);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const GlobalKeyboardPlugin = new GlobalKeyboardHelper();
|
|
|
|
|
class JlGraphicAppKeyboardPlugin {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
app;
|
|
|
|
|
/**
|
|
|
|
|
* 结构为Map<key.code|key.key|key.keyCode, Map<KeyListener.identifier, KeyListener>>
|
|
|
|
|
*/
|
|
|
|
|
keyListenerMap = new Map(); // 键值监听map
|
|
|
|
|
keyListenerStackMap = new Map(); // 键值监听栈(多次注册相同的监听会把之前注册的监听器入栈,移除最新的监听会从栈中弹出一个作为指定事件监听处理器)
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(app) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.app = app;
|
|
|
|
|
GlobalKeyboardPlugin.registerGAKPlugin(this);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const onMouseUpdateTarget = (e) => {
|
|
|
|
|
const node = e.target;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
target = node;
|
|
|
|
|
};
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const keydownHandle = (e) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// console.debug(e.key, e.code, e.keyCode);
|
2023-12-13 13:29:51 +08:00
|
|
|
|
if (target && target == this.app.dom?.getElementsByTagName('canvas')[0]) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const listenerMap = this.getKeyListener(e);
|
2023-12-13 13:29:51 +08:00
|
|
|
|
listenerMap?.forEach((listener) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!listener.global) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
listener.press(e, this.app);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const keyupHandle = (e) => {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
if (target && target == this.app.dom?.getElementsByTagName('canvas')[0]) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const listenerMap = this.getKeyListener(e);
|
2023-12-13 13:29:51 +08:00
|
|
|
|
listenerMap?.forEach((listener) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!listener.global) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
listener.release(e, this.app);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener('mousedown', onMouseUpdateTarget, false);
|
|
|
|
|
document.addEventListener('keydown', keydownHandle, false);
|
|
|
|
|
document.addEventListener('keyup', keyupHandle, false);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.app.on('destroy', () => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
document.removeEventListener('mousedown', onMouseUpdateTarget, false);
|
|
|
|
|
document.removeEventListener('keydown', keydownHandle, false);
|
|
|
|
|
document.removeEventListener('keyup', keyupHandle, false);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
GlobalKeyboardPlugin.removeGAKPlugin(this);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
getOrInit(key) {
|
|
|
|
|
let map = this.keyListenerMap.get(key);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (map === undefined) {
|
|
|
|
|
map = new Map();
|
|
|
|
|
this.keyListenerMap.set(key, map);
|
|
|
|
|
}
|
|
|
|
|
return map;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
getOrInitStack(key) {
|
|
|
|
|
let stack = this.keyListenerStackMap.get(key);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (stack === undefined) {
|
|
|
|
|
stack = [];
|
|
|
|
|
this.keyListenerStackMap.set(key, stack);
|
|
|
|
|
}
|
|
|
|
|
return stack;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 注册按键监听,若有旧的,旧的入栈
|
|
|
|
|
* @param keyListener
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
addKeyListener(keyListener) {
|
|
|
|
|
const map = this.getOrInit(keyListener.value);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 查询是否有旧的监听,若有入栈
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const old = map.get(keyListener.identifier);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (old) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const stack = this.getOrInitStack(keyListener.identifier);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
stack.push(old);
|
|
|
|
|
}
|
|
|
|
|
map.set(keyListener.identifier, keyListener);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 移除按键监听,若是当前注册的监听,尝试从栈中取出作为按键监听器,若是旧的,则同时移除栈中的监听
|
|
|
|
|
* @param keyListener
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
removeKeyListener(keyListener) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
keyListener.onRemove();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const map = this.getOrInit(keyListener.value);
|
|
|
|
|
const old = map.get(keyListener.identifier);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
map.delete(keyListener.identifier);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const stack = this.getOrInitStack(keyListener.identifier);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (old && old === keyListener) {
|
|
|
|
|
// 是旧的监听
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const listener = stack.pop();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (listener) {
|
|
|
|
|
map.set(keyListener.identifier, listener);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
// 移除栈中的
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const index = stack.findIndex((ls) => ls === keyListener);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (index >= 0) {
|
|
|
|
|
stack.splice(index, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
getKeyListenerBy(key) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.keyListenerMap.get(key);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
getKeyListener(e) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return (this.getKeyListenerBy(e.key) ||
|
|
|
|
|
this.getKeyListenerBy(e.code) ||
|
|
|
|
|
this.getKeyListenerBy(e.keyCode));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
isKeyListened(key) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.getOrInit(key).size > 0;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 获取所有注册监听的键值(组合键)
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
getAllListenedKeys() {
|
|
|
|
|
const keys = [];
|
|
|
|
|
this.keyListenerMap.forEach((v) => v.forEach((_listener, ck) => keys.push(ck)));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return keys;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
var CombinationKey;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
(function (CombinationKey) {
|
|
|
|
|
CombinationKey["Ctrl"] = "Ctrl";
|
|
|
|
|
CombinationKey["Alt"] = "Alt";
|
|
|
|
|
CombinationKey["Shift"] = "Shift";
|
2023-12-12 17:31:07 +08:00
|
|
|
|
})(CombinationKey || (CombinationKey = {}));
|
|
|
|
|
const DefaultKeyListenerOptions = {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
value: '',
|
|
|
|
|
combinations: [],
|
|
|
|
|
global: false,
|
|
|
|
|
onPress: undefined,
|
|
|
|
|
pressTriggerAsOriginalEvent: false,
|
|
|
|
|
onRelease: undefined,
|
|
|
|
|
};
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class KeyListener {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
// value 支持keyCode,key,code三种值
|
|
|
|
|
options;
|
|
|
|
|
isPress = false;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(options) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.options = Object.assign({}, DefaultKeyListenerOptions, options);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
static create(options) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new KeyListener(options);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
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) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 检查组合键是否匹配
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
checkCombinations(e) {
|
|
|
|
|
const cbs = this.combinations;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (cbs.length > 0) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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)))) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
return !e.altKey && !e.ctrlKey && !e.shiftKey;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
release(e, app) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.isPress) {
|
|
|
|
|
this.isPress = false;
|
|
|
|
|
if (this.onRelease) {
|
|
|
|
|
this.onRelease(e, app);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onRemove() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 重置按下状态
|
|
|
|
|
this.isPress = false;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 图形复制插件
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class GraphicCopyPlugin {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
container;
|
|
|
|
|
scene;
|
|
|
|
|
keyListeners;
|
|
|
|
|
copys;
|
|
|
|
|
start;
|
|
|
|
|
running = false;
|
|
|
|
|
moveLimit;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(scene) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.scene = scene;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.container = new Container();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.copys = [];
|
|
|
|
|
this.keyListeners = [];
|
|
|
|
|
this.keyListeners.push(new KeyListener({
|
|
|
|
|
// ESC 用于取消复制操作
|
|
|
|
|
value: 'Escape',
|
|
|
|
|
global: true,
|
|
|
|
|
// combinations: [CombinationKey.Ctrl],
|
2023-12-12 17:31:07 +08:00
|
|
|
|
onPress: () => {
|
|
|
|
|
this.cancle();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
},
|
|
|
|
|
}), new KeyListener({
|
|
|
|
|
// X 限制只能在x轴移动
|
|
|
|
|
value: 'KeyX',
|
|
|
|
|
global: true,
|
|
|
|
|
// combinations: [CombinationKey.Ctrl],
|
2023-12-12 17:31:07 +08:00
|
|
|
|
onPress: () => {
|
|
|
|
|
this.updateMoveLimit('x');
|
2023-12-12 15:29:52 +08:00
|
|
|
|
},
|
|
|
|
|
}), new KeyListener({
|
|
|
|
|
// Y 限制只能在y轴移动
|
|
|
|
|
value: 'KeyY',
|
|
|
|
|
global: true,
|
|
|
|
|
// combinations: [CombinationKey.Ctrl],
|
2023-12-12 17:31:07 +08:00
|
|
|
|
onPress: () => {
|
|
|
|
|
this.updateMoveLimit('y');
|
2023-12-12 15:29:52 +08:00
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
updateMoveLimit(limit) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.moveLimit === limit) {
|
|
|
|
|
this.moveLimit = undefined;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
this.moveLimit = limit;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
init() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
clone.repaint();
|
|
|
|
|
});
|
|
|
|
|
this.scene.canvas.on('mousemove', this.onPointerMove, this);
|
|
|
|
|
this.scene.canvas.on('mouseup', this.onFinish, this);
|
|
|
|
|
this.scene.canvas.on('rightup', this.cancle, this);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.keyListeners.forEach((kl) => {
|
|
|
|
|
this.scene.app.addKeyboardListener(kl);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
clear() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.keyListeners.forEach((kl) => {
|
|
|
|
|
this.scene.app.removeKeyboardListener(kl);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onPointerMove(e) {
|
|
|
|
|
const cp = this.scene.toCanvasCoordinates(e.global);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!this.start) {
|
|
|
|
|
this.start = cp;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
if (this.moveLimit === 'x') {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const dx = cp.x - this.start.x;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.container.position.x = dx;
|
|
|
|
|
this.container.position.y = 0;
|
|
|
|
|
}
|
|
|
|
|
else if (this.moveLimit === 'y') {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const dy = cp.y - this.start.y;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.container.position.x = 0;
|
|
|
|
|
this.container.position.y = dy;
|
|
|
|
|
}
|
|
|
|
|
else {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const dx = cp.x - this.start.x;
|
|
|
|
|
const dy = cp.y - this.start.y;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.container.position.x = dx;
|
|
|
|
|
this.container.position.y = dy;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onFinish() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
console.log('复制确认');
|
|
|
|
|
// 将图形添加到app
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.copys.forEach((g) => {
|
|
|
|
|
g.position.x += this.container.position.x;
|
|
|
|
|
g.position.y += this.container.position.y;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.scene.app.addGraphicAndRecord(...this.copys);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.scene.detectRelations();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.scene.updateSelected(...this.copys);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.clear();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
cancle() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
console.log('复制操作取消');
|
|
|
|
|
this.scene.canvas.removeChild(this.container);
|
|
|
|
|
this.clear();
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class VectorGraphicUtil {
|
|
|
|
|
static handle(obj) {
|
|
|
|
|
const vg = obj;
|
|
|
|
|
const onScaleChange = function (obj) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (vg.isParent(obj)) {
|
|
|
|
|
vg.updateOnScaled();
|
|
|
|
|
}
|
|
|
|
|
};
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const registerScaleChange = function registerScaleChange(obj) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!obj.scaledListenerOn) {
|
|
|
|
|
obj.scaledListenerOn = true;
|
|
|
|
|
obj.getGraphicApp().on('viewport-scaled', onScaleChange);
|
|
|
|
|
}
|
|
|
|
|
};
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const unregisterScaleChange = function unregisterScaleChange(obj) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
obj.scaledListenerOn = false;
|
|
|
|
|
obj.getGraphicApp().off('viewport-scaled', onScaleChange);
|
|
|
|
|
};
|
|
|
|
|
obj.onAddToCanvas = function onAddToCanvas() {
|
|
|
|
|
obj.updateOnScaled();
|
|
|
|
|
registerScaleChange(obj);
|
|
|
|
|
};
|
|
|
|
|
obj.onRemoveFromCanvas = function onRemoveFromCanvas() {
|
|
|
|
|
// console.debug('矢量图像onRemoveFromCanvas');
|
|
|
|
|
unregisterScaleChange(obj);
|
|
|
|
|
};
|
2023-12-12 17:31:07 +08:00
|
|
|
|
obj.on('added', (container) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (container.isInCanvas()) {
|
|
|
|
|
obj.onAddToCanvas(container.getCanvas());
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 矢量文字.实现原理:在缩放发生变化时,更新fontSize
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class VectorText extends Text {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
vectorFontSize = 8;
|
|
|
|
|
scaled = 1;
|
|
|
|
|
scaledListenerOn = false;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(text, style, canvas) {
|
|
|
|
|
super(text, style, canvas);
|
|
|
|
|
VectorGraphicUtil.handle(this);
|
|
|
|
|
}
|
|
|
|
|
updateOnScaled() {
|
|
|
|
|
const scaled = this.getAllParentScaled();
|
|
|
|
|
const scale = Math.max(scaled.x, scaled.y);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.style.fontSize = this.vectorFontSize * scale;
|
|
|
|
|
this.scale.set(1 / scale, 1 / scale);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 设置矢量文字的字体大小
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
setVectorFontSize(fontSize) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.vectorFontSize !== fontSize) {
|
|
|
|
|
this.vectorFontSize = fontSize;
|
|
|
|
|
this.updateOnScaled();
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 拖拽点参数
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const DraggablePointParam = {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
lineWidth: 1,
|
|
|
|
|
lineColor: 0x000000,
|
|
|
|
|
fillColor: 0xffffff,
|
|
|
|
|
radius: 5, // 半径
|
|
|
|
|
};
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const DraggablePointGraphic = new Graphics();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
DraggablePointGraphic.lineStyle(DraggablePointParam.lineWidth, DraggablePointParam.lineColor);
|
|
|
|
|
DraggablePointGraphic.beginFill(DraggablePointParam.fillColor);
|
|
|
|
|
DraggablePointGraphic.drawCircle(0, 0, DraggablePointParam.radius);
|
|
|
|
|
DraggablePointGraphic.endFill();
|
|
|
|
|
/**
|
|
|
|
|
* 拖拽点,用于更新图形属性
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class DraggablePoint extends Graphics {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
scaledListenerOn = false;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param point 画布坐标点
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.scale.set(1 / scale, 1 / scale);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 浮点数相等判断误差值
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const epsilon = 0.00001;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 判断浮点数是不是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 */
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class Vector2 {
|
|
|
|
|
constructor(values) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (values !== undefined) {
|
|
|
|
|
this.xy = values;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
static from(p) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new Vector2([p.x, p.y]);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 13:29:51 +08:00
|
|
|
|
values = new Float32Array(2);
|
|
|
|
|
static zero = new Vector2([0, 0]);
|
|
|
|
|
static one = new Vector2([1, 1]);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.values[index];
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
reset() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.x = 0;
|
|
|
|
|
this.y = 0;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
copy(dest) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!dest) {
|
|
|
|
|
dest = new Vector2();
|
|
|
|
|
}
|
|
|
|
|
dest.x = this.x;
|
|
|
|
|
dest.y = this.y;
|
|
|
|
|
return dest;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
negate(dest) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!dest) {
|
|
|
|
|
dest = this;
|
|
|
|
|
}
|
|
|
|
|
dest.x = -this.x;
|
|
|
|
|
dest.y = -this.y;
|
|
|
|
|
return dest;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
equals(vector, threshold = epsilon) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (Math.abs(this.x - vector.x) > threshold) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (Math.abs(this.y - vector.y) > threshold) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
length() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return Math.sqrt(this.squaredLength());
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
squaredLength() {
|
|
|
|
|
const x = this.x;
|
|
|
|
|
const y = this.y;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return x * x + y * y;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
add(vector) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.x += vector.x;
|
|
|
|
|
this.y += vector.y;
|
|
|
|
|
return this;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
subtract(vector) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.x -= vector.x;
|
|
|
|
|
this.y -= vector.y;
|
|
|
|
|
return this;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
multiply(vector) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.x *= vector.x;
|
|
|
|
|
this.y *= vector.y;
|
|
|
|
|
return this;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
divide(vector) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.x /= vector.x;
|
|
|
|
|
this.y /= vector.y;
|
|
|
|
|
return this;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
scale(value, dest) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!dest) {
|
|
|
|
|
dest = this;
|
|
|
|
|
}
|
|
|
|
|
dest.x *= value;
|
|
|
|
|
dest.y *= value;
|
|
|
|
|
return dest;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
normalize(dest) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!dest) {
|
|
|
|
|
dest = this;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
let length = this.length();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 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
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
static dot(vector, vector2) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return vector.x * vector2.x + vector.y * vector2.y;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 向量长度
|
|
|
|
|
* @param vector
|
|
|
|
|
* @param vector2
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
static distance(vector, vector2) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return Math.sqrt(this.squaredDistance(vector, vector2));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 向量长度平方
|
|
|
|
|
* @param vector
|
|
|
|
|
* @param vector2
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
static squaredDistance(vector, vector2) {
|
|
|
|
|
const x = vector2.x - vector.x;
|
|
|
|
|
const y = vector2.y - vector.y;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return x * x + y * y;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* v2->v1的方向的单位向量
|
|
|
|
|
* @param v1
|
|
|
|
|
* @param v2
|
|
|
|
|
* @param dest
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
static direction(v1, v2, dest) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!dest) {
|
|
|
|
|
dest = new Vector2();
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const x = v1.x - v2.x;
|
|
|
|
|
const y = v1.y - v2.y;
|
|
|
|
|
let length = Math.sqrt(x * x + y * y);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (length === 0) {
|
|
|
|
|
dest.x = 0;
|
|
|
|
|
dest.y = 0;
|
|
|
|
|
return dest;
|
|
|
|
|
}
|
|
|
|
|
length = 1 / length;
|
|
|
|
|
dest.x = x * length;
|
|
|
|
|
dest.y = y * length;
|
|
|
|
|
return dest;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
static mix(vector, vector2, time, dest) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!dest) {
|
|
|
|
|
dest = new Vector2();
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const x = vector.x;
|
|
|
|
|
const y = vector.y;
|
|
|
|
|
const x2 = vector2.x;
|
|
|
|
|
const y2 = vector2.y;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
dest.x = x + time * (x2 - x);
|
|
|
|
|
dest.y = y + time * (y2 - y);
|
|
|
|
|
return dest;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 向量加法
|
|
|
|
|
* @param vector
|
|
|
|
|
* @param vector2
|
|
|
|
|
* @param dest
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
static sum(vector, vector2, dest) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!dest) {
|
|
|
|
|
dest = new Vector2();
|
|
|
|
|
}
|
|
|
|
|
dest.x = vector.x + vector2.x;
|
|
|
|
|
dest.y = vector.y + vector2.y;
|
|
|
|
|
return dest;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 向量减法
|
|
|
|
|
* @param vector
|
|
|
|
|
* @param vector2
|
|
|
|
|
* @param dest
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
static difference(vector, vector2, dest) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!dest) {
|
|
|
|
|
dest = new Vector2();
|
|
|
|
|
}
|
|
|
|
|
dest.x = vector.x - vector2.x;
|
|
|
|
|
dest.y = vector.y - vector2.y;
|
|
|
|
|
return dest;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 向量乘法
|
|
|
|
|
* @param vector
|
|
|
|
|
* @param vector2
|
|
|
|
|
* @param dest
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
static product(vector, vector2, dest) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!dest) {
|
|
|
|
|
dest = new Vector2();
|
|
|
|
|
}
|
|
|
|
|
dest.x = vector.x * vector2.x;
|
|
|
|
|
dest.y = vector.y * vector2.y;
|
|
|
|
|
return dest;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 向量除法
|
|
|
|
|
* @param vector
|
|
|
|
|
* @param vector2
|
|
|
|
|
* @param dest
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
static quotient(vector, vector2, dest) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!dest) {
|
|
|
|
|
dest = new Vector2();
|
|
|
|
|
}
|
|
|
|
|
dest.x = vector.x / vector2.x;
|
|
|
|
|
dest.y = vector.y / vector2.y;
|
|
|
|
|
return dest;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 递归父节点执行逻辑
|
|
|
|
|
* @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) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
container.children.forEach((child) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
handler(child);
|
|
|
|
|
if (child.children) {
|
|
|
|
|
recursiveChildren(child, handler);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 递归子节点查询子节点对象
|
|
|
|
|
*/
|
|
|
|
|
function recursiveFindChild(container, finder) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
let result = null;
|
|
|
|
|
for (let i = 0; i < container.children.length; i++) {
|
|
|
|
|
const child = container.children[i];
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
throw new Error(`bezierCurve 数据错误: ${points}`);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 转换为贝塞尔曲线参数
|
|
|
|
|
* @param points
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function convertToBezierParams(points) {
|
|
|
|
|
assertBezierPoints(points);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
bps.push({
|
2023-12-12 17:31:07 +08:00
|
|
|
|
p1,
|
|
|
|
|
p2,
|
|
|
|
|
cp1,
|
|
|
|
|
cp2,
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return bps;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 根据分段数计算贝塞尔曲线所有点坐标
|
|
|
|
|
* @param basePoints
|
|
|
|
|
* @param segmentsCount
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function calculateBezierPoints(basePoints, segmentsCount) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const bps = convertToBezierParams(basePoints);
|
|
|
|
|
const points = [];
|
|
|
|
|
bps.forEach((bp) => {
|
|
|
|
|
points.push(...calculateOneBezierPoints(bp.p1, bp.p2, bp.cp1, bp.cp2, segmentsCount));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
|
|
|
|
return points;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 根据分段数计算贝塞尔曲线所有点坐标
|
|
|
|
|
* @param basePoints
|
|
|
|
|
* @param segmentsCount
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function calculateOneBezierPoints(p1, p2, cp1, cp2, segmentsCount) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
j = i / n;
|
|
|
|
|
dt = 1 - j;
|
|
|
|
|
dt2 = dt * dt;
|
|
|
|
|
dt3 = dt2 * dt;
|
|
|
|
|
t2 = j * j;
|
|
|
|
|
t3 = t2 * j;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
return points;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 计算矩形中点
|
|
|
|
|
*/
|
|
|
|
|
function getRectangleCenter(rectangle) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
return new Point(rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 计算两个矩形中心对齐的坐标, PS: 计算的是较大包围框的中心
|
|
|
|
|
* @param rect1
|
|
|
|
|
* @param rect2
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function getCenterOfTwoRectangle(rect1, rect2) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const x = Math.abs(rect1.width - rect2.width) / 2;
|
|
|
|
|
const y = Math.abs(rect1.height - rect2.height) / 2;
|
|
|
|
|
return new Point(x, y);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 序列化图形变换
|
|
|
|
|
* @param obj
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function serializeTransform(obj) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const position = obj.position;
|
|
|
|
|
const scale = obj.scale;
|
|
|
|
|
const angle = obj.angle;
|
|
|
|
|
const skew = obj.skew;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return [
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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),
|
2023-12-12 15:29:52 +08:00
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 转换矩形为多边形点坐标
|
|
|
|
|
* @param rect 矩形
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function convertRectangleToPolygonPoints(rect) {
|
|
|
|
|
return [
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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),
|
2023-12-12 15:29:52 +08:00
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 计算线段中点坐标
|
|
|
|
|
* @param p1
|
|
|
|
|
* @param p2
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function calculateLineMidpoint(p1, p2) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const x = (p1.x + p2.x) / 2;
|
|
|
|
|
const y = (p1.y + p2.y) / 2;
|
|
|
|
|
return new Point(x, y);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 计算线段细分坐标--线段分成几份
|
|
|
|
|
* @param p1
|
|
|
|
|
* @param p2
|
|
|
|
|
* @param knife
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function calculateLineSegmentingPoint(p1, p2, knife) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
return segmentingPoints;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 计算点到直线距离
|
|
|
|
|
* @param p1
|
|
|
|
|
* @param p2
|
|
|
|
|
* @param p
|
|
|
|
|
*/
|
|
|
|
|
function calculateDistanceFromPointToLine(p1, p2, p) {
|
|
|
|
|
// 求直线的一般方程参数ABC,直线的一般式方程AX+BY+C=0
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const A = p1.y - p2.y;
|
|
|
|
|
const B = p2.x - p1.x;
|
|
|
|
|
const C = p1.x * p2.y - p1.y * p2.x;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 计算点到直线垂直距离: d = |Ax+By+C|/sqrt(A*A+B*B),其中x,y为点坐标
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const dl = Math.abs(A * p.x + B * p.y + C) / Math.sqrt(A * A + B * B);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return dl;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 计算点到直线的垂足坐标
|
|
|
|
|
* @param p
|
|
|
|
|
* @param p1
|
|
|
|
|
* @param p2
|
|
|
|
|
*/
|
|
|
|
|
function calculateFootPointFromPointToLine(p1, p2, p) {
|
|
|
|
|
if (p1.x == p2.x && p1.y == p2.y) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
throw new Error(`直线两坐标点相等:${p1}`);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const k = -(((p1.x - p.x) * (p2.x - p1.x) + (p1.y - p.y) * (p2.y - p1.y)) /
|
2023-12-12 15:29:52 +08:00
|
|
|
|
(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)));
|
|
|
|
|
if (isZero(k)) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
return new Point(p.x, p.y);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const xf = k * (p2.x - p1.x) + p1.x;
|
|
|
|
|
const yf = k * (p2.y - p1.y) + p1.y;
|
|
|
|
|
return new Point(xf, yf);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 计算直线与圆的交点
|
|
|
|
|
* 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) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const distance = calculateDistanceFromPointToLine(p1, p2, p0);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (distance <= radius) {
|
|
|
|
|
// 有交点
|
|
|
|
|
// 计算垂点
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const pr = calculateFootPointFromPointToLine(p1, p2, p0);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (floatEquals(distance, radius)) {
|
|
|
|
|
// 切线
|
|
|
|
|
return [pr];
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const vpr = new Vector2([pr.x, pr.y]);
|
|
|
|
|
const vc = new Vector2([p0.x, p0.y]);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 计算直线单位向量
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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)];
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
// 无交点
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 计算圆心与圆心外一点与圆的交点(取圆心到点的向量与圆的交点)
|
|
|
|
|
* @param p0 圆心坐标
|
|
|
|
|
* @param radius 圆半径
|
|
|
|
|
* @param p 点坐标
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function calculateIntersectionPointOfCircleAndPoint(p0, radius, p) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (ve.equals(vecp)) {
|
|
|
|
|
return ip;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
throw new Error('计算圆心与圆心外一点与圆的交点逻辑错误');
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 计算点基于点的镜像点坐标
|
|
|
|
|
* @param bp 基准点
|
|
|
|
|
* @param p 待镜像的点坐标
|
|
|
|
|
* @param distance 镜像点到基准点的距离,默认为p到基准点的距离,即对称
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function calculateMirrorPoint(bp, p, distance) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const vbp = Vector2.from(bp);
|
|
|
|
|
const vp = Vector2.from(p);
|
|
|
|
|
const direction = Vector2.direction(vbp, vp);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (distance == undefined) {
|
|
|
|
|
distance = Vector2.distance(vbp, vp);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const vmp = Vector2.sum(vbp, direction.scale(distance));
|
|
|
|
|
return new Point(vmp.x, vmp.y);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 计算基于给定轴的给定点的镜像点坐标
|
|
|
|
|
* @param pa 给定轴线的坐标
|
|
|
|
|
* @param pb 给定轴线的坐标
|
|
|
|
|
* @param p 待镜像点坐标
|
|
|
|
|
* @param distance
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function calculateMirrorPointBasedOnAxis(pa, pb, p, distance) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const fp = calculateFootPointFromPointToLine(pa, pb, p);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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('一个点无法计算角度');
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const dx = Math.abs(p1.x - p2.x);
|
|
|
|
|
const dy = Math.abs(p1.y - p2.y);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const angle = (Math.atan2(dy, dx) * 180) / Math.PI;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const abAngle = angleToAxisx(pa, pb);
|
|
|
|
|
const acAngle = angleToAxisx(pa, pc);
|
|
|
|
|
let angle = acAngle - abAngle;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (angle < -180) {
|
|
|
|
|
angle = 360 + angle;
|
|
|
|
|
}
|
|
|
|
|
else if (angle > 180) {
|
|
|
|
|
angle = -(360 - angle);
|
|
|
|
|
}
|
|
|
|
|
return angle;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 计算两点连线的法向量
|
|
|
|
|
* @param point1
|
|
|
|
|
* @param point2
|
|
|
|
|
* @returns 单位法向量
|
|
|
|
|
*/
|
|
|
|
|
function getNormalVector(point1, point2) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const x1 = point1.x, y1 = point1.y;
|
|
|
|
|
const x2 = point2.x, y2 = point2.y;
|
2023-12-13 13:29:51 +08:00
|
|
|
|
const length = Math.sqrt((y2 - y1) ** 2 + (x2 - x1) ** 2);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return [(y2 - y1) / length, (x1 - x2) / length];
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 点延向量方向移动
|
|
|
|
|
* @param point
|
|
|
|
|
* @param normal 单位向量
|
|
|
|
|
* @param length 平移长度
|
|
|
|
|
* @returns 移动后的点
|
|
|
|
|
*/
|
|
|
|
|
function movePointAlongNormal(point, normal, length) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const newPoint = new Point(point.x + length * normal[0], point.y + length * normal[1]);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return newPoint;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 计算两组点各自组成直线的相交点(若两线平行 返回第一组坐标第一个点)
|
|
|
|
|
* @param line1 两点坐标列表
|
|
|
|
|
* @param line2 两点坐标列表
|
|
|
|
|
* @returns 相交点
|
|
|
|
|
*/
|
|
|
|
|
function getIntersectionPoint(line1, line2) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (denominator === 0) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
return new Point(a1, b1);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const x = ((a3 - a4) * (a2 * b1 - a1 * b2) - (a1 - a2) * (a4 * b3 - a3 * b4)) /
|
2023-12-12 15:29:52 +08:00
|
|
|
|
denominator;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const y = ((b3 - b4) * (b2 * a1 - b1 * a2) - (b1 - b2) * (b4 * a3 - b3 * a4)) /
|
2023-12-12 15:29:52 +08:00
|
|
|
|
-denominator;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
return new Point(x, y);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 是否平行线
|
|
|
|
|
* @param p1
|
|
|
|
|
* @param p2
|
|
|
|
|
* @param pa
|
|
|
|
|
* @param pb
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function isParallelLines(p1, p2, pa, pb) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const vle1 = Vector2.direction(Vector2.from(p1), Vector2.from(p2));
|
|
|
|
|
const vle2 = Vector2.direction(Vector2.from(pa), Vector2.from(pb));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const vp1 = Vector2.from(p1);
|
|
|
|
|
const vp2 = Vector2.from(p2);
|
|
|
|
|
const vp = Vector2.from(p);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (vp1.equals(vp) || vp2.equals(vp)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const vle = Vector2.direction(vp1, Vector2.from(p2));
|
|
|
|
|
const vpe = Vector2.direction(vp1, vp);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const [stepX, stepY] = [(p2.x - p1.x) / count, (p2.y - p1.y) / count];
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return Array(count)
|
|
|
|
|
.fill(1)
|
2023-12-12 17:31:07 +08:00
|
|
|
|
.map((_, i) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
lengths.push(len);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
counts[maxCountIndex] + intersection;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
return counts
|
2023-12-12 17:31:07 +08:00
|
|
|
|
.map((count, i) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return splitLineEvenly(points[i], points[i + 1], count);
|
|
|
|
|
})
|
|
|
|
|
.flat();
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
return splitLineEvenly(points[0], points[points.length - 1], count);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
function getParallelOfPolyline(points, offset, side) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const { PI, cos, acos } = Math;
|
|
|
|
|
const angleBase = side === 'L' ? -PI / 2 : PI / 2;
|
|
|
|
|
return points.map((p, i) => {
|
|
|
|
|
let baseUnitVec; //上一段的基准单位向量
|
|
|
|
|
let angle; //偏转角度
|
|
|
|
|
let len; //结合偏转角度的实际偏移量
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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 {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
angle = angleBase + theta / 2;
|
|
|
|
|
len = offset / cos(theta / 2);
|
|
|
|
|
baseUnitVec = Vector2.from(vp).normalize();
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
return new Matrix()
|
2023-12-12 15:29:52 +08:00
|
|
|
|
.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
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
function linePoint(pa, pb, p, lineWidth, exact = false) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!exact && lineWidth < 6) {
|
|
|
|
|
lineWidth = 6;
|
|
|
|
|
}
|
|
|
|
|
// 求直线的一般方程参数ABC,直线的一般式方程AX+BY+C=0
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const A = pa.y - pb.y;
|
|
|
|
|
const B = pb.x - pa.x;
|
|
|
|
|
const C = pa.x * pb.y - pa.y * pb.x;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 计算点到直线垂直距离: d = |Ax+By+C|/sqrt(A*A+B*B),其中x,y为点坐标
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const dl = Math.abs(A * p.x + B * p.y + C) / Math.sqrt(A * A + B * B);
|
|
|
|
|
const intersect = dl <= lineWidth / 2;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (intersect) {
|
|
|
|
|
// 距离在线宽范围内,再判断点是否超过线段两端点范围外(两端点外会有一点误差,两端点线宽一半半径的圆范围内)
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return da <= dl + dab && db <= dl + dab;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 折线与点碰撞
|
|
|
|
|
* @param points 折线端点列表
|
|
|
|
|
* @param p 点座标
|
|
|
|
|
* @param lineWidth 线宽
|
|
|
|
|
*/
|
|
|
|
|
function polylinePoint(points, p, lineWidth) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const len = points.length;
|
|
|
|
|
for (let i = 0; i < len - 1; i++) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return s >= 0 && s <= 1 && t >= 0 && t <= 1;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 点和矩形碰撞检测
|
|
|
|
|
* @param p 点作弊
|
|
|
|
|
* @param rect 矩形
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function pointBox(p, rect) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const { x, y, width, height } = rect;
|
|
|
|
|
const x2 = p.x;
|
|
|
|
|
const y2 = p.y;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
for (let i = 0; i < points.length - 1; i++) {
|
|
|
|
|
const p1 = points[i];
|
|
|
|
|
const p2 = points[i + 1];
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const x = x2 - x1;
|
|
|
|
|
const y = y2 - y1;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return x * x + y * y <= r1 * r1;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 圆和点的碰撞检测--不包括圆内部
|
|
|
|
|
*/
|
|
|
|
|
function circlePoint2(x1, y1, r1, x2, y2, tolerance) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const x = x2 - x1;
|
|
|
|
|
const y = y2 - y1;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return (x * x + y * y <= (r1 + tolerance) * (r1 + tolerance) &&
|
|
|
|
|
x * x + y * y >= (r1 - tolerance) * (r1 - tolerance));
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 点和多边形碰撞检测
|
|
|
|
|
*/
|
|
|
|
|
function pointPolygon(p, points, lineWidth) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const { x, y } = p;
|
|
|
|
|
const length = points.length;
|
|
|
|
|
let c = false;
|
|
|
|
|
let i, j;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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++) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
let p1, p2;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const length = points.length;
|
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
|
|
|
let pa, pb;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const length = polylinePoints.length;
|
|
|
|
|
for (let i = 0; i < length - 1; i++) {
|
|
|
|
|
const p1 = polylinePoints[i];
|
|
|
|
|
const p2 = polylinePoints[i + 1];
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (linePolygon(p1, p2, polygonPoints, polylineWidth, polygonWidth)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-12 17:31:07 +08:00
|
|
|
|
function debounce(fn, waitMs = 250) {
|
|
|
|
|
let timeoutId;
|
|
|
|
|
const debouncedFunction = function (context, ...args) {
|
|
|
|
|
const invokeFunction = function () {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const UP = new Point(0, -1);
|
|
|
|
|
const DOWN = new Point(0, 1);
|
|
|
|
|
const LEFT = new Point(-1, 0);
|
|
|
|
|
const RIGHT = new Point(1, 0);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 越界结果
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class OutOfBound {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
left;
|
|
|
|
|
top;
|
|
|
|
|
right;
|
|
|
|
|
bottom;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(left, top, right, bottom) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.left = left;
|
|
|
|
|
this.top = top;
|
|
|
|
|
this.right = right;
|
|
|
|
|
this.bottom = bottom;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new OutOfBound(left, top, right, bottom);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
static none() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new OutOfBound(false, false, false, false);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
static leftOut() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new OutOfBound(true, false, false, false);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
static topOut() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new OutOfBound(false, true, false, false);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
static rightOut() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new OutOfBound(false, false, true, false);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
static bottomOut() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new OutOfBound(false, false, false, true);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
static leftTopOut() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new OutOfBound(true, true, false, false);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
static rightBottomOut() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new OutOfBound(false, false, true, true);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
2023-12-12 17:31:07 +08:00
|
|
|
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 可吸附点图形参数
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const AbsorbablePointParam = {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
lineWidth: 1,
|
|
|
|
|
lineColor: '#000000',
|
|
|
|
|
fillColor: '#E77E0E',
|
|
|
|
|
radius: 5, // 半径
|
|
|
|
|
};
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const AbsorbablePointGraphic = new Graphics();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// AbsorbablePointGraphic.lineStyle(
|
|
|
|
|
// AbsorbablePointParam.lineWidth,
|
|
|
|
|
// AbsorbablePointParam.lineColor
|
|
|
|
|
// );
|
|
|
|
|
AbsorbablePointGraphic.beginFill(AbsorbablePointParam.fillColor);
|
|
|
|
|
AbsorbablePointGraphic.drawCircle(0, 0, AbsorbablePointParam.radius);
|
|
|
|
|
AbsorbablePointGraphic.endFill();
|
|
|
|
|
/**
|
|
|
|
|
* 可吸附线
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class AbsorbableLine extends Graphics {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
p1;
|
|
|
|
|
p2;
|
|
|
|
|
absorbRange;
|
|
|
|
|
_color = '#E77E0E';
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param p1 画布坐标
|
|
|
|
|
* @param p2 画布坐标
|
|
|
|
|
* @param absorbRange
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (other instanceof AbsorbableLine) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const contain = isLineContainOther({ p1: this.p1, p2: this.p2 }, { p1: other.p1, p2: other.p2 });
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return contain;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
compareTo(other) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (other instanceof AbsorbableLine) {
|
|
|
|
|
return distance2(this.p1, this.p2) - distance2(other.p1, other.p2);
|
|
|
|
|
}
|
|
|
|
|
throw new Error('非可吸附线');
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
redraw() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.clear();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.lineStyle(1, new Color(this._color));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.moveTo(this.p1.x, this.p1.y);
|
|
|
|
|
this.lineTo(this.p2.x, this.p2.y);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
tryAbsorb(...objs) {
|
|
|
|
|
for (let i = 0; i < objs.length; i++) {
|
|
|
|
|
const obj = objs[i];
|
|
|
|
|
const canvasPosition = obj.getPositionOnCanvas();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (linePoint(this.p1, this.p2, canvasPosition, this.absorbRange, true)) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const fp = calculateFootPointFromPointToLine(this.p1, this.p2, canvasPosition);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
obj.updatePositionByCanvasPosition(fp);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 可吸附圆
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class AbsorbableCircle extends Graphics {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
absorbRange;
|
|
|
|
|
p0;
|
|
|
|
|
radius;
|
|
|
|
|
_color = '#E77E0E';
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param p 画布坐标
|
|
|
|
|
* @param radius
|
|
|
|
|
* @param absorbRange
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(p, radius, absorbRange = 10) {
|
|
|
|
|
super();
|
|
|
|
|
this.p0 = new Point(p.x, p.y);
|
|
|
|
|
this.radius = radius;
|
|
|
|
|
this.absorbRange = absorbRange;
|
|
|
|
|
this.redraw();
|
|
|
|
|
}
|
|
|
|
|
isOverlapping(other) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (other instanceof AbsorbableCircle) {
|
|
|
|
|
return this.p0.equals(other.p0) && this.radius === other.radius;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
compareTo(other) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (other instanceof AbsorbableCircle) {
|
|
|
|
|
return this.absorbRange - other.absorbRange;
|
|
|
|
|
}
|
|
|
|
|
throw new Error('非可吸附圆');
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
redraw() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.clear();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.lineStyle(1, new Color(this._color));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.drawCircle(this.p0.x, this.p0.y, this.radius);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (len > this.radius - this.absorbRange &&
|
|
|
|
|
len < this.radius + this.absorbRange) {
|
|
|
|
|
// 吸附,计算直线与圆交点,更新对象坐标
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const p = calculateIntersectionPointOfCircleAndPoint(this.p0, this.radius, canvasPosition);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
obj.updatePositionByCanvasPosition(p);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const DefaultDashedLineOptions = {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
length: 4,
|
|
|
|
|
startSpace: 0,
|
|
|
|
|
space: 4,
|
|
|
|
|
lineWidth: 1,
|
|
|
|
|
color: '#0000ff',
|
|
|
|
|
};
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class DashedLine extends Container {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
p1;
|
|
|
|
|
p2;
|
|
|
|
|
_options;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
redraw() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.removeChildren();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
while (len < total) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
let dashedLen = option.length;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (len + option.length > total) {
|
|
|
|
|
dashedLen = total - len;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const line = new Graphics();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
2023-12-13 10:29:16 +08:00
|
|
|
|
class GraphicEditPlugin extends Container {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
graphic;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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 {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
linePoints;
|
|
|
|
|
editedPoints = [];
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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 {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
static Name = 'line_points_edit';
|
|
|
|
|
options;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
constructor(g, options) {
|
|
|
|
|
super(g);
|
|
|
|
|
this.options = Object.assign({}, options);
|
|
|
|
|
this.name = PolylineEditPlugin.Name;
|
|
|
|
|
this.initEditPoints();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
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('不能移除:剩余点数不足');
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
else if (i === linePoints.length - 1) {
|
|
|
|
|
// 最后一个点
|
|
|
|
|
if (linePoints.length > 4) {
|
|
|
|
|
points = linePoints.slice(0, linePoints.length - 3);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
console.error('无法移除:剩余点数不足');
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 贝塞尔曲线编辑
|
|
|
|
|
*/
|
|
|
|
|
class BezierCurveEditPlugin extends LineEditPlugin {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
static Name = 'bezier_curve_points_edit';
|
|
|
|
|
options;
|
|
|
|
|
// 曲线控制点辅助线
|
|
|
|
|
auxiliaryLines = [];
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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 {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
/**
|
|
|
|
|
* 起始位置
|
|
|
|
|
*/
|
|
|
|
|
startPosition;
|
|
|
|
|
/**
|
|
|
|
|
* 上一次终点位置
|
|
|
|
|
*/
|
|
|
|
|
lastPosition;
|
|
|
|
|
/**
|
|
|
|
|
* 当前位置
|
|
|
|
|
*/
|
|
|
|
|
currentPosition;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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 {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
start;
|
|
|
|
|
current;
|
|
|
|
|
last;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
constructor(start, current, last) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.start = start;
|
|
|
|
|
this.current = current;
|
|
|
|
|
this.last = last;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
static new(start, current, last) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new ScaleData(start, current, last);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 图形平移事件
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class GraphicTransformEvent {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
/**
|
|
|
|
|
* 图形对象
|
|
|
|
|
*/
|
|
|
|
|
target;
|
|
|
|
|
type;
|
|
|
|
|
data;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(target, type, data) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.target = target;
|
|
|
|
|
this.type = type;
|
|
|
|
|
this.data = data;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
getData() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.data;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
static shift(target, data) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new GraphicTransformEvent(target, 'shift', data);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
static scale(target) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new GraphicTransformEvent(target, 'scale', null);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
static rotate(target) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new GraphicTransformEvent(target, 'rotate', null);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
static skew(target) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new GraphicTransformEvent(target, 'skew', null);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
isShift() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.type === 'shift';
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
isRotate() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.type === 'rotate';
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
isScale() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.type === 'scale';
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
isSkew() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.type === 'skew';
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
class GraphicTransformPlugin extends InteractionPluginBase {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
static Name = '__graphic_transform_plugin';
|
|
|
|
|
/**
|
|
|
|
|
* 可吸附位置列表
|
|
|
|
|
*/
|
|
|
|
|
absorbablePositions;
|
|
|
|
|
apContainer;
|
|
|
|
|
static AbsorbablePosisiontsName = '__AbsorbablePosisionts';
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (options.absorbablePositions) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.absorbablePositions = this.filterAbsorbablePositions(options.absorbablePositions);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 过滤重复的吸附位置
|
|
|
|
|
* @param positions
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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];
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (ap.isOverlapping(ap2) && ap.compareTo(ap2) <= 0) {
|
|
|
|
|
ap = null;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (ap != null) {
|
|
|
|
|
aps.push(ap);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return aps;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
static new(app) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return new GraphicTransformPlugin(app);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
bind() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
unbind() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
getDraggedTargets(e) {
|
|
|
|
|
const targets = [];
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (e.target.isGraphicChild() && e.target.selected && e.target.draggable) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const graphic = e.target.getGraphic();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 图形子元素
|
2023-12-12 17:31:07 +08:00
|
|
|
|
recursiveChildren(graphic, (child) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (child.selected && child.draggable) {
|
|
|
|
|
targets.push(child);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
else if ((e.target.isGraphic() || e.target.isGraphicChild()) &&
|
2023-12-13 13:29:51 +08:00
|
|
|
|
e.target.getGraphic()?.draggable) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 图形对象
|
2023-12-12 17:31:07 +08:00
|
|
|
|
targets.push(...this.app.selectedGraphics);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
else if (e.target.draggable) {
|
|
|
|
|
targets.push(e.target);
|
|
|
|
|
}
|
|
|
|
|
return targets;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onDragStart(e) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!e.target.isCanvas() && e.isLeftButton) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const targets = this.getDraggedTargets(e);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (targets.length > 0) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
targets.forEach((target) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.apContainer.addChild(...this.absorbablePositions);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onDragMove(e) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!e.target.isCanvas() && e.isLeftButton) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const targets = this.getDraggedTargets(e);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (targets.length > 0) {
|
|
|
|
|
// 处理位移
|
2023-12-12 17:31:07 +08:00
|
|
|
|
targets.forEach((target) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (target.shiftStartPoint) {
|
|
|
|
|
target.shiftLastPoint = target.position.clone();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const { dx, dy } = e.toTargetShiftLen(target.parent);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
target.position.set(target.shiftStartPoint.x + dx, target.shiftStartPoint.y + dy);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// 处理吸附
|
|
|
|
|
if (this.absorbablePositions) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
for (let i = 0; i < this.absorbablePositions.length; i++) {
|
|
|
|
|
const ap = this.absorbablePositions[i];
|
|
|
|
|
ap.tryAbsorb(...targets);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// const start = new Date().getTime();
|
|
|
|
|
// 事件发布
|
2023-12-12 17:31:07 +08:00
|
|
|
|
targets.forEach((target) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (target.shiftStartPoint && target.shiftLastPoint) {
|
|
|
|
|
target.emit('transforming', GraphicTransformEvent.shift(target, ShiftData.new(target.shiftStartPoint, target.position.clone(), target.shiftLastPoint)));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onDragEnd(e) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!e.target.isCanvas() && e.isLeftButton) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const targets = this.getDraggedTargets(e);
|
|
|
|
|
targets.forEach((target) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (target.shiftStartPoint) {
|
|
|
|
|
target.emit('transformend', GraphicTransformEvent.shift(target, ShiftData.new(target.shiftStartPoint, target.position.clone())));
|
|
|
|
|
}
|
|
|
|
|
target.shiftStartPoint = null;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
this.clearCache();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 清理缓存
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
clearCache() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 移除吸附图形
|
|
|
|
|
this.absorbablePositions = [];
|
|
|
|
|
this.apContainer.removeChildren();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onGraphicSelectedChange(g, selected) {
|
|
|
|
|
let br = g.getAssistantAppend(BoundsGraphic.Name);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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) {
|
|
|
|
|
// 缩放点
|
2023-12-12 17:31:07 +08:00
|
|
|
|
let sp = g.getAssistantAppend(TransformPoints.Name);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!sp) {
|
|
|
|
|
sp = new TransformPoints(g);
|
|
|
|
|
}
|
|
|
|
|
if (selected) {
|
|
|
|
|
sp.update();
|
|
|
|
|
sp.visible = true;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
sp.visible = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 缩放、旋转辅助
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class TransformPoints extends Container {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
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 = [];
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(obj) {
|
|
|
|
|
super();
|
|
|
|
|
this.obj = obj;
|
|
|
|
|
this.name = TransformPoints.Name;
|
|
|
|
|
this.angleAssistantText = new VectorText('', {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
fill: AppConsts.assistantElementColor,
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.angleAssistantText.setVectorFontSize(16);
|
|
|
|
|
this.angleAssistantText.anchor.set(0.5);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 创建缩放拖拽点
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const pg = this.obj.getGraphic();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (pg != null) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
|
|
|
|
// 创建旋转拖拽点
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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({
|
2023-12-12 15:29:52 +08:00
|
|
|
|
value: '' + i,
|
2023-12-12 17:31:07 +08:00
|
|
|
|
onPress: () => {
|
|
|
|
|
this.angleStep = i;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.obj.addAssistantAppend(this);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
onObjTransformStart() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.visible = false;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onObjTransformEnd() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.update();
|
|
|
|
|
this.visible = true;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onGraphicRepaint() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.visible) {
|
|
|
|
|
this.update();
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 旋转开始
|
|
|
|
|
* @param de
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
onRotateStart(de) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.hideAll();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const assistantPoint = this.obj.localToCanvasPoint(this.obj.pivot);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.rotatePivot.copyFrom(assistantPoint);
|
|
|
|
|
this.rotateLastPoint.copyFrom(de.target.position);
|
|
|
|
|
this.startAngle = this.obj.angle;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const app = this.obj.getGraphicApp();
|
|
|
|
|
this.rotateAngleStepKeyListeners.forEach((listener) => app.addKeyboardListener(listener));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.obj.emit('transformstart', GraphicTransformEvent.rotate(this.obj));
|
|
|
|
|
// app.emit('transformstart', app.selectedGraphics);
|
|
|
|
|
this.obj.getCanvas().addAssistantAppends(this.angleAssistantText);
|
|
|
|
|
this.updateAngleAssistantText(de);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
updateAngleAssistantText(de) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.angleAssistantText.text = this.obj.angle + '°';
|
2023-12-13 13:29:51 +08:00
|
|
|
|
let cursorPoint = de.data?.startPosition;
|
|
|
|
|
if (de.data?.currentPosition) {
|
|
|
|
|
cursorPoint = de.data?.currentPosition;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
if (cursorPoint) {
|
|
|
|
|
this.angleAssistantText.position.x = cursorPoint.x;
|
|
|
|
|
this.angleAssistantText.position.y = cursorPoint.y - 10;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 旋转移动
|
|
|
|
|
* @param de
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
onRotateMove(de) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 旋转角度计算逻辑:取锚点y负方向一点作为旋转点,求旋转点和锚点所形成的直线与x轴角度,此角度+90°即为最终旋转角度,再将旋转角度限制到(-180,180]之间
|
2023-12-12 17:31:07 +08:00
|
|
|
|
let angle = angleToAxisx(this.rotatePivot, de.target.position);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
angle = Math.floor(angle / this.angleStep) * this.angleStep;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const parentAngle = this.obj.parent.worldAngle;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
angle = (angle + 90 - parentAngle) % 360;
|
|
|
|
|
if (angle > 180) {
|
|
|
|
|
angle = angle - 360;
|
|
|
|
|
}
|
|
|
|
|
this.obj.angle = angle;
|
|
|
|
|
this.updateAngleAssistantText(de);
|
|
|
|
|
// this.obj.emit('rotatemove', this.obj);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 旋转结束
|
|
|
|
|
* @param de
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
onRotateEnd() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.showAll();
|
|
|
|
|
this.obj.getCanvas().removeAssistantAppends(this.angleAssistantText);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.rotateAngleStepKeyListeners.forEach((listener) => this.obj.getGraphicApp().removeKeyboardListener(listener));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.obj.emit('transformend', GraphicTransformEvent.rotate(this.obj));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 缩放开始
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
onScaleDragStart() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.hideAll();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const points = convertRectangleToPolygonPoints(this.obj.getLocalBounds());
|
|
|
|
|
const p0 = points[0];
|
|
|
|
|
const p1 = points[1];
|
|
|
|
|
const p2 = points[2];
|
|
|
|
|
const p3 = points[3];
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onScaleDragMove(e) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// 缩放计算逻辑:共8个方向缩放点,根据所拖拽的方向:
|
|
|
|
|
// 1,计算缩放为1时的此点在拖拽开始时的位置到锚点x、y距离,
|
|
|
|
|
// 2,计算拖拽点的当前位置到锚点的x、y方向距离,
|
|
|
|
|
// PS:以上两个计算都是在local(也就是图形对象本地)坐标系,
|
|
|
|
|
// 用当前距离除以原始距离即为缩放比例
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const defaultScale = new Point(1, 1);
|
|
|
|
|
let originWidth = 0;
|
|
|
|
|
let originHeight = 0;
|
|
|
|
|
let width = 0;
|
|
|
|
|
let height = 0;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.obj.scale.copyFrom(defaultScale);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const point = this.obj.toLocal(e.target.parent.localToScreenPoint(e.target.position));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
// 计算缩放比例,并根据是否保持纵横比两种情况进行缩放处理
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const sx = originWidth == 0 ? this.originScale.x : width / originWidth;
|
|
|
|
|
const sy = originHeight == 0 ? this.originScale.y : height / originHeight;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.obj.keepAspectRatio) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
let max = Math.max(sx, sy);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onScaleDragEnd() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.showAll();
|
|
|
|
|
this.obj.emit('transformend', GraphicTransformEvent.scale(this.obj));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
hideOthers(dg) {
|
|
|
|
|
this.children.forEach((child) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (child.name !== dg.name) {
|
|
|
|
|
child.visible = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
hideAll() {
|
|
|
|
|
this.children.forEach((child) => (child.visible = false));
|
|
|
|
|
}
|
|
|
|
|
showAll() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.update();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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 };
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 更新位置和cursor
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
update() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.obj.scalable) {
|
|
|
|
|
this.updateScalePoints();
|
|
|
|
|
}
|
|
|
|
|
if (this.obj.rotatable) {
|
|
|
|
|
this.updateRotatePoint();
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
updateRotatePoint() {
|
|
|
|
|
const rect = this.obj.getLocalBounds();
|
|
|
|
|
const lp = this.obj.pivot.clone();
|
|
|
|
|
const dy = 10 / this.obj.scale.y;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
lp.y = rect.y - dy;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const p = this.obj.localToCanvasPoint(lp);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.rotatePoint.position.copyFrom(p);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
updateScalePoints() {
|
|
|
|
|
const points = this.obj.localBoundsToCanvasPoints();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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]));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const angle = this.obj.worldAngle;
|
|
|
|
|
const angle360 = (360 + angle) % 360;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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';
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 包围盒矩形图形,现使用外边框转画布多边形实现
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class BoundsGraphic extends Graphics {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
static Name = '_BoundsRect';
|
|
|
|
|
static BoundsLineStyle = {
|
|
|
|
|
width: 1,
|
|
|
|
|
color: '#1976d2',
|
|
|
|
|
alpha: 1,
|
|
|
|
|
};
|
|
|
|
|
obj;
|
|
|
|
|
debouncedRedraw;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const pg = this.obj.getGraphic();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (pg != null) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
pg.on('transformstart', this.onObjTransformStart, this);
|
|
|
|
|
pg.on('transformend', this.onObjTransformEnd, this);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.obj.on('repaint', this.onGraphicRepaint, this);
|
|
|
|
|
graphic.addAssistantAppend(this);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
onObjTransformStart() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.visible = false;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onObjTransformEnd() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.redraw();
|
|
|
|
|
this.visible = true;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
onGraphicRepaint() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.visible) {
|
|
|
|
|
this.redraw();
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
destroy(options) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.obj.isGraphic()) {
|
|
|
|
|
this.obj.off('repaint', this.onGraphicRepaint, this);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
super.destroy(options);
|
|
|
|
|
}
|
|
|
|
|
redraw() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.debouncedRedraw(this);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
doRedraw() {
|
|
|
|
|
const visible = this.visible;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.visible = false; // 屏蔽包围框本身
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const bounds = new Polygon(this.obj.localBoundsToCanvasPoints());
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.clear().lineStyle(BoundsGraphic.BoundsLineStyle).drawShape(bounds);
|
|
|
|
|
this.visible = visible;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 操作
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class JlOperation {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
type; // 操作类型/名称
|
|
|
|
|
app;
|
|
|
|
|
obj; // 操作对象
|
|
|
|
|
data; // 操作数据
|
|
|
|
|
description = ''; // 操作描述
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(app, type) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.app = app;
|
|
|
|
|
this.type = type;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
undo1() {
|
|
|
|
|
const updates = this.undo();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (updates) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.app.updateSelected(...updates);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
redo1() {
|
|
|
|
|
const updates = this.redo();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (updates) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
this.app.updateSelected(...updates);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 操作记录
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class OperationRecord {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
undoStack = [];
|
|
|
|
|
redoStack = [];
|
|
|
|
|
maxLen;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(maxLen = 100) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.maxLen = maxLen;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
get hasUndo() {
|
|
|
|
|
return this.undoStack.length > 0;
|
|
|
|
|
}
|
|
|
|
|
get hasRedo() {
|
|
|
|
|
return this.redoStack.length > 0;
|
|
|
|
|
}
|
|
|
|
|
setMaxLen(v) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.maxLen = v;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const len = this.undoStack.length;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (len > v) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const removeCount = len - v;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.undoStack.splice(0, removeCount);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 记录
|
|
|
|
|
* @param op
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
record(op) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (this.undoStack.length >= this.maxLen) {
|
|
|
|
|
this.undoStack.shift();
|
|
|
|
|
}
|
|
|
|
|
this.undoStack.push(op);
|
|
|
|
|
this.redoStack.splice(0, this.redoStack.length);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 撤销
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
undo() {
|
|
|
|
|
const op = this.undoStack.pop();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (op) {
|
|
|
|
|
op.undo1();
|
|
|
|
|
this.redoStack.push(op);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 重做
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
redo() {
|
|
|
|
|
const op = this.redoStack.pop();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (op) {
|
|
|
|
|
op.redo1();
|
|
|
|
|
this.undoStack.push(op);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新画布操作
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class UpdateCanvasOperation extends JlOperation {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
obj;
|
|
|
|
|
old;
|
|
|
|
|
data;
|
|
|
|
|
description = '';
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(app, obj, old, data) {
|
|
|
|
|
super(app, 'update-canvas');
|
|
|
|
|
this.app = app;
|
|
|
|
|
this.obj = obj;
|
|
|
|
|
this.old = old;
|
|
|
|
|
this.data = data;
|
|
|
|
|
}
|
|
|
|
|
undo() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.obj.update(this.old);
|
|
|
|
|
return [];
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
redo() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.obj.update(this.data);
|
|
|
|
|
return [];
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 创建图形操作
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class GraphicCreateOperation extends JlOperation {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
obj;
|
|
|
|
|
description = '';
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.obj;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 删除图形操作
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class GraphicDeleteOperation extends JlOperation {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
obj;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(app, obj) {
|
|
|
|
|
super(app, 'graphic-delete');
|
|
|
|
|
this.app = app;
|
|
|
|
|
this.obj = obj;
|
|
|
|
|
}
|
|
|
|
|
undo() {
|
|
|
|
|
this.app.addGraphics(...this.obj);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.obj;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
redo() {
|
|
|
|
|
this.app.deleteGraphics(...this.obj);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
class GraphicDataUpdateOperation extends JlOperation {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
obj;
|
|
|
|
|
oldData;
|
|
|
|
|
newData;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
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];
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// g.exitChildEdit();
|
|
|
|
|
g.updateData(this.oldData[i]);
|
|
|
|
|
}
|
|
|
|
|
return this.obj;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
redo() {
|
|
|
|
|
for (let i = 0; i < this.obj.length; i++) {
|
|
|
|
|
const g = this.obj[i];
|
2023-12-12 15:29:52 +08:00
|
|
|
|
// g.exitChildEdit();
|
|
|
|
|
g.updateData(this.newData[i]);
|
|
|
|
|
}
|
|
|
|
|
return this.obj;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 图形关系数据
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class GraphicRelationParam {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
g;
|
|
|
|
|
param;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(g, param = null) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.g = g;
|
|
|
|
|
this.param = param;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
isTheGraphic(g) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.g.id === g.id;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
getGraphic() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.g;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
getParam() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.param;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
equals(other) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.isTheGraphic(other.g) && this.param === other.param;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 图形关系
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class GraphicRelation {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
rp1;
|
|
|
|
|
rp2;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor(rp1, rp2) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.rp1 = rp1;
|
|
|
|
|
this.rp2 = rp2;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
contains(g) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.rp1.isTheGraphic(g) || this.rp2.isTheGraphic(g);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 获取给定图形的关系参数
|
|
|
|
|
* @param g
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
getRelationParam(g) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!this.contains(g)) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
throw new Error(`图形关系${this.rp1.g.id}-${this.rp2.g.id}中不包含给定图形${g.id}`);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
if (this.rp1.isTheGraphic(g)) {
|
|
|
|
|
return this.rp1;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
return this.rp2;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 获取关联的另一个图形的关系参数
|
|
|
|
|
* @param g
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
getOtherRelationParam(g) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!this.contains(g)) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
throw new Error(`图形关系${this.rp1.g.id}-${this.rp2.g.id}中不包含给定图形${g.id}`);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
if (this.rp1.isTheGraphic(g)) {
|
|
|
|
|
return this.rp2;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
return this.rp1;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 获取关联的另一个图形对象
|
|
|
|
|
* @param g
|
|
|
|
|
* @returns graphic
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
getOtherGraphic(g) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.getOtherRelationParam(g).g;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
equals(orp1, orp2) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
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;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
isEqualOther(other) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.equals(other.rp1, other.rp2);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 图形关系管理
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class RelationManage {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
relations = [];
|
2023-12-12 17:31:07 +08:00
|
|
|
|
isContainsRelation(rp1, rp2) {
|
|
|
|
|
const relation = this.relations.find((relation) => relation.equals(rp1, rp2));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return !!relation;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
addRelation(rp1, rp2) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!(rp1 instanceof GraphicRelationParam)) {
|
|
|
|
|
rp1 = new GraphicRelationParam(rp1);
|
|
|
|
|
}
|
|
|
|
|
if (!(rp2 instanceof GraphicRelationParam)) {
|
|
|
|
|
rp2 = new GraphicRelationParam(rp2);
|
|
|
|
|
}
|
|
|
|
|
if (!this.isContainsRelation(rp1, rp2)) {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const relation = new GraphicRelation(rp1, rp2);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.relations.push(relation);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 获取图形的所有关系
|
|
|
|
|
* @param g
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
getRelationsOfGraphic(g) {
|
|
|
|
|
return this.relations.filter((rl) => rl.contains(g));
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 获取指定图形的指定关系图形类型的所有关系
|
|
|
|
|
* @param g 指定图形
|
|
|
|
|
* @param type 关联图形的类型
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
getRelationsOfGraphicAndOtherType(g, type) {
|
|
|
|
|
return this.relations.filter((rl) => rl.contains(g) && rl.getOtherGraphic(g).type === type);
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 删除关系
|
|
|
|
|
* @param relation
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
deleteRelation(relation) {
|
|
|
|
|
const index = this.relations.findIndex((rl) => rl.isEqualOther(relation));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (index >= 0) {
|
|
|
|
|
this.relations.splice(index, 1);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 删除指定图形的所有关系
|
|
|
|
|
* @param g
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
deleteRelationOfGraphic(g) {
|
|
|
|
|
const relations = this.getRelationsOfGraphic(g);
|
|
|
|
|
relations.forEach((rl) => this.deleteRelation(rl));
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 删除指定图形的所有关系
|
|
|
|
|
* @param g
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
deleteRelationOfGraphicAndOtherType(g, type) {
|
|
|
|
|
const relations = this.getRelationsOfGraphicAndOtherType(g, type);
|
|
|
|
|
relations.forEach((rl) => this.deleteRelation(rl));
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 清空
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
clear() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.relations.splice(0, this.relations.length);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 图形存储
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
class GraphicStore {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
store;
|
|
|
|
|
relationManage;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
constructor() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.store = new Map();
|
|
|
|
|
this.relationManage = new RelationManage();
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 获取所有图形对象
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
getAllGraphics() {
|
|
|
|
|
return [...this.store.values()];
|
|
|
|
|
}
|
|
|
|
|
queryById(id) {
|
|
|
|
|
const graphic = this.store.get(id);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!graphic)
|
2023-12-12 17:31:07 +08:00
|
|
|
|
throw Error(`未找到id为 [${id}] 的图形.`);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return this.store.get(id);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
queryByIdAmbiguous(id) {
|
|
|
|
|
const list = [];
|
|
|
|
|
this.store.forEach((g) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (g.id.search(id) >= 0) {
|
|
|
|
|
list.push(g);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return list;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
queryByType(type) {
|
|
|
|
|
const list = [];
|
|
|
|
|
this.store.forEach((g) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (g.type === type) {
|
|
|
|
|
list.push(g);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return list;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
queryByCode(code) {
|
|
|
|
|
const list = [];
|
|
|
|
|
this.store.forEach((g) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (g.code === code) {
|
|
|
|
|
list.push(g);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return list;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
queryByCodeAmbiguous(code) {
|
|
|
|
|
const list = [];
|
|
|
|
|
this.store.forEach((g) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (g.code.search(code) >= 0) {
|
|
|
|
|
list.push(g);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return list;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
queryByIdOrCode(s) {
|
|
|
|
|
const list = [];
|
|
|
|
|
this.store.forEach((g) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (g.isIdOrCode(s)) {
|
|
|
|
|
list.push(g);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return list;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
queryByIdOrCodeAndType(s, type) {
|
|
|
|
|
const list = [];
|
|
|
|
|
this.store.forEach((g) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (g.isIdOrCode(s) && g.type === type) {
|
|
|
|
|
list.push(g);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return list;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
queryByCodeAndType(code, type) {
|
|
|
|
|
for (const item of this.store.values()) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (item.code === code && item.type === type) {
|
|
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
queryByCodeAndTypeAmbiguous(code, type) {
|
|
|
|
|
const list = [];
|
|
|
|
|
this.store.forEach((g) => {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (g.type === type && g.code.search(code) >= 0) {
|
|
|
|
|
list.push(g);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return list;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 存储图形对象
|
|
|
|
|
* @param graphics 要存储的图形
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
storeGraphics(graphic) {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (!graphic.id || graphic.id.trim() === '') {
|
2023-12-12 17:31:07 +08:00
|
|
|
|
throw new Error(`存储图形对象异常: id为空, ${graphic}`);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
if (this.store.has(graphic.id)) {
|
|
|
|
|
// 已经存在
|
2023-12-12 17:31:07 +08:00
|
|
|
|
const exist = this.store.get(graphic.id);
|
|
|
|
|
console.error(`已经存在id=${graphic.id}的设备${exist}`);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
this.store.set(graphic.id, graphic);
|
|
|
|
|
graphic.queryStore = this;
|
|
|
|
|
graphic.relationManage = this.relationManage;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 删除图形
|
|
|
|
|
* @param graph 要删除的图形
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
deleteGraphics(graphic) {
|
|
|
|
|
const id = graphic.id;
|
|
|
|
|
const remove = this.store.get(id);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
if (remove) {
|
|
|
|
|
this.store.delete(id);
|
|
|
|
|
// 删除图形关系
|
|
|
|
|
this.relationManage.deleteRelationOfGraphic(remove);
|
|
|
|
|
}
|
|
|
|
|
return remove;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
|
|
|
|
* 清空
|
|
|
|
|
*/
|
2023-12-12 17:31:07 +08:00
|
|
|
|
clear() {
|
2023-12-12 15:29:52 +08:00
|
|
|
|
this.relationManage.clear();
|
|
|
|
|
this.store.clear();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
2023-12-13 10:29:16 +08:00
|
|
|
|
//基础图形对象扩展
|
|
|
|
|
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();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
2023-12-13 10:29:16 +08:00
|
|
|
|
return scaled;
|
|
|
|
|
};
|
|
|
|
|
DisplayObject.prototype.getPositionOnCanvas =
|
|
|
|
|
function getPositionOnCanvas() {
|
|
|
|
|
if (this.parent.isCanvas()) {
|
|
|
|
|
return this.position;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
else {
|
|
|
|
|
return this.parent.localToCanvasPoint(this.position);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
};
|
|
|
|
|
DisplayObject.prototype.updatePositionByCanvasPosition =
|
|
|
|
|
function updatePositionByCanvasPosition(p) {
|
|
|
|
|
if (this.parent.isCanvas()) {
|
|
|
|
|
this.position.copyFrom(p);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
else {
|
|
|
|
|
const localPosition = this.parent.canvasToLocalPoint(p);
|
|
|
|
|
this.position.copyFrom(localPosition);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
};
|
|
|
|
|
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不能为空');
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
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();
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
2023-12-13 10:29:16 +08:00
|
|
|
|
};
|
|
|
|
|
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;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
return this;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
};
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 图形变换数据
|
|
|
|
|
*/
|
|
|
|
|
class GraphicTransform {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
position;
|
|
|
|
|
scale;
|
|
|
|
|
rotation;
|
|
|
|
|
skew;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
constructor(position, scale, rotation, skew) {
|
|
|
|
|
this.position = position;
|
|
|
|
|
this.scale = scale;
|
|
|
|
|
this.rotation = rotation;
|
|
|
|
|
this.skew = skew;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
static default() {
|
|
|
|
|
return new GraphicTransform(new Point(0, 0), new Point(1, 1), 0, new Point(0, 0));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
static fromObject(obj) {
|
|
|
|
|
return new GraphicTransform(obj.position.clone(), obj.scale.clone(), obj.rotation, obj.skew.clone());
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 图形子元素变换
|
|
|
|
|
*/
|
|
|
|
|
class ChildTransform {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
name;
|
|
|
|
|
transform;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
constructor(name, transform) {
|
|
|
|
|
this.name = name;
|
|
|
|
|
this.transform = transform;
|
|
|
|
|
}
|
|
|
|
|
static fromChild(child) {
|
|
|
|
|
if (child.name == null ||
|
|
|
|
|
child.name == undefined ||
|
|
|
|
|
child.name.trim() == '') {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
throw new Error(`图形type=${child.getGraphic()?.type}的子元素${child}name为空,但设置为需要保存变换数据`);
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
return new ChildTransform(child.name, GraphicTransform.fromObject(child));
|
|
|
|
|
}
|
|
|
|
|
static from(ct) {
|
|
|
|
|
return new ChildTransform(ct.name, GraphicTransform.from(ct.transform));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
class GraphicAnimation {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
options;
|
|
|
|
|
_running;
|
|
|
|
|
/**
|
|
|
|
|
* 倍速
|
|
|
|
|
*/
|
|
|
|
|
_xSpeed;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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 {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
__JlGraphic = true;
|
|
|
|
|
type; // 图形类型
|
|
|
|
|
_id = ''; // 图形的唯一标识,不具有业务意义,唯一,不可重复,可用做图形数据关联。
|
|
|
|
|
_code = ''; // 业务编号/名称,用于标识图形对象,具有业务意义
|
|
|
|
|
_datas; // 图形数据
|
|
|
|
|
_states; // 图形状态数据
|
|
|
|
|
_relationManage; // 图形关系管理
|
|
|
|
|
_queryStore; // 图形对象查询仓库
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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}的的图形没有状态`);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
get queryStore() {
|
2023-12-13 10:29:16 +08:00
|
|
|
|
if (this._queryStore) {
|
|
|
|
|
return this._queryStore;
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`type=${this.type}的图形没有QueryStore`);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
set queryStore(v) {
|
|
|
|
|
this._queryStore = v;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
get relationManage() {
|
|
|
|
|
if (this._relationManage) {
|
|
|
|
|
return this._relationManage;
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`type=${this.type}的图形没有关系管理`);
|
|
|
|
|
}
|
|
|
|
|
set relationManage(v) {
|
|
|
|
|
this._relationManage = v;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 构建图形关系
|
|
|
|
|
* @param g
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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));
|
2023-12-12 17:39:01 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-13 10:29:16 +08:00
|
|
|
|
return childTransforms;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 加载数据
|
|
|
|
|
* @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;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 图形状态更新处理
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
onStateChange(newVal, old) { }
|
|
|
|
|
repaint() {
|
|
|
|
|
try {
|
|
|
|
|
this.doRepaint();
|
|
|
|
|
this.emit('repaint', this);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
catch (e) {
|
|
|
|
|
console.error(`设备id=${this.id},type=${this.type}重绘逻辑异常`, e);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 处理删除逻辑
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
onDelete() {
|
|
|
|
|
this.removeAllAssistantAppend();
|
|
|
|
|
this.removeAllListeners();
|
|
|
|
|
recursiveChildren(this, (child) => child.removeAllAssistantAppend());
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 框选检测,默认取图形包围盒判定,若需要精细判定-子类重写此方法
|
|
|
|
|
* @param box
|
|
|
|
|
* @returns
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
boxIntersectCheck(box) {
|
|
|
|
|
return box.intersects(this.getLocalBounds(), this.localTransform);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 图形对象模板
|
|
|
|
|
*/
|
|
|
|
|
class JlGraphicTemplate {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
type;
|
|
|
|
|
options;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
constructor(type, options) {
|
|
|
|
|
this.type = type;
|
|
|
|
|
this.options = options;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
get datas() {
|
|
|
|
|
if (this.options.dataTemplate) {
|
|
|
|
|
return this.options.dataTemplate.clone();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
throw new Error(`type=${this.type}的图形模板没有数据模板`);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
get states() {
|
|
|
|
|
if (this.options.stateTemplate) {
|
|
|
|
|
return this.options.stateTemplate.clone();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
throw new Error(`type=${this.type}的图形模板没有状态模板`);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 加载图形对象需要用到的资源
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 13:29:51 +08:00
|
|
|
|
async loadAssets() { }
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 克隆图形对象
|
|
|
|
|
* @param graphic
|
|
|
|
|
* @returns
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
|
|
|
class MessageClient extends EventEmitter {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
options;
|
|
|
|
|
subClients = []; // 订阅客户端
|
2023-12-13 10:29:16 +08:00
|
|
|
|
constructor(options) {
|
|
|
|
|
super();
|
|
|
|
|
this.options = options;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
unsubscribe(destination) {
|
|
|
|
|
this.unsubscribe0(destination);
|
|
|
|
|
const idx = this.subClients.findIndex((cli) => cli.destination === destination);
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
this.subClients.splice(idx, 1);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
class SubscriptionClient {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
mc;
|
|
|
|
|
destination;
|
|
|
|
|
protocol;
|
|
|
|
|
handlers = [];
|
|
|
|
|
subscripted = false;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
addHandler(handler) {
|
|
|
|
|
if (this.handlers.filter((h) => h.App === handler.App).length == 0) {
|
|
|
|
|
this.handlers.push(handler);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
if (!this.subscripted) {
|
|
|
|
|
this.trySubscribe();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
trySubscribe() {
|
|
|
|
|
if (this.mc.connected) {
|
|
|
|
|
this.subscripted = this.mc.subscribe(this.destination, (data) => {
|
|
|
|
|
this.handleMessage(data);
|
|
|
|
|
});
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
unsubscribe() {
|
|
|
|
|
this.mc.unsubscribe(this.destination);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
handleMessage(data) {
|
|
|
|
|
if (this.protocol === 'json') {
|
|
|
|
|
console.debug('收到消息:', data);
|
|
|
|
|
}
|
|
|
|
|
this.handlers.forEach((handler) => {
|
|
|
|
|
try {
|
|
|
|
|
handler.handle(data);
|
|
|
|
|
}
|
|
|
|
|
catch (error) {
|
|
|
|
|
console.error('图形应用状态消息处理异常', error);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
onDisconnect() {
|
|
|
|
|
this.subscripted = false;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ReconnectDelay = 3000;
|
|
|
|
|
const HeartbeatIncoming = 30000;
|
|
|
|
|
const HeartbeatOutgoing = 30000;
|
|
|
|
|
class StompMessagingClient extends MessageClient {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
cli;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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.cli.activate();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
else {
|
|
|
|
|
handle(frame.binaryBody);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}, {
|
|
|
|
|
id: destination,
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
2023-12-13 10:29:16 +08:00
|
|
|
|
return true;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
unsubscribe0(destination) {
|
|
|
|
|
this.cli.unsubscribe(destination);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
close() {
|
|
|
|
|
this.cli.deactivate();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class MqttMsgClient extends MessageClient {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
cli;
|
|
|
|
|
retryTimes = 0;
|
|
|
|
|
subMsgHandler = new Map();
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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 达到重连最大尝试次数,停止重试');
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
catch (error) {
|
|
|
|
|
console.error(error);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
else {
|
|
|
|
|
// 字节流
|
|
|
|
|
handle(message);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
catch (err) {
|
|
|
|
|
console.error('MQTT connect error', err);
|
|
|
|
|
this.emit('error', err);
|
|
|
|
|
throw err;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
subscribe(destination, handle) {
|
|
|
|
|
console.debug('MQTT订阅执行', destination);
|
|
|
|
|
this.cli.subscribe(destination, { qos: 0 }, (error, res) => {
|
|
|
|
|
if (error) {
|
|
|
|
|
console.warn('MQTT 订阅失败', error);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
return;
|
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
console.debug('MQTT 订阅成功', res);
|
|
|
|
|
return false;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
2023-12-13 10:29:16 +08:00
|
|
|
|
this.subMsgHandler.set(destination, handle);
|
|
|
|
|
return true;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
unsubscribe0(destination) {
|
|
|
|
|
console.debug('MQTT取消订阅执行', destination);
|
|
|
|
|
this.cli.unsubscribe(destination, (error, packet) => {
|
|
|
|
|
if (error) {
|
|
|
|
|
console.warn('MQTT 取消订阅失败', error);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
console.debug('MQTT 取消订阅成功', packet);
|
|
|
|
|
this.subMsgHandler.delete(destination);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
get connected() {
|
|
|
|
|
return this.cli.connected;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
close() {
|
|
|
|
|
try {
|
|
|
|
|
console.debug('MQTT关闭执行');
|
|
|
|
|
this.cli.end(true, () => {
|
|
|
|
|
console.debug('MQTT 消息客户端关闭成功');
|
|
|
|
|
this.subMsgHandler.clear();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
catch (error) {
|
|
|
|
|
console.warn('MQTT 消息客户端关闭失败', error);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
static client;
|
|
|
|
|
static options;
|
|
|
|
|
static appMsgBroker = [];
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
case ClientEngine.Stomp: {
|
|
|
|
|
WsMsgCli.client = new StompMessagingClient(WsMsgCli.options);
|
|
|
|
|
break;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
default: {
|
|
|
|
|
throw new Error('未知的消息客户端引擎类型', WsMsgCli.options.engine);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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 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;
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 关闭websocket连接
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
static close() {
|
|
|
|
|
if (WsMsgCli.client) {
|
|
|
|
|
WsMsgCli.client.close();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
class AppMessageHandler {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
app;
|
|
|
|
|
sub;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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 {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
app;
|
|
|
|
|
subscriptions = new Map();
|
2023-12-13 10:29:16 +08:00
|
|
|
|
constructor(app) {
|
|
|
|
|
this.app = app;
|
|
|
|
|
WsMsgCli.registerAppMsgBroker(this);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
subscribe(sub) {
|
|
|
|
|
const handler = new AppMessageHandler(this.app, sub);
|
|
|
|
|
WsMsgCli.registerSubscription(sub.destination, handler);
|
|
|
|
|
this.subscriptions.set(sub.destination, handler);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
unsbuscribe(destination) {
|
|
|
|
|
const oldSub = this.subscriptions.get(destination);
|
|
|
|
|
if (oldSub) {
|
|
|
|
|
WsMsgCli.unregisterSubscription(destination, oldSub);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
unsubscribeAll() {
|
|
|
|
|
this.subscriptions.forEach((record, destination) => {
|
|
|
|
|
this.unsbuscribe(destination);
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
resubscribeAll() {
|
|
|
|
|
this.subscriptions.forEach((handler, destination) => {
|
|
|
|
|
WsMsgCli.registerSubscription(destination, handler);
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 取消所有订阅,从通用Stomp客户端移除此消息代理
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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 {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
app;
|
|
|
|
|
contextMenuMap = new Map();
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 获取视口屏幕宽度
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
get screenWidth() {
|
|
|
|
|
return this.app.viewport.screenWidth;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 获取视口屏幕高度
|
|
|
|
|
*/
|
|
|
|
|
get screenHeight() {
|
|
|
|
|
return this.app.viewport.screenHeight;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 打开菜单
|
|
|
|
|
* @param menu
|
|
|
|
|
* @param global
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 关闭菜单
|
|
|
|
|
* @param menu
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
close(menu) {
|
|
|
|
|
if (menu.opened) {
|
|
|
|
|
menu.opened = false;
|
|
|
|
|
this.app.pixi.stage.removeChild(menu);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 关闭所有菜单
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
closeAll() {
|
|
|
|
|
this.contextMenuMap.forEach((cm) => {
|
|
|
|
|
this.close(cm);
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 越界检查
|
|
|
|
|
* @param menu
|
|
|
|
|
* @param global
|
|
|
|
|
* @returns
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 上下文菜单,多级嵌套
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
class ContextMenu extends Container {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
_plugin;
|
|
|
|
|
parentMenuItem;
|
|
|
|
|
openedSubMenu;
|
|
|
|
|
menuOptions;
|
|
|
|
|
opened = false;
|
|
|
|
|
bg;
|
|
|
|
|
title;
|
|
|
|
|
groups;
|
|
|
|
|
padding = 5;
|
|
|
|
|
_active = false; // 激活状态
|
|
|
|
|
timeoutCloseHandle;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
static init(options) {
|
|
|
|
|
return new ContextMenu(options);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
get style() {
|
|
|
|
|
return this.menuOptions.style;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 父级菜单
|
|
|
|
|
*/
|
|
|
|
|
get parentMenu() {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
return this.parentMenuItem?.menu;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 最顶级菜单
|
|
|
|
|
*/
|
|
|
|
|
get rootMenu() {
|
|
|
|
|
if (this.parentMenu) {
|
|
|
|
|
return this.parentMenu.rootMenu;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
return this;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 是否存在激活的菜单项
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
hasActiveItem() {
|
|
|
|
|
for (let i = 0; i < this.groups.length; i++) {
|
|
|
|
|
const group = this.groups[i];
|
|
|
|
|
if (group.hasActiveItem()) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 初始化
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 显示菜单
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
open(global) {
|
|
|
|
|
if (this.parentMenu) {
|
|
|
|
|
this.parentMenu.openSub(this, global);
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
this.update();
|
|
|
|
|
this.plugin.open(this, global);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 关闭菜单
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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 {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
gutter = 3; // 名称、快捷键、箭头文本间隙
|
|
|
|
|
config;
|
|
|
|
|
menu;
|
|
|
|
|
items = [];
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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++;
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
updateItemBox(maxItemWidth) {
|
|
|
|
|
this.items.forEach((item) => item.updateBox(maxItemWidth, item.totalHeight));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 菜单项
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
class ContextMenuItem extends Container {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
menu;
|
|
|
|
|
config;
|
|
|
|
|
/**
|
|
|
|
|
* 名称文本
|
|
|
|
|
*/
|
|
|
|
|
nameText;
|
|
|
|
|
/**
|
|
|
|
|
* 快捷键文本
|
|
|
|
|
*/
|
|
|
|
|
shortcutKeyText;
|
|
|
|
|
gutter = 3; // 名称、快捷键、箭头文本间隙
|
|
|
|
|
arrowText;
|
|
|
|
|
box;
|
|
|
|
|
subMenu;
|
|
|
|
|
_active = false; // 激活状态
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
get active() {
|
|
|
|
|
return this._active || (this.subMenu != undefined && this.subMenu.active);
|
|
|
|
|
}
|
|
|
|
|
set active(v) {
|
|
|
|
|
this._active = v;
|
|
|
|
|
if (this.subMenu) {
|
|
|
|
|
this.subMenu.onActiveChanged();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
this.onActiveChanged();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
2023-12-13 13:29:51 +08:00
|
|
|
|
this.nameText?.position.set(this.paddingLeft, this.paddingTop);
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
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 {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
width;
|
|
|
|
|
height;
|
|
|
|
|
backgroundColor;
|
|
|
|
|
viewportTransform;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
constructor(properties = {
|
|
|
|
|
width: 1920,
|
|
|
|
|
height: 1080,
|
|
|
|
|
backgroundColor: '#ffffff',
|
|
|
|
|
viewportTransform: GraphicTransform.default(),
|
|
|
|
|
}) {
|
|
|
|
|
this.width = properties.width;
|
|
|
|
|
this.height = properties.height;
|
|
|
|
|
this.backgroundColor = properties.backgroundColor;
|
|
|
|
|
this.viewportTransform = properties.viewportTransform;
|
|
|
|
|
}
|
|
|
|
|
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;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
this.backgroundColor = properties.backgroundColor;
|
|
|
|
|
this.viewportTransform = properties.viewportTransform;
|
|
|
|
|
return sizeChange;
|
|
|
|
|
}
|
|
|
|
|
clone() {
|
|
|
|
|
const cp = new CanvasData(this);
|
|
|
|
|
return cp;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
class JlCanvas extends Container {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
__JlCanvas = true;
|
|
|
|
|
type = 'Canvas';
|
|
|
|
|
scene;
|
|
|
|
|
_properties;
|
|
|
|
|
bg = new Graphics(); // 背景
|
|
|
|
|
nonInteractiveContainer; // 无交互对象容器
|
|
|
|
|
assistantAppendContainer; // 辅助附加容器
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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.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();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 图形重绘(数据/状态变化时触发)
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
repaint() {
|
|
|
|
|
this.doRepaint();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
get width() {
|
|
|
|
|
return this._properties.width;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
get height() {
|
|
|
|
|
return this._properties.height;
|
|
|
|
|
}
|
|
|
|
|
get backgroundColor() {
|
|
|
|
|
return this._properties.backgroundColor;
|
|
|
|
|
}
|
|
|
|
|
doRepaint() {
|
|
|
|
|
this.bg.clear();
|
|
|
|
|
this.bg
|
|
|
|
|
.beginFill(new Color(this.backgroundColor))
|
|
|
|
|
.drawRect(0, 0, this._properties.width, this._properties.height)
|
|
|
|
|
.endFill();
|
|
|
|
|
}
|
|
|
|
|
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));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
2023-12-13 10:29:16 +08:00
|
|
|
|
return rt;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
removeChild(...children) {
|
|
|
|
|
children.forEach((g) => {
|
|
|
|
|
g.onRemoveFromCanvas(this);
|
|
|
|
|
recursiveChildren(g, (child) => child.onRemoveFromCanvas(this));
|
|
|
|
|
});
|
|
|
|
|
return super.removeChild(...children);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 添加无交互Child
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
addNonInteractiveChild(...obj) {
|
|
|
|
|
this.nonInteractiveContainer.addChild(...obj);
|
|
|
|
|
obj.forEach((g) => {
|
|
|
|
|
g.onAddToCanvas(this);
|
|
|
|
|
recursiveChildren(g, (child) => child.onAddToCanvas(this));
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
removeGraphic(...obj) {
|
|
|
|
|
obj.forEach((g) => {
|
|
|
|
|
g.onRemoveFromCanvas(this);
|
|
|
|
|
recursiveChildren(g, (child) => child.onRemoveFromCanvas(this));
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
2023-12-13 10:29:16 +08:00
|
|
|
|
this.removeChild(...obj);
|
|
|
|
|
this.nonInteractiveContainer.removeChild(...obj);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 移除无交互Child
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
removeNonInteractiveChild(...obj) {
|
|
|
|
|
obj.forEach((g) => {
|
|
|
|
|
g.onRemoveFromCanvas(this);
|
|
|
|
|
recursiveChildren(g, (child) => child.onRemoveFromCanvas(this));
|
|
|
|
|
});
|
|
|
|
|
this.nonInteractiveContainer.removeChild(...obj);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
addAssistantAppends(...appends) {
|
|
|
|
|
this.assistantAppendContainer.addChild(...appends);
|
|
|
|
|
appends.forEach((g) => {
|
|
|
|
|
g.onAddToCanvas(this);
|
|
|
|
|
recursiveChildren(g, (child) => child.onAddToCanvas(this));
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
removeAssistantAppends(...appends) {
|
|
|
|
|
appends.forEach((g) => {
|
|
|
|
|
g.onRemoveFromCanvas(this);
|
|
|
|
|
recursiveChildren(g, (child) => child.onAddToCanvas(this));
|
|
|
|
|
});
|
|
|
|
|
this.assistantAppendContainer.removeChild(...appends);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 暂停所有可交互对象
|
|
|
|
|
*/
|
|
|
|
|
pauseInteractiveChildren() {
|
|
|
|
|
this.interactiveChildren = false;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 恢复所有可交互对象
|
|
|
|
|
*/
|
|
|
|
|
resumeInteractiveChildren() {
|
|
|
|
|
this.interactiveChildren = true;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
class GraphicSceneBase extends EventEmitter {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
graphicStore;
|
|
|
|
|
_options;
|
|
|
|
|
pixi; // Pixi 渲染器
|
|
|
|
|
viewport; // 视口
|
|
|
|
|
canvas; // 画布
|
|
|
|
|
_loaded = false; // 是否已经加载
|
|
|
|
|
_dom; // 场景绑定到的dom节点
|
|
|
|
|
_viewportResizer; // 自动根据dom大小变化调整视口尺寸
|
|
|
|
|
graphicTemplateMap = new Map(); // 图形对象模板
|
|
|
|
|
interactionPluginMap = new Map(); // 交互插件
|
|
|
|
|
graphicCopyPlugin; // 图形复制操作插件
|
|
|
|
|
animationManager; // 动画管理组件
|
|
|
|
|
menuPlugin; // 菜单插件
|
|
|
|
|
debounceEmitFunc;
|
|
|
|
|
wsMsgBroker; // websocket消息代理
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
});
|
2023-12-13 10:29:16 +08:00
|
|
|
|
// 发布选项更新事件
|
|
|
|
|
this.emit('options-update', this._options);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
get appOptions() {
|
|
|
|
|
return this._options;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
get dom() {
|
|
|
|
|
return this._dom;
|
|
|
|
|
}
|
|
|
|
|
get queryStore() {
|
|
|
|
|
return this.graphicStore;
|
|
|
|
|
}
|
|
|
|
|
get selectedGraphics() {
|
|
|
|
|
return this.queryStore.getAllGraphics().filter((g) => g.selected);
|
|
|
|
|
}
|
2023-12-13 13:29:51 +08:00
|
|
|
|
async load() {
|
|
|
|
|
if (this._options.dataLoader) {
|
|
|
|
|
const storage = await this._options.dataLoader();
|
|
|
|
|
if (storage.canvasProperty) {
|
|
|
|
|
this.canvas.update(storage.canvasProperty);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 13:29:51 +08:00
|
|
|
|
if (storage.datas) {
|
|
|
|
|
await this.loadGraphic(storage.datas);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this._loaded = true;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 重新加载数据
|
|
|
|
|
*/
|
2023-12-13 13:29:51 +08:00
|
|
|
|
async reload() {
|
|
|
|
|
if (!this._loaded) {
|
2023-12-13 10:29:16 +08:00
|
|
|
|
this.graphicStore.clear();
|
2023-12-13 13:29:51 +08:00
|
|
|
|
await this.load();
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
console.debug('场景已经加载过数据,不重新加载', this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
async forceReload() {
|
|
|
|
|
console.debug('场景强制重新加载数据', this);
|
|
|
|
|
this.graphicStore.clear();
|
|
|
|
|
await this.load();
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 更新选项
|
|
|
|
|
* @param options
|
|
|
|
|
*/
|
|
|
|
|
setOptions(options) {
|
|
|
|
|
if (this._options) {
|
|
|
|
|
this._options = Object.assign(this._options, options);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
else {
|
2023-12-13 10:29:16 +08:00
|
|
|
|
this._options = options;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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}的图形对象模板`);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
return template;
|
|
|
|
|
}
|
|
|
|
|
updateViewport(domWidth, domHeight) {
|
|
|
|
|
let screenWidth = this.viewport.screenWidth;
|
|
|
|
|
let screenHeight = this.viewport.screenHeight;
|
|
|
|
|
if (domWidth) {
|
|
|
|
|
screenWidth = domWidth;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
if (domHeight) {
|
|
|
|
|
screenHeight = domHeight;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 暂停
|
|
|
|
|
*/
|
|
|
|
|
pause() {
|
|
|
|
|
// 暂停动画
|
|
|
|
|
this.animationManager.pause();
|
|
|
|
|
// 取消消息订阅
|
|
|
|
|
this.wsMsgBroker.unsubscribeAll();
|
|
|
|
|
// 关闭所有上下文菜单
|
|
|
|
|
this.menuPlugin.closeAll();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 恢复
|
|
|
|
|
*/
|
|
|
|
|
resume() {
|
|
|
|
|
// 恢复动画
|
|
|
|
|
this.animationManager.resume();
|
|
|
|
|
// 重新订阅
|
|
|
|
|
this.wsMsgBroker.resubscribeAll();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
bindDom(dom) {
|
|
|
|
|
this._dom = dom;
|
|
|
|
|
this.pixi.resizeTo = dom;
|
|
|
|
|
dom.appendChild(this.pixi.view);
|
|
|
|
|
this._viewportResizer = setInterval(() => {
|
|
|
|
|
this.updateViewport(dom.clientWidth, dom.clientHeight);
|
|
|
|
|
}, 1000);
|
|
|
|
|
// 恢复
|
|
|
|
|
this.resume();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
unbindDom() {
|
|
|
|
|
if (this._dom) {
|
|
|
|
|
clearInterval(this._viewportResizer);
|
|
|
|
|
this._dom.removeChild(this.pixi.view);
|
|
|
|
|
this._dom = undefined;
|
|
|
|
|
// 暂停
|
|
|
|
|
this.pause();
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 加载图形,GraphicApp默认添加到无交互容器,DrawApp默认添加到交互容器,如需定制,提供选项配置
|
|
|
|
|
* @param protos
|
|
|
|
|
* @param options 添加到可交互/不可交互容器选项配置
|
|
|
|
|
*/
|
2023-12-13 13:29:51 +08:00
|
|
|
|
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()
|
|
|
|
|
.filter((g) => !isNaN(parseInt(g.id)))
|
|
|
|
|
.map((g) => parseInt(g.id))
|
|
|
|
|
.sort((a, b) => a - b)
|
|
|
|
|
.pop() ?? 0;
|
|
|
|
|
GraphicIdGenerator.initSerial(max);
|
|
|
|
|
// 数据加载完成后
|
|
|
|
|
this.emit('postdataloaded');
|
|
|
|
|
// 加载完成通知
|
|
|
|
|
this.emit('loadfinish');
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 添加图形前处理
|
|
|
|
|
* @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);
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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) => {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
if (this._options?.isSupportDeletion == undefined ||
|
2023-12-13 10:29:16 +08:00
|
|
|
|
this._options.isSupportDeletion(g)) {
|
|
|
|
|
this.doDeleteGraphics(g);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
console.debug(`type=${g.type},id=${g.id}的图形不支持删除`);
|
|
|
|
|
return false;
|
2023-12-12 17:39:01 +08:00
|
|
|
|
});
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
2023-12-12 17:39:01 +08:00
|
|
|
|
});
|
2023-12-13 10:29:16 +08:00
|
|
|
|
graphics.forEach((graphic) => {
|
|
|
|
|
graphic.updateSelected(true);
|
2023-12-12 17:39:01 +08:00
|
|
|
|
});
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
doEmitAppGraphicSelected() {
|
|
|
|
|
// 场景发布图形选中
|
|
|
|
|
this.emit('graphicselected', this.selectedGraphics);
|
|
|
|
|
// this.app.emit('graphicselected', this.selectedGraphics);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 更新画布
|
|
|
|
|
* @param param
|
|
|
|
|
*/
|
|
|
|
|
updateCanvas(param) {
|
|
|
|
|
this.canvas.update(param);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 使图形居中显示(所有图形的外包围盒)
|
|
|
|
|
*/
|
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 注册交互插件,会替换旧的
|
|
|
|
|
*/
|
|
|
|
|
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);
|
2023-12-12 17:39:01 +08:00
|
|
|
|
});
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 根据名称获取交互插件
|
|
|
|
|
* @param name
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
interactionPlugin(name) {
|
|
|
|
|
const plugin = this.interactionPluginMap.get(name);
|
|
|
|
|
if (!plugin) {
|
|
|
|
|
throw new Error(`未找到name=${name}的交互插件`);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
return plugin;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 停止应用交互插件
|
|
|
|
|
*/
|
|
|
|
|
pauseAppInteractionPlugins() {
|
|
|
|
|
this.interactionPluginMap.forEach((plugin) => {
|
|
|
|
|
if (plugin.isActive() && plugin._type === InteractionPluginType.App) {
|
|
|
|
|
this.doPauseInteractionPlugin(plugin);
|
|
|
|
|
}
|
2023-12-12 17:39:01 +08:00
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
doPauseInteractionPlugin(plugin) {
|
|
|
|
|
if (plugin && plugin.isActive()) {
|
|
|
|
|
plugin.pause();
|
|
|
|
|
this.emit('interaction-plugin-pause', plugin);
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 移除交互插件
|
|
|
|
|
*/
|
|
|
|
|
removeInteractionPlugin(plugin) {
|
|
|
|
|
this.interactionPluginMap.delete(plugin.name);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
checkWsMsgCli() {
|
|
|
|
|
if (!WsMsgCli.isInitiated()) {
|
|
|
|
|
throw new Error('订阅消息需先启动消息代理, 执行app.enableWebsocket()');
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 订阅websocket消息
|
|
|
|
|
*/
|
|
|
|
|
subscribe(sub) {
|
|
|
|
|
this.checkWsMsgCli();
|
|
|
|
|
this.wsMsgBroker.subscribe(sub);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 取消websocket订阅
|
|
|
|
|
*/
|
|
|
|
|
unsubscribe(destination) {
|
|
|
|
|
this.checkWsMsgCli();
|
|
|
|
|
this.wsMsgBroker.unsbuscribe(destination);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 处理图形状态
|
|
|
|
|
* @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;
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 销毁
|
|
|
|
|
*/
|
|
|
|
|
destroy() {
|
|
|
|
|
console.debug('场景销毁', this);
|
|
|
|
|
this.unbindDom();
|
|
|
|
|
if (this.wsMsgBroker) {
|
|
|
|
|
this.wsMsgBroker.close();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
this.interactionPluginMap.forEach((plugin) => {
|
|
|
|
|
plugin.pause();
|
|
|
|
|
});
|
|
|
|
|
this.animationManager.destroy();
|
|
|
|
|
this.canvas.destroy(true);
|
|
|
|
|
this.viewport.destroy();
|
|
|
|
|
this.pixi.destroy(true, true);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 图形app基类
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
class GraphicApp extends GraphicSceneBase {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
/**
|
|
|
|
|
* 场景列表
|
|
|
|
|
*/
|
|
|
|
|
scenes = new Map();
|
|
|
|
|
opRecord; // 操作记录
|
|
|
|
|
keyboardPlugin; // 键盘操作处理插件
|
2023-12-13 10:29:16 +08:00
|
|
|
|
constructor(options) {
|
|
|
|
|
super(options);
|
|
|
|
|
this.opRecord = new OperationRecord();
|
|
|
|
|
// 绑定键盘监听
|
|
|
|
|
this.keyboardPlugin = new JlGraphicAppKeyboardPlugin(this);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
get app() {
|
|
|
|
|
return this;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
setOptions(options) {
|
|
|
|
|
if (options.maxOperationRecords && options.maxOperationRecords > 0) {
|
|
|
|
|
this.opRecord.setMaxLen(options.maxOperationRecords);
|
|
|
|
|
}
|
|
|
|
|
super.setOptions(options);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
addGraphicAndRecord(...graphics) {
|
|
|
|
|
this.addGraphics(...graphics);
|
|
|
|
|
this.opRecord.record(new GraphicCreateOperation(this, graphics));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
deleteGraphicAndRecord(...graphics) {
|
|
|
|
|
this.deleteGraphics(...graphics);
|
|
|
|
|
this.opRecord.record(new GraphicDeleteOperation(this, graphics));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 实例化一个场景
|
|
|
|
|
* @param code 场景标识
|
|
|
|
|
* @returns
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
initScene(code, options) {
|
|
|
|
|
let scene = this.scenes.get(code);
|
|
|
|
|
if (!scene) {
|
|
|
|
|
scene = new JlScene(this, code, options);
|
|
|
|
|
this.scenes.set(code, scene);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
return scene;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
/**
|
|
|
|
|
* 获取场景
|
|
|
|
|
* @param code
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
getScene(code) {
|
|
|
|
|
const scene = this.scenes.get(code);
|
|
|
|
|
if (!scene) {
|
|
|
|
|
throw new Error(`不存在code=${code}的场景`);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
return scene;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
scene.bindDom(dom);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
removeScene(code) {
|
|
|
|
|
const scene = this.scenes.get(code);
|
|
|
|
|
if (scene) {
|
|
|
|
|
this.scenes.delete(code);
|
|
|
|
|
scene.destroy();
|
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 启动websocket消息客户端
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
enableWsMassaging(options) {
|
|
|
|
|
WsMsgCli.new(options);
|
|
|
|
|
this.wsMsgBroker = new AppWsMsgBroker(this);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 添加键盘监听器,如果是相同的按键,新注册的会覆盖旧的,当移除新的时,旧的自动生效
|
|
|
|
|
* @param keyListeners
|
2023-12-12 17:31:07 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
addKeyboardListener(...keyListeners) {
|
|
|
|
|
keyListeners.forEach((keyListener) => this.keyboardPlugin.addKeyListener(keyListener));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 移除键盘监听器
|
|
|
|
|
* @param keyListeners
|
2023-12-12 17:31:07 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
removeKeyboardListener(...keyListeners) {
|
|
|
|
|
keyListeners.forEach((keyListener) => this.keyboardPlugin.removeKeyListener(keyListener));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 销毁
|
2023-12-12 17:31:07 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
destroy() {
|
|
|
|
|
console.debug('图形应用销毁', this);
|
|
|
|
|
this.emit('destroy', this);
|
|
|
|
|
super.destroy();
|
|
|
|
|
this.scenes.forEach((scene) => scene.destroy());
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 场景
|
|
|
|
|
*/
|
|
|
|
|
class JlScene extends GraphicSceneBase {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
code;
|
|
|
|
|
app;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
constructor(app, code, options) {
|
|
|
|
|
super(options);
|
|
|
|
|
this.code = code;
|
|
|
|
|
this.app = app;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
|
|
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
|
|
|
/**
|
|
|
|
|
* 图形绘制助手
|
|
|
|
|
*/
|
|
|
|
|
class GraphicDrawAssistant extends AppInteractionPlugin {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
__GraphicDrawAssistant = true;
|
|
|
|
|
app;
|
|
|
|
|
type; // 图形对象类型
|
|
|
|
|
description; // 描述
|
|
|
|
|
icon; // 界面显示的图标
|
|
|
|
|
container = new Container();
|
|
|
|
|
graphicTemplate;
|
|
|
|
|
escListener = new KeyListener({
|
|
|
|
|
value: 'Escape',
|
|
|
|
|
onRelease: () => {
|
|
|
|
|
this.onEsc();
|
|
|
|
|
},
|
|
|
|
|
});
|
2023-12-13 10:29:16 +08:00
|
|
|
|
onEsc() {
|
|
|
|
|
this.createAndStore(true);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
get canvas() {
|
|
|
|
|
return this.app.canvas;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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',
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
unbind() {
|
|
|
|
|
this.clearCache();
|
|
|
|
|
const canvas = this.canvas;
|
2023-12-13 13:29:51 +08:00
|
|
|
|
if (this.container?.parent) {
|
2023-12-13 10:29:16 +08:00
|
|
|
|
canvas.removeChild(this.container);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
onLeftDown(e) { }
|
|
|
|
|
onMouseMove(e) {
|
|
|
|
|
this.redraw(this.toCanvasCoordinates(e.global));
|
|
|
|
|
}
|
|
|
|
|
onLeftUp(e) { }
|
|
|
|
|
onRightDown(e) { }
|
|
|
|
|
onRightUp(e) { }
|
|
|
|
|
onRightClick(e) {
|
|
|
|
|
this.finish();
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 获取下一个id
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
nextId() {
|
|
|
|
|
return GraphicIdGenerator.next();
|
|
|
|
|
}
|
|
|
|
|
clearCache() { }
|
|
|
|
|
toCanvasCoordinates(p) {
|
|
|
|
|
return this.app.toCanvasCoordinates(p);
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 保存创建的图形对象
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
storeGraphic(...graphics) {
|
|
|
|
|
this.app.addGraphicAndRecord(...graphics);
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 创建并添加到图形App
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 绘制完成
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
finish(...graphics) {
|
|
|
|
|
this.clearCache();
|
|
|
|
|
this.app.interactionPlugin(CommonMouseTool.Name).resume();
|
|
|
|
|
this.app.updateSelected(...graphics);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 绘制应用
|
|
|
|
|
*/
|
|
|
|
|
class JlDrawApp extends GraphicApp {
|
2023-12-13 13:29:51 +08:00
|
|
|
|
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;
|
2023-12-13 10:29:16 +08:00
|
|
|
|
get drawing() {
|
|
|
|
|
return this._drawing;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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 = [];
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 绘制状态信息显示
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
|
|
|
|
},
|
|
|
|
|
}));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 图形对象存储处理,默认添加图形交互
|
|
|
|
|
* @param graphic
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
beforeGraphicStore(graphic) {
|
|
|
|
|
graphic.eventMode = 'static';
|
|
|
|
|
graphic.selectable = true;
|
|
|
|
|
graphic.draggable = true;
|
|
|
|
|
graphic.on('repaint', () => {
|
|
|
|
|
this.handleFormDataUpdate(graphic);
|
|
|
|
|
});
|
|
|
|
|
graphic.on('transformend', () => {
|
|
|
|
|
this.handleFormDataUpdate(graphic);
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 13:29:51 +08:00
|
|
|
|
formData = undefined;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 绑定form表单对象
|
|
|
|
|
* @param form
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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;
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 移除form绑定
|
|
|
|
|
* @param form
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
unbindFormData(form) {
|
|
|
|
|
if (this.formData == form) {
|
|
|
|
|
this.formData = undefined;
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
formDataSyncListen() {
|
|
|
|
|
this.on('graphicselected', () => {
|
|
|
|
|
if (this.selectedGraphics.length == 1) {
|
|
|
|
|
this.handleFormDataUpdate(this.selectedGraphics[0]);
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
/**
|
2023-12-13 10:29:16 +08:00
|
|
|
|
* 处理表单数据更新(使用debounce限流)
|
2023-12-12 15:29:52 +08:00
|
|
|
|
*/
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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());
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
updateGraphicAndRecord(g, data) {
|
|
|
|
|
const old = g.saveData();
|
|
|
|
|
g.updateData(data);
|
|
|
|
|
const newVal = g.saveData();
|
|
|
|
|
this.opRecord.record(new GraphicDataUpdateOperation(this, [g], [old], [newVal]));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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);
|
|
|
|
|
}
|
2023-12-12 17:39:01 +08:00
|
|
|
|
});
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
app.selectedGraphics.forEach((g) => {
|
|
|
|
|
handler(g);
|
2023-12-12 17:39:01 +08:00
|
|
|
|
});
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
}
|
|
|
|
|
function updateGraphicPositionOnKeyboardEvent(app, dp) {
|
|
|
|
|
let dragStart = false;
|
|
|
|
|
if (dragStartDatas.length === 0) {
|
|
|
|
|
dragStartDatas = app.selectedGraphics.map((g) => g.saveData());
|
|
|
|
|
dragStart = true;
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
handleArrowKeyMoveGraphics(app, (g) => {
|
|
|
|
|
if (dragStart) {
|
|
|
|
|
g.shiftStartPoint = g.position.clone();
|
|
|
|
|
g.emit('transformstart', GraphicTransformEvent.shift(g, ShiftData.new(g.shiftStartPoint)));
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
else {
|
|
|
|
|
g.shiftLastPoint = g.position.clone();
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
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)));
|
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
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())));
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-13 10:29:16 +08:00
|
|
|
|
dragStartDatas = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 实例化图形app
|
|
|
|
|
* @param options
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function newGraphicApp(options) {
|
|
|
|
|
return new GraphicApp(options);
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 实例化绘图app
|
|
|
|
|
* @param options
|
|
|
|
|
* @returns
|
|
|
|
|
*/
|
|
|
|
|
function newDrawApp(options) {
|
|
|
|
|
return new JlDrawApp(options);
|
2023-12-12 17:31:07 +08:00
|
|
|
|
}
|
2023-12-12 15:29:52 +08:00
|
|
|
|
|
2023-12-13 10:29:16 +08:00
|
|
|
|
export { AbsorbableCircle, AbsorbableLine, 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, OperationRecord, OtherInteractionPlugin, OutOfBound, PolylineEditPlugin, RIGHT, RelationManage, ScaleData, ShiftData, TransformPoints, UP, 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 };
|