重构拖拽操作和图形变换代码结构

调整DisplayObject可发布的事件及事件对象
调整App可发布的事件
This commit is contained in:
walker 2023-05-11 18:20:22 +08:00
parent cd951906b0
commit df3522e205
16 changed files with 941 additions and 607 deletions

View File

@ -1,5 +1,10 @@
import { FederatedMouseEvent, Point } from 'pixi.js';
import { GraphicDrawAssistant, JlDrawApp } from 'src/jlgraphic';
import {
GraphicDrawAssistant,
GraphicInteractionPlugin,
JlDrawApp,
JlGraphic,
} from 'src/jlgraphic';
import { IIscsFanData, IscsFan, IscsFanTemplate } from './IscsFan';
export class IscsFanDraw extends GraphicDrawAssistant<
@ -11,6 +16,7 @@ export class IscsFanDraw extends GraphicDrawAssistant<
constructor(app: JlDrawApp, createData: () => IIscsFanData) {
const template = new IscsFanTemplate();
super(app, template, createData, IscsFan.Type, '');
IscsFanInteraction.init(app);
}
bind(): void {
@ -46,3 +52,29 @@ export class IscsFanDraw extends GraphicDrawAssistant<
this.finish();
}
}
export class IscsFanInteraction extends GraphicInteractionPlugin<IscsFan> {
static Name = 'iscs_fan_transform';
constructor(app: JlDrawApp) {
super(IscsFanInteraction.Name, app);
}
static init(app: JlDrawApp) {
return new IscsFanInteraction(app);
}
filter(...grahpics: JlGraphic[]): IscsFan[] | undefined {
return grahpics
.filter((g) => g.type === IscsFan.Type)
.map((g) => g as IscsFan);
}
bind(g: IscsFan): void {
g.eventMode = 'static';
g.cursor = 'pointer';
g.scalable = true;
g.rotatable = true;
}
unbind(g: IscsFan): void {
g.eventMode = 'none';
g.scalable = false;
g.rotatable = false;
}
}

View File

@ -72,9 +72,9 @@ export class Link extends JlGraphic implements ILineGraphic {
return this.datas.points;
}
set linePoints(points: IPointData[]) {
this.datas.points = points;
this.repaint();
this.emit('pointupdate', this, this.datas.points);
const old = this.datas.clone();
old.points = points;
this.updateData(old);
}
getStartPoint(): IPointData {
return this.datas.points[0];

View File

@ -13,6 +13,7 @@ import {
GraphicApp,
GraphicDrawAssistant,
GraphicInteractionPlugin,
GraphicTransformEvent,
JlDrawApp,
JlGraphic,
KeyListener,
@ -252,10 +253,12 @@ function onEditPointCreate(
const link = g as Link;
if (index === 0 || index == link.datas.points.length - 1) {
// 端点
dp.on('dragstart', () => {
dp.on('transformstart', (e: GraphicTransformEvent) => {
if (e.isShift()) {
link.getGraphicApp().setOptions({
absorbablePositions: buildAbsorbablePositions(link),
});
}
});
}
}

View File

@ -11,7 +11,13 @@ import {
} from 'pixi.js';
import { GraphicIdGenerator } from '../core/IdGenerator';
import { GraphicData, GraphicTemplate, JlGraphic } from '../core/JlGraphic';
import { BoundsGraphic, TransformPoints } from '../graphic';
import {
AppDragEvent,
BoundsGraphic,
GraphicTransformEvent,
ShiftData,
TransformPoints,
} from '../plugins';
import { JlOperation } from '../operation/JlOperation';
import {
AppInteractionPlugin,
@ -24,7 +30,6 @@ import { CommonMouseTool } from '../plugins/CommonMousePlugin';
import { DOWN, LEFT, RIGHT, UP, recursiveChildren } from '../utils';
import {
CanvasData,
CanvasEvent,
GraphicApp,
GraphicAppOptions,
ICanvasProperties,
@ -266,20 +271,27 @@ export class JlDrawApp extends GraphicApp {
private appDragRecord() {
let dragStartDatas: GraphicData[] = [];
this.on('dragstart', () => {
this.on('drag_op_start', (e: AppDragEvent) => {
// 图形拖拽,记录初始数据
// console.log('app图形拖拽开始记录图形数据');
if (!e.target.isCanvas()) {
dragStartDatas = this.selectedGraphics.map((g) => g.saveData());
}
});
// 图形拖拽操作监听
this.on('dragend', () => {
this.on('drag_op_end', () => {
// 图形拖拽,记录操作
// console.log('app图形拖拽结束记录操作');
if (dragStartDatas.length > 0) {
const newData = this.selectedGraphics.map((g) => g.saveData());
this.opRecord.record(
new DragOperation(this, this.selectedGraphics, dragStartDatas, newData)
new DragOperation(
this,
this.selectedGraphics,
dragStartDatas,
newData
)
);
dragStartDatas = [];
}
});
}
@ -289,7 +301,7 @@ export class JlDrawApp extends GraphicApp {
private initSelectedDataUpdateListen() {
// 画布数据更新
this.selectedData = new CanvasData(this.canvas.properties);
this.canvas.on(CanvasEvent.propertiesupdate, () => {
this.canvas.on('dataupdate', () => {
if (this.selectedGraphics.length === 0) {
this.selectedData = this.canvas.saveData();
this.emit('propertiesupdate', this.selectedData);
@ -309,36 +321,10 @@ export class JlDrawApp extends GraphicApp {
}
};
this.on('graphicselectedchange', (g: DisplayObject, selected) => {
let br = g.getAssistantAppend<BoundsGraphic>(BoundsGraphic.Name);
if (!br) {
// 绘制辅助包围框
br = new BoundsGraphic(g);
}
if (selected) {
g.on('repaint', graphicPropertiesUpdate, this);
if (br) {
br.redraw();
br.visible = true;
}
} else {
g.off('repaint', graphicPropertiesUpdate, this);
if (br) {
br.visible = false;
}
}
if (g.scalable) {
// 缩放点
let sp = g.getAssistantAppend<TransformPoints>(TransformPoints.Name);
if (!sp) {
sp = new TransformPoints(g);
}
if (selected) {
sp.update();
sp.visible = true;
} else {
sp.visible = false;
}
}
if (this.selectedGraphics.length === 0) {
this.selectedData = this.canvas.properties.clone();
@ -350,19 +336,6 @@ export class JlDrawApp extends GraphicApp {
}
this.emit(DrawAppEvent.propertiesupdate, this.selectedData);
});
this.on('graphicchildselectedchange', (_g, child, selected) => {
let br = child.getAssistantAppend<BoundsGraphic>(BoundsGraphic.Name);
if (!br) {
// 绘制辅助包围框
br = new BoundsGraphic(child);
}
if (selected) {
br.redraw();
br.visible = true;
} else {
br.visible = false;
}
});
}
/**
@ -448,7 +421,7 @@ export class JlDrawApp extends GraphicApp {
this.addKeyboardListener(
new KeyListener({
value: 'ArrowUp',
pressTriggerEveryTime: true,
pressTriggerAsOriginalEvent: true,
onPress: (e: KeyboardEvent, app: GraphicApp) => {
updateGraphicPositionOnKeyboardEvent(app, UP);
},
@ -460,7 +433,7 @@ export class JlDrawApp extends GraphicApp {
this.addKeyboardListener(
new KeyListener({
value: 'ArrowDown',
pressTriggerEveryTime: true,
pressTriggerAsOriginalEvent: true,
onPress: (e: KeyboardEvent, app: GraphicApp) => {
updateGraphicPositionOnKeyboardEvent(app, DOWN);
},
@ -472,7 +445,7 @@ export class JlDrawApp extends GraphicApp {
this.addKeyboardListener(
new KeyListener({
value: 'ArrowLeft',
pressTriggerEveryTime: true,
pressTriggerAsOriginalEvent: true,
onPress: (e: KeyboardEvent, app: GraphicApp) => {
updateGraphicPositionOnKeyboardEvent(app, LEFT);
},
@ -484,7 +457,7 @@ export class JlDrawApp extends GraphicApp {
this.addKeyboardListener(
new KeyListener({
value: 'ArrowRight',
pressTriggerEveryTime: true,
pressTriggerAsOriginalEvent: true,
onPress: (e: KeyboardEvent, app: GraphicApp) => {
updateGraphicPositionOnKeyboardEvent(app, RIGHT);
},
@ -557,31 +530,73 @@ export class JlDrawApp extends GraphicApp {
}
let dragStartDatas: GraphicData[] = [];
function updateGraphicPositionOnKeyboardEvent(app: GraphicApp, dp: IPointData) {
let dragStart = false;
if (dragStartDatas.length === 0) {
dragStartDatas = app.selectedGraphics.map((g) => g.saveData());
dragStart = true;
}
function handleArrowKeyMoveGraphics(
app: GraphicApp,
handler: (obj: DisplayObject) => void
) {
if (
app.selectedGraphics.length === 1 &&
app.selectedGraphics[0].hasSelectedChilds()
) {
recursiveChildren(app.selectedGraphics[0], (child) => {
if (child.selected && child.draggable) {
child.emit('transformstart', child);
child.position.x += dp.x;
child.position.y += dp.y;
handler(child);
}
// if (child.selected && child.draggable) {
// child.emit(
// 'transformstart',
// GraphicTransformEvent.init(child, 'shift')
// );
// child.position.x += dp.x;
// child.position.y += dp.y;
// }
});
} else {
app.selectedGraphics.forEach((g) => {
g.emit('transformstart', g);
g.position.x += dp.x;
g.position.y += dp.y;
handler(g);
// g.emit('transformstart', GraphicTransformEvent.init(g, 'shift'));
// g.position.x += dp.x;
// g.position.y += dp.y;
});
}
}
function updateGraphicPositionOnKeyboardEvent(app: GraphicApp, dp: IPointData) {
let dragStart = false;
if (dragStartDatas.length === 0) {
dragStartDatas = app.selectedGraphics.map((g) => g.saveData());
dragStart = true;
}
handleArrowKeyMoveGraphics(app, (g) => {
if (dragStart) {
g.shiftStartPoint = g.position.clone();
g.emit(
'transformstart',
GraphicTransformEvent.shift(g, ShiftData.new(g.shiftStartPoint))
);
} else {
g.shiftLastPoint = g.position.clone();
}
g.position.x += dp.x;
g.position.y += dp.y;
if (!dragStart) {
if (g.shiftStartPoint && g.shiftLastPoint) {
g.emit(
'transforming',
GraphicTransformEvent.shift(
g,
ShiftData.new(
g.shiftStartPoint,
g.position.clone(),
g.shiftLastPoint
)
)
);
}
}
});
}
function recordGraphicMoveOperation(app: GraphicApp) {
if (
dragStartDatas.length > 0 &&
@ -591,20 +606,18 @@ function recordGraphicMoveOperation(app: GraphicApp) {
app.opRecord.record(
new DragOperation(app, app.selectedGraphics, dragStartDatas, newData)
);
if (
app.selectedGraphics.length === 1 &&
app.selectedGraphics[0].hasSelectedChilds()
) {
recursiveChildren(app.selectedGraphics[0], (child) => {
if (child.selected && child.draggable) {
child.emit('transformend', child);
handleArrowKeyMoveGraphics(app, (g) => {
if (g.shiftStartPoint) {
g.emit(
'transformend',
GraphicTransformEvent.shift(
g,
ShiftData.new(g.shiftStartPoint, g.position.clone())
)
);
}
});
} else {
app.selectedGraphics.forEach((g) => {
g.emit('transformend', g);
});
}
}
dragStartDatas = [];
}

View File

@ -12,11 +12,11 @@ import {
import { GraphicQueryStore, GraphicStore } from '../core/GraphicStore';
import { GraphicIdGenerator } from '../core/IdGenerator';
import {
JlGraphic,
GraphicData,
GraphicState,
GraphicTemplate,
GraphicTransform,
JlGraphic,
} from '../core/JlGraphic';
import { AbsorbablePosition } from '../graphic';
import {
@ -28,11 +28,13 @@ import {
import { OperationRecord } from '../operation/JlOperation';
import {
CommonMouseTool,
GraphicDragEvent,
GraphicTransformPlugin,
IMouseToolOptions,
} from '../plugins';
import { GraphicCopyPlugin } from '../plugins/CopyPlugin';
import {
AppDragEvent,
DragPlugin,
InteractionPlugin,
InteractionPluginType,
ViewportMovePlugin,
@ -79,11 +81,25 @@ export class CanvasData implements ICanvasProperties {
this.viewportTransform = properties.viewportTransform;
}
copyFrom(properties: ICanvasProperties): void {
this.width = properties.width;
this.height = properties.height;
copyFrom(properties: ICanvasProperties): boolean {
let sizeChange = false;
if (properties.width <= 0 || properties.height <= 0) {
console.error('画布宽度/高度不能小于等于0');
} else {
const width = Math.floor(properties.width);
const height = Math.floor(properties.height);
if (this.width != width) {
this.width = width;
sizeChange = true;
}
if (this.height != height) {
this.height = height;
sizeChange = true;
}
}
this.backgroundColor = properties.backgroundColor;
this.viewportTransform = properties.viewportTransform;
return sizeChange;
}
clone(): CanvasData {
@ -92,13 +108,6 @@ export class CanvasData implements ICanvasProperties {
}
}
export enum CanvasEvent {
canvassizechange = 'canvassizechange',
propertiesupdate = 'propertiesupdate',
enterAbsorbableArea = 'enter-absorbable-area',
outAbsorbableArea = 'out-absorbable-area',
}
export class JlCanvas extends Container {
__JlCanvas = true;
type = 'Canvas';
@ -169,19 +178,15 @@ export class JlCanvas extends Container {
update(properties: ICanvasProperties) {
// 更新画布
if (
this.properties.width !== properties.width ||
this.properties.height !== properties.height
) {
this._properties.copyFrom(properties);
this.emit(CanvasEvent.canvassizechange, this);
} else {
this._properties.copyFrom(properties);
}
const old = this.properties.clone();
const sizeChange = this._properties.copyFrom(properties);
this.repaint();
if (sizeChange) {
this.app.updateViewport();
}
const vp = this.getViewport();
vp.loadTransform(properties.viewportTransform);
this.emit(CanvasEvent.propertiesupdate, this);
this.emit('dataupdate', this.properties, old);
}
addChild<U extends DisplayObject[]>(...children: U): U[0] {
@ -274,15 +279,11 @@ export interface GraphicAppEvents extends GlobalMixins.GraphicAppEvents {
'interaction-plugin-pause': [activeTool: InteractionPlugin]; // 交互插件停止
'options-update': [options: GraphicAppOptions]; // 配置更新
graphicselectedchange: [graphic: JlGraphic, selected: boolean];
graphicchildselectedchange: [
graphic: JlGraphic,
child: DisplayObject,
selected: boolean
];
scaleend: [obj: DisplayObject];
dragstart: [event: GraphicDragEvent];
dragmove: [event: GraphicDragEvent];
dragend: [event: GraphicDragEvent];
graphicchildselectedchange: [child: DisplayObject, selected: boolean];
'viewport-scaled': [vp: Viewport];
drag_op_start: [event: AppDragEvent];
drag_op_move: [event: AppDragEvent];
drag_op_end: [event: AppDragEvent];
destroy: [app: GraphicApp];
}
@ -298,6 +299,10 @@ export interface IGraphicAppConfig {
* 100
*/
maxOperationRecords?: number;
/**
*
*/
threshold?: number;
/**
*
*/
@ -413,11 +418,7 @@ export class GraphicApp extends EventEmitter<GraphicAppEvents> {
// 监听并通知缩放变化事件
this.viewport.on('zoomed-end', () => {
this.emit('scaleend', this.viewport);
});
this.canvas.on(CanvasEvent.canvassizechange, () => {
this.updateViewport();
this.emit('viewport-scaled', this.viewport);
});
this.opRecord = new OperationRecord();
@ -430,9 +431,15 @@ export class GraphicApp extends EventEmitter<GraphicAppEvents> {
// 添加通用交互插件
const tool = new CommonMouseTool(this);
tool.resume();
// drag插件
DragPlugin.new(this).resume();
// 视口移动插件
ViewportMovePlugin.new(this);
// 图形变换插件
GraphicTransformPlugin.new(this).resume();
this.app.ticker.add((dt: number) => {
this.queryStore.getAllGraphics().forEach((g) => {
g.animationMap.forEach((animation) => {
@ -686,10 +693,10 @@ export class GraphicApp extends EventEmitter<GraphicAppEvents> {
graphic.repaint();
this.emit('graphicstored', graphic);
graphic.on('childselected', (child) => {
this.emit('graphicchildselectedchange', graphic, child, true);
this.emit('graphicchildselectedchange', child, true);
});
graphic.on('childunselected', (child) => {
this.emit('graphicchildselectedchange', graphic, child, false);
this.emit('graphicchildselectedchange', child, false);
});
}
}
@ -814,7 +821,7 @@ export class GraphicApp extends EventEmitter<GraphicAppEvents> {
this.interactionPluginMap.delete(plugin.name);
}
private updateViewport(domWidth?: number, domHeight?: number): void {
updateViewport(domWidth?: number, domHeight?: number): void {
let screenWidth = this.viewport.screenWidth;
let screenHeight = this.viewport.screenHeight;
if (domWidth) {

View File

@ -29,7 +29,8 @@ DisplayObject.prototype._childEdit = false;
DisplayObject.prototype._transformSave = false;
DisplayObject.prototype._assistantAppendMap = null;
DisplayObject.prototype._draggable = false;
DisplayObject.prototype._dragStartPoint = null;
DisplayObject.prototype._shiftStartPoint = null;
DisplayObject.prototype._shiftLastPoint = null;
DisplayObject.prototype._scalable = false;
DisplayObject.prototype._keepAspectRatio = true;
DisplayObject.prototype._rotatable = false;
@ -82,12 +83,20 @@ Object.defineProperties(DisplayObject.prototype, {
this._draggable = v;
},
},
dragStartPoint: {
shiftStartPoint: {
get() {
return this._dragStartPoint;
return this._shiftStartPoint;
},
set(v) {
this._dragStartPoint = v;
this._shiftStartPoint = v;
},
},
shiftLastPoint: {
get() {
return this._shiftLastPoint;
},
set(v) {
this._shiftLastPoint = v;
},
},
scalable: {
@ -255,6 +264,9 @@ DisplayObject.prototype.getCanvas = function getCanvas() {
}
throw new Error(`图形${this.name}不在画布中`);
};
DisplayObject.prototype.isCanvas = function isCanvas(): boolean {
return Object.hasOwn(this, '__JlCanvas');
};
DisplayObject.prototype.getViewport = function getViewport() {
const canvas = this.getCanvas();
return canvas.parent as Viewport;
@ -815,7 +827,7 @@ export abstract class JlGraphic extends Container {
this.getDatas().copyFrom(data);
this.onDataChange(data, old);
this.loadTransformFrom(data);
this.emit('dataupdate', this, data, old);
this.emit('dataupdate', this.getDatas(), old);
this.repaint();
}
return update;
@ -850,7 +862,7 @@ export abstract class JlGraphic extends Container {
this.getStates().copyFrom(state);
this.onStateChange(state, old);
stateChange = true;
this.emit('stateupdate', this, state, old);
this.emit('stateupdate', this.getStates(), old);
this.repaint();
}
return stateChange;

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
declare namespace GlobalMixins {
type JlCanvasType = import('./app').JlCanvas;
type CanvasProperties = import('./app').ICanvasProperties;
@ -6,33 +7,21 @@ declare namespace GlobalMixins {
type GraphicData = import('./core').GraphicData;
type GraphicState = import('./core').GraphicState;
type GraphicTransform = import('./core').GraphicTransform;
type AppDragEventType = import('./plugins').GraphicDragEvent;
type BoundsGraphic = import('./graphic').BoundsGraphic;
type GraphicTransformEvent = import('./plugins').GraphicTransformEvent;
type BoundsGraphic = import('./plugins').BoundsGraphic;
type IPointDataType = import('pixi.js').IPointData;
type PointType = import('pixi.js').Point;
type DisplayObjectType = import('pixi.js').DisplayObject;
type ContainerType = import('pixi.js').Container;
interface DisplayObjectEvents {
canvassizechange: [JlCanvasType];
'enter-absorbable-area': [number | undefined, number | undefined];
'out-absorbable-area': [number | undefined, number | undefined];
transforming: [JlCanvasType];
dataupdate: [JlGraphicType, GraphicData, GraphicData | undefined];
pointupdate: [obj: DisplayObjectType, points: IPointDataType[]];
stateupdate: [JlGraphicType, GraphicState, GraphicState | undefined];
dataupdate: [newVal: any, oldVal: any];
stateupdate: [newVal: any, oldVal: any];
repaint: [DisplayObjectType];
propertiesupdate: [JlGraphicType | JlCanvasType];
dragstart: [AppDragEventType];
dragmove: [AppDragEventType];
dragend: [AppDragEventType];
scalestart: [DisplayObjectType];
scalemove: [DisplayObjectType];
scaleend: [DisplayObjectType];
rotatestart: [DisplayObjectType];
rotatemove: [DisplayObjectType];
rotateend: [DisplayObjectType];
transformstart: [DisplayObjectType];
transformend: [DisplayObjectType];
transformstart: [e: GraphicTransformEvent];
transforming: [e: GraphicTransformEvent];
transformend: [e: GraphicTransformEvent];
selected: [DisplayObjectType];
unselected: [DisplayObjectType];
childselected: [DisplayObjectType];
@ -57,8 +46,10 @@ declare namespace GlobalMixins {
assistantAppendMap: Map<string, DisplayObjectType>;
_draggable: boolean; // 是否可拖拽
draggable: boolean;
_dragStartPoint: PointType | null; // 拖拽起始坐标
dragStartPoint: PointType | null;
_shiftStartPoint: PointType | null; // 位移起始坐标
shiftStartPoint: PointType | null;
_shiftLastPoint: PointType | null; // 位移上一个事件时坐标
shiftLastPoint: PointType | null;
_scalable: boolean; // 是否可缩放
scalable: boolean;
_keepAspectRatio: boolean; // 缩放是否保持纵横比,默认保持
@ -86,6 +77,7 @@ declare namespace GlobalMixins {
onRemoveFromCanvas(): void; //从画布移除处理
isInCanvas(): boolean; // 是否添加到画布中
getCanvas(): JlCanvasType; // 获取所在画布
isCanvas(): boolean; // 是否画布对象
getViewport(): Viewport; // 获取视口
getGraphicApp(): GraphicApp; // 获取图形app
localToCanvasPoint(p: IPointData): PointType; // 图形本地坐标转为画布坐标

View File

@ -32,7 +32,7 @@ export class DraggablePoint extends Graphics implements VectorGraphic {
constructor(point: IPointData) {
super(DraggablePointGraphic.geometry);
this.position.copyFrom(point);
this.interactive = true;
this.eventMode = 'static';
this.draggable = true;
this.cursor = 'crosshair';
VectorGraphicUtil.handle(this);

View File

@ -18,14 +18,14 @@ export class VectorGraphicUtil {
): void {
if (!obj.scaledListenerOn) {
obj.scaledListenerOn = true;
obj.getGraphicApp().on('scaleend', onScaleChange);
obj.getGraphicApp().on('viewport-scaled', onScaleChange);
}
};
const unregisterScaleChange = function unregisterScaleChange(
obj: VectorGraphic
): void {
obj.scaledListenerOn = false;
obj.getGraphicApp().off('scaleend', onScaleChange);
obj.getGraphicApp().off('viewport-scaled', onScaleChange);
};
obj.onAddToCanvas = function onAddToCanvas() {
obj.updateOnScaled();

View File

@ -2,4 +2,3 @@ export * from './VectorGraphic';
export * from './VectorText';
export * from './DraggablePoint';
export * from './AbsorbablePosition';
export * from './GraphicTransformGraphics';

View File

@ -1,77 +1,11 @@
import {
Container,
DisplayObject,
FederatedMouseEvent,
Graphics,
Point,
} from 'pixi.js';
import { DisplayObject, FederatedMouseEvent, Graphics, Point } from 'pixi.js';
import { GraphicApp, JlCanvas } from '../app';
import { JlGraphic } from '../core';
import { AppInteractionPlugin } from './InteractionPlugin';
import { AbsorbablePosition } from '../graphic';
import { recursiveChildren } from '../utils';
export class GraphicDragEvent {
/**
*
*/
event: FederatedMouseEvent;
app: GraphicApp;
target: DisplayObject;
start: Point;
point: Point;
dx = 0; // 距上一个move点的x移动距离
dy = 0; // 距上一个move点的y移动距离
constructor(
app: GraphicApp,
event: FederatedMouseEvent,
target: DisplayObject,
start: Point,
point: Point,
dx?: number,
dy?: number
) {
this.app = app;
this.event = event;
this.target = target;
this.start = start;
this.point = point;
if (dx) {
this.dx = dx;
}
if (dy) {
this.dy = dy;
}
}
// static buildJlGraphicDragEvent(
// graphic: JlGraphic,
// source: GraphicDragEvent
// ): GraphicDragEvent {
// const e = new GraphicDragEvent(
// source.app,
// source.event,
// source.target,
// source.start,
// source.point,
// source.dx,
// source.dy
// );
// return e;
// }
/**
* x方向距离
*/
public get dsx(): number {
return this.point.x - this.start.x;
}
/**
* y方向距离
*/
public get dsy(): number {
return this.point.y - this.start.y;
}
}
import {
AppDragEvent,
AppInteractionPlugin,
ViewportMovePlugin,
} from './InteractionPlugin';
type GraphicSelectFilter = (graphic: JlGraphic) => boolean;
@ -88,10 +22,6 @@ export interface IMouseToolOptions {
* ,
*/
wheelZoom?: boolean;
/**
* boxSelect , 3
*/
threshold?: number;
/**
*
*/
@ -102,13 +32,11 @@ class CompleteMouseToolOptions implements IMouseToolOptions {
boxSelect: boolean;
viewportDrag: boolean;
wheelZoom: boolean;
threshold: number;
selectFilter?: GraphicSelectFilter | undefined;
constructor() {
this.boxSelect = true;
this.viewportDrag = true;
this.wheelZoom = true;
this.threshold = 3;
}
update(options: IMouseToolOptions) {
if (options.boxSelect != undefined) {
@ -120,9 +48,6 @@ class CompleteMouseToolOptions implements IMouseToolOptions {
if (options.wheelZoom != undefined) {
this.wheelZoom = options.wheelZoom;
}
if (options.threshold != undefined) {
this.threshold = options.threshold;
}
this.selectFilter = options.selectFilter;
}
}
@ -135,29 +60,25 @@ const DefaultSelectToolOptions: CompleteMouseToolOptions =
*/
export class CommonMouseTool extends AppInteractionPlugin {
static Name = 'mouse-tool';
static SelectBox = '__select_box';
options: CompleteMouseToolOptions;
box: Graphics = new Graphics();
box: Graphics;
leftDownTarget: DisplayObject | null = null;
start: Point | null = null;
last: Point | null = null;
drag = false;
graphicSelect = false;
rightTarget: DisplayObject | null = null;
/**
*
*/
absorbablePositions?: AbsorbablePosition[];
apContainer: Container;
static AbsorbablePosisiontsName = '__AbsorbablePosisionts';
constructor(graphicApp: GraphicApp) {
super(CommonMouseTool.Name, graphicApp);
this.options = DefaultSelectToolOptions;
this.apContainer = new Container();
this.apContainer.name = CommonMouseTool.AbsorbablePosisiontsName;
this.app.canvas.addAssistantAppend(this.apContainer);
this.box = new Graphics();
this.box.name = CommonMouseTool.SelectBox;
this.box.visible = false;
this.app.canvas.addAssistantAppends(this.box);
graphicApp.on('options-update', (options) => {
if (options.mouseToolOptions) {
this.options.update(options.mouseToolOptions);
@ -166,17 +87,16 @@ export class CommonMouseTool extends AppInteractionPlugin {
this.resume();
}
}
if (options.absorbablePositions) {
this.absorbablePositions = options.absorbablePositions;
}
});
}
bind(): void {
const canvas = this.app.canvas;
canvas.interactive = true;
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) {
this.app.viewport.drag({
mouseButtons: 'right',
@ -196,9 +116,11 @@ export class CommonMouseTool extends AppInteractionPlugin {
// 确保所有事件取消监听
canvas.off('mousedown', this.onMouseDown, this);
canvas.off('mouseup', this.onMouseUp, this);
canvas.off('mousemove', this.onDrag, this);
canvas.off('mouseup', this.onDragEnd, this);
canvas.off('mouseupoutside', this.onDragEnd, 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('rightdown', this.setCursor, this);
canvas.off('rightup', this.resumeCursor, this);
@ -208,6 +130,41 @@ export class CommonMouseTool extends AppInteractionPlugin {
this.clearCache();
}
onDragStart(event: AppDragEvent) {
// console.log(
// 'start',
// `pointerType:${event.original.pointerType},pointerId:${event.original.pointerId},button: ${event.original.button},buttons:${event.original.buttons}`
// );
if (this.boxSelect && event.target.isCanvas() && event.isLeftButton) {
this.box.visible = true;
this.app.interactionPlugin(ViewportMovePlugin.Name).resume();
}
this.drag = true;
}
onDragMove(event: AppDragEvent) {
// console.log(
// 'moving',
// `pointerType:${event.original.pointerType},pointerId:${event.original.pointerId},button: ${event.original.button},buttons:${event.original.buttons}`
// );
if (this.boxSelect && event.target.isCanvas()) {
this.boxSelectDraw(event.start, event.end);
}
}
onDragEnd(event: AppDragEvent) {
// console.log(
// 'end',
// `pointerType:${event.original.pointerType},pointerId:${event.original.pointerId},button: ${event.original.button},buttons:${event.original.buttons}`
// );
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;
}
}
setCursor(e: FederatedMouseEvent) {
this.rightTarget = e.target as DisplayObject;
if (e.target instanceof JlCanvas && this.app.app.view.style) {
@ -228,19 +185,9 @@ export class CommonMouseTool extends AppInteractionPlugin {
onMouseDown(e: FederatedMouseEvent) {
this.leftDownTarget = e.target as DisplayObject;
const canvas = this.app.canvas;
let enableDrag = false;
this.graphicSelect = false;
if (e.target instanceof JlCanvas) {
// 画布
if (this.options.boxSelect) {
// 框选
enableDrag = true;
}
} else {
// 图形
const graphic = (e.target as DisplayObject).getGraphic();
const graphic = this.leftDownTarget.getGraphic();
if (graphic) {
const app = this.app;
// 图形选中
@ -260,29 +207,6 @@ export class CommonMouseTool extends AppInteractionPlugin {
graphic.exitChildEdit();
}
}
// 判断是否开启拖拽
if (
!this.leftDownTarget.isAssistantAppend() &&
!graphic.draggable &&
!this.leftDownTarget.draggable
) {
console.debug(
'图形未开启拖拽,若需开启,设置draggable = true',
graphic,
this.leftDownTarget
);
return;
}
}
enableDrag = true;
}
if (enableDrag) {
// 初始化拖拽监听
// console.log('drag 开始');
this.start = this.app.toCanvasCoordinates(e.global);
canvas.on('mousemove', this.onDrag, this);
canvas.on('mouseupoutside', this.onDragEnd, this);
}
}
@ -292,9 +216,7 @@ export class CommonMouseTool extends AppInteractionPlugin {
*/
onMouseUp(e: FederatedMouseEvent) {
const app = this.app;
if (this.drag) {
this.onDragEnd(e);
} else {
if (!this.drag) {
const target = e.target as DisplayObject;
const graphic = (e.target as DisplayObject).getGraphic();
if (
@ -346,12 +268,8 @@ export class CommonMouseTool extends AppInteractionPlugin {
*
*/
clearCache() {
this.start = null;
this.last = null;
this.drag = false;
this.leftDownTarget = null;
this.box.clear();
this.app.canvas.removeChild(this.box);
}
public get boxSelect(): boolean | undefined {
@ -362,10 +280,6 @@ export class CommonMouseTool extends AppInteractionPlugin {
return this.options.selectFilter;
}
public get threshold(): number {
return this.options.threshold;
}
/**
*
*/
@ -408,175 +322,4 @@ export class CommonMouseTool extends AppInteractionPlugin {
});
app.updateSelected(...selects);
}
onDrag(e: FederatedMouseEvent) {
if (this.start) {
const current = this.app.toCanvasCoordinates(e.global);
const dragStart =
Math.abs(current.x - this.start.x) > this.threshold ||
Math.abs(current.y - this.start.y) > this.threshold;
if (this.leftDownTarget instanceof JlCanvas) {
// 画布drag
if (!this.drag && dragStart) {
this.drag = true;
this.app.canvas.addChild(this.box);
}
if (this.drag) {
this.boxSelectDraw(this.start, current);
// this.boxSelectGraphicCheck();
}
} else {
// 图形drag
if (this.start && !this.drag && dragStart) {
// 启动拖拽
if (this.leftDownTarget) {
const s = this.start;
const dragEvent = new GraphicDragEvent(
this.app,
e,
this.leftDownTarget,
s,
s
);
this.handleDragEvent(dragEvent, 'dragstart');
}
this.last = this.start.clone();
this.drag = true;
}
// drag移动处理
if (this.drag && this.last && this.start) {
const current = this.app.toCanvasCoordinates(e.global);
const dx = current.x - this.last.x;
const dy = current.y - this.last.y;
if (this.leftDownTarget) {
const dragEvent = new GraphicDragEvent(
this.app,
e,
this.leftDownTarget,
this.start,
current,
dx,
dy
);
this.handleDragEvent(dragEvent, 'dragmove');
}
this.last = current;
}
}
}
}
/**
* drag操作结束处理
* @param e
*/
onDragEnd(e: FederatedMouseEvent) {
if (this.leftDownTarget instanceof JlCanvas) {
this.boxSelectGraphicCheck();
} else {
if (this.drag) {
const end = this.app.toCanvasCoordinates(e.global);
if (this.start && this.leftDownTarget) {
const dragEvent = new GraphicDragEvent(
this.app,
e,
this.leftDownTarget,
this.start,
end
);
this.handleDragEvent(dragEvent, 'dragend');
}
}
}
const canvas = this.app.canvas;
canvas.off('mousemove', this.onDrag, this);
canvas.off('mouseup', this.onDragEnd, this);
canvas.off('mouseupoutside', this.onDragEnd, this);
this.clearCache();
}
/**
* ()
* @param dragEvent
* @param type
*/
handleDragEvent(
dragEvent: GraphicDragEvent,
type: 'dragstart' | 'dragmove' | 'dragend'
): void {
const graphic = dragEvent.target.getGraphic();
const targets: DisplayObject[] = [];
if (!graphic) {
targets.push(dragEvent.target);
} else {
if (
dragEvent.target.isGraphicChild() &&
dragEvent.target.selected &&
dragEvent.target.draggable
) {
// 图形子元素
recursiveChildren(graphic, (child) => {
if (child.selected && child.draggable) {
targets.push(child);
}
});
} else {
// 图形对象
targets.push(...this.app.selectedGraphics);
}
}
if (type === 'dragstart') {
targets.forEach((target) => {
target.dragStartPoint = target.position.clone();
target.emit(type, dragEvent);
if (!target.isAssistantAppend()) {
target.emit('transformstart', target);
}
});
// 显示吸附图形
if (this.absorbablePositions && this.absorbablePositions.length > 0) {
this.apContainer.removeChildren();
this.apContainer.addChild(...this.absorbablePositions);
}
} else {
// 处理位移
targets.forEach((target) => {
if (target.dragStartPoint) {
target.position.set(
target.dragStartPoint.x + dragEvent.dsx,
target.dragStartPoint.y + dragEvent.dsy
);
}
});
// 处理吸附
if (this.absorbablePositions) {
for (let i = 0; i < this.absorbablePositions.length; i++) {
const ap = this.absorbablePositions[i];
if (ap.tryAbsorb(...targets)) {
break;
}
}
}
// 事件发布
targets.forEach((target) => {
target.emit(type, dragEvent);
if (type === 'dragend' && !target.isAssistantAppend()) {
target.emit('transformend', target);
}
});
if (type === 'dragend') {
if (this.app.selectedGraphics.length == 1) {
this.app.selectedGraphics[0].repaint();
}
targets.forEach((target) => {
target.dragStartPoint = null;
});
// 移除吸附图形
this.absorbablePositions = [];
this.apContainer.removeChildren();
}
}
this.app.emit(type, dragEvent);
}
}

View File

@ -10,9 +10,9 @@ import {
import { JlGraphic } from '../core';
import { DraggablePoint } from '../graphic';
import { calculateMirrorPoint, distance2 } from '../utils';
import { GraphicDragEvent } from './CommonMousePlugin';
import { MenuItemOptions, MenuOptions } from '../ui/Menu';
import { ContextMenu } from '../ui/ContextMenu';
import { GraphicTransformEvent, ShiftData } from './GraphicTransformPlugin';
export abstract class GraphicEditPlugin<
DO extends DisplayObject = DisplayObject
@ -76,16 +76,16 @@ export abstract class LineEditPlugin extends GraphicEditPlugin<ILineGraphic> {
constructor(g: ILineGraphic) {
super(g);
this.linePoints = g.linePoints;
this.graphic.on('pointupdate', this.reset, this);
this.graphic.on('dataupdate', this.reset, this);
}
destroy(options?: boolean | IDestroyOptions | undefined): void {
this.graphic.off('pointupdate', this.reset, this);
this.graphic.off('dataupdate', this.reset, this);
super.destroy(options);
}
reset(_obj: DisplayObject, points: IPointData[]): void {
this.linePoints = points;
reset(): void {
this.linePoints = this.graphic.linePoints;
this.removeChildren();
this.editedPoints.splice(0, this.editedPoints.length);
this.initEditPoints();
@ -125,7 +125,7 @@ export class PolylineEditPlugin extends LineEditPlugin {
for (let i = 0; i < cps.length; i++) {
const p = cps[i];
const dp = new DraggablePoint(p);
dp.on('dragmove', () => {
dp.on('transforming', () => {
const tlp = this.graphic.canvasToLocalPoint(dp.position);
const cp = this.linePoints[i];
cp.x = tlp.x;
@ -181,13 +181,13 @@ export class BezierCurveEditPlugin extends LineEditPlugin {
this.initEditPoints();
}
reset(_obj: DisplayObject, points: IPointData[]): void {
reset(): void {
this.auxiliaryLines.splice(0, this.auxiliaryLines.length);
super.reset(_obj, points);
super.reset();
}
initEditPoints() {
console.log('initEditPoints');
// console.log('initEditPoints');
const cps = this.graphic.localToCanvasPoints(...this.linePoints);
for (let i = 0; i < cps.length; i++) {
const p = cps[i];
@ -240,19 +240,20 @@ export class BezierCurveEditPlugin extends LineEditPlugin {
EditPointContextMenu.open(e.global);
}
});
dp.on('dragmove', (de: GraphicDragEvent) => {
dp.on('transforming', (e: GraphicTransformEvent) => {
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<ShiftData>();
const fp = this.linePoints[i - 1];
const np = this.linePoints[i + 1];
fp.x = fp.x + de.dx;
fp.y = fp.y + de.dy;
np.x = np.x + de.dx;
np.y = np.y + de.dy;
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];

View File

@ -6,20 +6,364 @@ import {
Point,
Polygon,
} from 'pixi.js';
import { GraphicDragEvent, KeyListener } from '../plugins';
import {
AppDragEvent,
BaseInteractionPlugin,
InteractionPluginType,
KeyListener,
} from '.';
import { GraphicApp } from '../app';
import { JlGraphic } from '../core';
import { AbsorbablePosition } from '../graphic';
import { DraggablePoint } from '../graphic/DraggablePoint';
import {
angleToAxisx,
calculateLineMidpoint,
convertRectangleToPolygonPoints,
distance,
calculateLineMidpoint,
recursiveChildren,
} from '../utils';
import { DraggablePoint } from './DraggablePoint';
const BoundsLineStyle = {
width: 1,
color: 0x29b6f2,
alpha: 1,
};
export class ShiftData {
/**
*
*/
startPosition: Point;
/**
*
*/
lastPosition?: Point;
/**
*
*/
currentPosition?: Point;
constructor(
startPosition: Point,
currentPosition?: Point,
lastPosition?: Point
) {
this.startPosition = startPosition;
this.lastPosition = lastPosition;
this.currentPosition = currentPosition;
}
static new(
startPosition: Point,
currentPosition?: Point,
lastPosition?: Point
) {
return new ShiftData(startPosition, currentPosition, lastPosition);
}
public get dx(): number {
if (!this.lastPosition || !this.currentPosition) {
throw new Error('错误的位移数据或阶段');
}
return this.currentPosition.x - this.lastPosition.x;
}
public get dy(): number {
if (!this.lastPosition || !this.currentPosition) {
throw new Error('错误的位移数据或阶段');
}
return this.currentPosition.y - this.lastPosition.y;
}
public get dsx(): number {
if (!this.currentPosition) {
throw new Error('错误的位移数据或阶段');
}
return this.currentPosition.x - this.startPosition.x;
}
public get dsy(): number {
if (!this.currentPosition) {
throw new Error('错误的位移数据或阶段');
}
return this.currentPosition.y - this.startPosition.y;
}
}
export class ScaleData {
start: Point;
current?: Point;
last?: Point;
constructor(start: Point, current?: Point, last?: Point) {
this.start = start;
this.current = current;
this.last = last;
}
static new(start: Point, current?: Point, last?: Point) {
return new ScaleData(start, current, last);
}
}
export type TransformData = ShiftData | null;
/**
*
*/
export class GraphicTransformEvent {
/**
*
*/
target: DisplayObject;
type: 'shift' | 'rotate' | 'scale' | 'skew';
data: TransformData;
constructor(
target: DisplayObject,
type: 'shift' | 'rotate' | 'scale' | 'skew',
data: TransformData
) {
this.target = target;
this.type = type;
this.data = data;
}
getData<D extends TransformData>(): D {
return this.data as D;
}
static shift(target: DisplayObject, data: ShiftData) {
return new GraphicTransformEvent(target, 'shift', data);
}
static scale(target: DisplayObject) {
return new GraphicTransformEvent(target, 'scale', null);
}
static rotate(target: DisplayObject) {
return new GraphicTransformEvent(target, 'rotate', null);
}
static skew(target: DisplayObject) {
return new GraphicTransformEvent(target, 'skew', null);
}
isShift(): boolean {
return this.type === 'shift';
}
isRotate(): boolean {
return this.type === 'rotate';
}
isScale(): boolean {
return this.type === 'scale';
}
isSkew(): boolean {
return this.type === 'skew';
}
}
export class GraphicTransformPlugin extends BaseInteractionPlugin {
static Name = '__graphic_transform_plugin';
/**
*
*/
absorbablePositions?: AbsorbablePosition[];
apContainer: Container;
static AbsorbablePosisiontsName = '__AbsorbablePosisionts';
constructor(app: GraphicApp) {
super(app, GraphicTransformPlugin.Name, InteractionPluginType.Other);
this.apContainer = new Container();
this.apContainer.name = GraphicTransformPlugin.AbsorbablePosisiontsName;
this.app.canvas.addAssistantAppend(this.apContainer);
app.on('options-update', (options) => {
if (options.absorbablePositions) {
this.absorbablePositions = options.absorbablePositions;
}
});
}
static new(app: GraphicApp) {
return new GraphicTransformPlugin(app);
}
bind(): void {
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.onGraphicChildSelectedChange,
this
);
}
unbind(): void {
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.onGraphicChildSelectedChange,
this
);
}
getDraggedTargets(e: AppDragEvent): DisplayObject[] {
const targets: DisplayObject[] = [];
if (e.target.isGraphicChild() && e.target.selected && e.target.draggable) {
const graphic = e.target.getGraphic() as JlGraphic;
// 图形子元素
recursiveChildren(graphic, (child) => {
if (child.selected && child.draggable) {
targets.push(child);
}
});
} else if (
(e.target.isGraphic() || e.target.isGraphicChild()) &&
e.target.getGraphic()?.draggable
) {
// 图形对象
targets.push(...this.app.selectedGraphics);
} else if (e.target.draggable) {
targets.push(e.target);
}
return targets;
}
onDragStart(e: AppDragEvent) {
if (!e.target.isCanvas()) {
const targets: DisplayObject[] = this.getDraggedTargets(e);
if (targets.length > 0) {
targets.forEach((target) => {
target.shiftStartPoint = target.position.clone();
target.emit(
'transformstart',
GraphicTransformEvent.shift(
target,
ShiftData.new(target.shiftStartPoint)
)
);
});
// 显示吸附图形
if (this.absorbablePositions && this.absorbablePositions.length > 0) {
this.apContainer.removeChildren();
this.apContainer.addChild(...this.absorbablePositions);
}
}
}
}
onDragMove(e: AppDragEvent) {
if (!e.target.isCanvas()) {
const targets: DisplayObject[] = this.getDraggedTargets(e);
if (targets.length > 0) {
// 处理位移
targets.forEach((target) => {
if (target.shiftStartPoint) {
target.shiftLastPoint = target.position.clone();
target.position.set(
target.shiftStartPoint.x + e.dsx,
target.shiftStartPoint.y + e.dsy
);
}
});
// 处理吸附
if (this.absorbablePositions) {
for (let i = 0; i < this.absorbablePositions.length; i++) {
const ap = this.absorbablePositions[i];
if (ap.tryAbsorb(...targets)) {
break;
}
}
}
// 事件发布
targets.forEach((target) => {
if (target.shiftStartPoint && target.shiftLastPoint) {
target.emit(
'transforming',
GraphicTransformEvent.shift(
target,
ShiftData.new(
target.shiftStartPoint,
target.position.clone(),
target.shiftLastPoint
)
)
);
}
});
}
}
}
onDragEnd(e: AppDragEvent) {
if (!e.target.isCanvas()) {
const targets: DisplayObject[] = this.getDraggedTargets(e);
targets.forEach((target) => {
if (target.shiftStartPoint) {
target.emit(
'transformend',
GraphicTransformEvent.shift(
target,
ShiftData.new(target.shiftStartPoint, target.position.clone())
)
);
}
target.shiftStartPoint = null;
});
}
this.clearCache();
}
/**
*
*/
clearCache() {
// 移除吸附图形
this.absorbablePositions = [];
this.apContainer.removeChildren();
}
onGraphicSelectedChange(g: DisplayObject, selected: boolean) {
let br = g.getAssistantAppend<BoundsGraphic>(BoundsGraphic.Name);
if (!br) {
// 绘制辅助包围框
br = new BoundsGraphic(g);
}
if (selected) {
if (br) {
br.redraw();
br.visible = true;
}
} else {
if (br) {
br.visible = false;
}
}
if (g.scalable) {
// 缩放点
let sp = g.getAssistantAppend<TransformPoints>(TransformPoints.Name);
if (!sp) {
sp = new TransformPoints(g);
}
if (selected) {
sp.update();
sp.visible = true;
} else {
sp.visible = false;
}
}
}
onGraphicChildSelectedChange(child: DisplayObject, selected: boolean) {
let br = child.getAssistantAppend<BoundsGraphic>(BoundsGraphic.Name);
if (!br) {
// 绘制辅助包围框
br = new BoundsGraphic(child);
}
if (selected) {
br.redraw();
br.visible = true;
} else {
br.visible = false;
}
}
}
/**
*
@ -118,17 +462,17 @@ export class TransformPoints extends Container {
this.obj.on('transformend', this.onObjTransformEnd, this);
this.obj.on('repaint', this.onGraphicRepaint, this);
this.children.forEach((dp) => {
dp.on('dragstart', this.onScaleDragStart, this);
dp.on('dragmove', this.onScaleDragMove, this);
dp.on('dragend', this.onScaleDragEnd, this);
dp.on('transformstart', this.onScaleDragStart, this);
dp.on('transforming', this.onScaleDragMove, this);
dp.on('transformend', this.onScaleDragEnd, this);
});
// 创建旋转拖拽点
this.rotatePoint = new DraggablePoint(new Point());
this.addChild(this.rotatePoint);
this.rotatePoint.on('dragstart', this.onRotateStart, this);
this.rotatePoint.on('dragmove', this.onRotateMove, this);
this.rotatePoint.on('dragend', this.onRotateEnd, this);
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();
// 初始化旋转角度修改键盘监听器
@ -164,23 +508,24 @@ export class TransformPoints extends Container {
*
* @param de
*/
onRotateStart(de: GraphicDragEvent) {
onRotateStart(de: GraphicTransformEvent) {
this.hideAll();
const assistantPoint = this.obj.localToCanvasPoint(this.obj.pivot);
this.rotatePivot.copyFrom(assistantPoint);
this.rotateLastPoint.copyFrom(de.target.position);
this.startAngle = this.obj.angle;
const app = this.obj.getGraphicApp();
this.rotateAngleStepKeyListeners.forEach((listener) =>
this.obj.getGraphicApp().addKeyboardListener(listener)
app.addKeyboardListener(listener)
);
this.obj.emit('rotatestart', this.obj);
this.obj.emit('transformstart', this.obj);
this.obj.emit('transformstart', GraphicTransformEvent.rotate(this.obj));
// app.emit('transformstart', app.selectedGraphics);
}
/**
*
* @param de
*/
onRotateMove(de: GraphicDragEvent) {
onRotateMove(de: GraphicTransformEvent) {
// 旋转角度计算逻辑取锚点y负方向一点作为旋转点求旋转点和锚点所形成的直线与x轴角度此角度+90°即为最终旋转角度再将旋转角度限制到(-180,180]之间
let angle = angleToAxisx(this.rotatePivot, de.target.position);
angle = Math.floor(angle / this.angleStep) * this.angleStep;
@ -189,7 +534,7 @@ export class TransformPoints extends Container {
angle = angle - 360;
}
this.obj.angle = angle;
this.obj.emit('rotatemove', this.obj);
// this.obj.emit('rotatemove', this.obj);
}
/**
*
@ -200,8 +545,7 @@ export class TransformPoints extends Container {
this.rotateAngleStepKeyListeners.forEach((listener) =>
this.obj.getGraphicApp().removeKeyboardListener(listener)
);
this.obj.emit('rotateend', this.obj);
this.obj.emit('transformend', this.obj);
this.obj.emit('transformend', GraphicTransformEvent.rotate(this.obj));
}
/**
@ -227,11 +571,10 @@ export class TransformPoints extends Container {
this.lbLocal.copyFrom(p3);
this.lLocal.copyFrom(calculateLineMidpoint(p0, p3));
this.originScale = this.obj.scale.clone();
this.obj.emit('scalestart', this.obj);
this.obj.emit('transformstart', this.obj);
this.obj.emit('transformstart', GraphicTransformEvent.scale(this.obj));
}
onScaleDragMove(de: GraphicDragEvent) {
onScaleDragMove(e: GraphicTransformEvent) {
// 缩放计算逻辑共8个方向缩放点根据所拖拽的方向:
// 1,计算缩放为1时的此点在拖拽开始时的位置到锚点x、y距离
// 2,计算拖拽点的当前位置到锚点的x、y方向距离
@ -243,38 +586,40 @@ export class TransformPoints extends Container {
let width = 0;
let height = 0;
this.obj.scale.copyFrom(defaultScale);
const point = this.obj.toLocal(de.event.global);
if (de.target === this.ltScalePoint) {
const point = this.obj.toLocal(
e.target.parent.localToScreenPoint(e.target.position)
);
if (e.target === this.ltScalePoint) {
// 左上角
originWidth = Math.abs(this.ltLocal.x - this.scalePivot.x);
originHeight = Math.abs(this.ltLocal.y - this.scalePivot.y);
width = Math.abs(point.x - this.scalePivot.x);
height = Math.abs(point.y - this.scalePivot.y);
} else if (de.target == this.tScalePoint) {
} 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 (de.target == this.rtScalePoint) {
} 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 (de.target == this.rScalePoint) {
} 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 (de.target == this.rbScalePoint) {
} 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 (de.target == this.bScalePoint) {
} 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 (de.target == this.lbScalePoint) {
} else if (e.target == this.lbScalePoint) {
// 左下
originWidth = Math.abs(this.lbLocal.x - this.scalePivot.x);
originHeight = Math.abs(this.lbLocal.y - this.scalePivot.y);
@ -305,9 +650,7 @@ export class TransformPoints extends Container {
onScaleDragEnd() {
this.showAll();
this.obj.emit('scaleend', this.obj);
this.obj.emit('transformend', this.obj);
this.obj.getGraphicApp().emit('scaleend', this.obj);
this.obj.emit('transformend', GraphicTransformEvent.scale(this.obj));
}
hideOthers(dg: DisplayObject) {
@ -433,6 +776,11 @@ export class TransformPoints extends Container {
*/
export class BoundsGraphic extends Graphics {
static Name = '_BoundsRect';
static BoundsLineStyle = {
width: 1,
color: 0x29b6f2,
alpha: 1,
};
obj: DisplayObject;
constructor(graphic: DisplayObject) {
super();
@ -470,6 +818,6 @@ export class BoundsGraphic extends Graphics {
redraw() {
this.visible = false; // 屏蔽包围框本身
const bounds = new Polygon(this.obj.localBoundsToCanvasPoints());
this.clear().lineStyle(BoundsLineStyle).drawShape(bounds);
this.clear().lineStyle(BoundsGraphic.BoundsLineStyle).drawShape(bounds);
}
}

View File

@ -1,5 +1,10 @@
import { FederatedMouseEvent } from 'pixi.js';
import { GraphicApp } from '../app/JlGraphicApp';
import {
DisplayObject,
FederatedMouseEvent,
FederatedPointerEvent,
Point,
} from 'pixi.js';
import { GraphicApp, IGraphicAppConfig } from '../app/JlGraphicApp';
import { JlGraphic } from '../core/JlGraphic';
export enum InteractionPluginType {
@ -16,14 +21,14 @@ export interface InteractionPlugin {
name: string; // 唯一标识
app: GraphicApp;
/**
*
*/
pause(): void;
/**
*
*/
resume(): void;
/**
*
*/
pause(): void;
/**
*
*/
@ -34,44 +39,243 @@ export interface InteractionPlugin {
destroy(): void;
}
export class ViewportMovePlugin implements InteractionPlugin {
static Name = '__viewport-move';
readonly _type = InteractionPluginType.Other;
name = ViewportMovePlugin.Name; // 唯一标识
export abstract class BaseInteractionPlugin implements InteractionPlugin {
readonly _type: string;
name: string; // 唯一标识
app: GraphicApp;
_pause: boolean;
constructor(app: GraphicApp, name: string, type: string) {
this._type = type;
this.app = app;
this.name = name;
this._pause = true;
app.registerInteractionPlugin(this);
}
/**
*
*/
resume(): void {
this.bind();
this._pause = false;
}
/**
*
*/
pause(): void {
this.unbind();
this._pause = true;
}
abstract bind(): void;
abstract unbind(): void;
/**
*
*/
isActive(): boolean {
return !this._pause;
}
/**
* (),app中移除
*/
destroy(): void {
this.pause();
this.app.removeInteractionPlugin(this);
}
}
export class AppDragEvent {
app: GraphicApp;
type: 'start' | 'move' | 'end';
target: DisplayObject;
original: FederatedPointerEvent;
start: Point;
constructor(
app: GraphicApp,
type: 'start' | 'move' | 'end',
target: DisplayObject,
original: FederatedPointerEvent,
start: Point
) {
this.app = app;
this.type = type;
this.target = target;
this.original = original;
this.start = start;
}
public get isMouse(): boolean {
return this.original.pointerType === 'mouse';
}
public get isLeftButton(): boolean {
return (
this.isMouse &&
((this.original.button === -1 && this.original.buttons === 1) ||
(this.original.button === 0 && this.original.buttons === 0))
);
}
public get isRightButton(): boolean {
return (
this.isMouse &&
((this.original.button === -1 && this.original.buttons === 2) ||
(this.original.button === 2 && this.original.buttons === 0))
);
}
public get isMiddleButton(): boolean {
return (
this.isMouse &&
((this.original.button === -1 && this.original.buttons === 4) ||
(this.original.button === 1 && this.original.buttons === 0))
);
}
public get isTouch(): boolean {
return this.original.pointerType === 'touch';
}
public get end(): Point {
return this.app.toCanvasCoordinates(this.original.global);
}
public get dx(): number {
const move = this.original.movement;
return move.x / this.app.viewport.scaled;
}
public get dy(): number {
const move = this.original.movement;
return move.y / this.app.viewport.scaled;
}
public get dsx(): number {
return this.end.x - this.start.x;
}
public get dsy(): number {
return this.end.y - this.start.y;
}
}
/**
*
*/
export class DragPlugin extends BaseInteractionPlugin {
static Name = '__drag_operation_plugin';
threshold = 3;
target: DisplayObject | null = null;
start: Point | null = null;
drag = false;
constructor(app: GraphicApp) {
super(app, DragPlugin.Name, InteractionPluginType.Other);
app.on('options-update', (options: IGraphicAppConfig) => {
if (options.threshold !== undefined) {
this.threshold = options.threshold;
}
});
}
static new(app: GraphicApp) {
return new DragPlugin(app);
}
bind(): void {
const canvas = this.app.canvas;
canvas.on('pointerdown', this.onPointerDown, this);
}
unbind(): void {
const canvas = this.app.canvas;
canvas.off('pointerdown', this.onPointerDown, this);
canvas.off('pointerup', this.onPointerUp, this);
canvas.off('pointerupoutside', this.onPointerUp, this);
}
onPointerDown(e: FederatedPointerEvent) {
this.target = e.target as DisplayObject;
this.start = this.app.toCanvasCoordinates(e.global);
const canvas = this.app.canvas;
canvas.on('pointermove', this.onPointerMove, this);
canvas.on('pointerup', this.onPointerUp, this);
canvas.on('pointerupoutside', this.onPointerUp, this);
}
onPointerMove(e: FederatedPointerEvent) {
if (this.start) {
const current = this.app.toCanvasCoordinates(e.global);
const dragStart =
Math.abs(current.x - this.start.x) > this.threshold ||
Math.abs(current.y - this.start.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) {
// console.log('drag move', e.movement);
this.app.emit(
'drag_op_move',
new AppDragEvent(this.app, 'move', this.target, e, this.start)
);
}
}
}
onPointerUp(e: FederatedPointerEvent) {
if (this.target && this.drag && this.start) {
// console.log('drag end');
this.app.emit(
'drag_op_end',
new AppDragEvent(this.app, 'end', this.target, e, this.start)
);
}
const canvas = this.app.canvas;
canvas.off('mousemove', this.onPointerMove, this);
canvas.off('mouseup', this.onPointerUp, this);
canvas.off('mouseupoutside', this.onPointerUp, this);
this.clearCache();
}
/**
*
*/
clearCache() {
this.drag = false;
this.start = null;
this.target = null;
}
}
/**
*
*/
export class ViewportMovePlugin extends BaseInteractionPlugin {
static Name = '__viewport_move_plugin';
moveHandler: NodeJS.Timeout | null = null;
moveSpeedx = 0;
moveSpeedy = 0;
_pause: boolean;
constructor(app: GraphicApp) {
this.app = app;
this._pause = true;
app.registerInteractionPlugin(this);
super(app, ViewportMovePlugin.Name, InteractionPluginType.Other);
}
static new(app: GraphicApp): ViewportMovePlugin {
return new ViewportMovePlugin(app);
}
resume(): void {
pause(): void {
super.pause();
this.stopMove();
}
bind(): void {
this.app.canvas.on('pointermove', this.viewportMove, this);
}
pause(): void {
unbind(): void {
this.app.canvas.off('pointermove', this.viewportMove, this);
}
isActive(): boolean {
return !this._pause;
}
destroy(): void {
this.pause();
this.app.removeInteractionPlugin(this);
}
startMove(moveSpeedx: number, moveSpeedy: number) {
this.moveSpeedx = moveSpeedx;
this.moveSpeedy = moveSpeedy;
@ -90,6 +294,7 @@ export class ViewportMovePlugin implements InteractionPlugin {
if (this.moveHandler != null) {
clearInterval(this.moveHandler);
this.moveHandler = null;
this.app.canvas.cursor = 'auto';
}
}
@ -127,24 +332,11 @@ export class ViewportMovePlugin implements InteractionPlugin {
/**
*
*/
export abstract class AppInteractionPlugin implements InteractionPlugin {
export abstract class AppInteractionPlugin extends BaseInteractionPlugin {
readonly _type = InteractionPluginType.App;
name: string; // 唯一标识
app: GraphicApp;
_pause: boolean;
constructor(name: string, app: GraphicApp) {
this.name = name;
this.app = app;
this._pause = true;
app.registerInteractionPlugin(this);
}
isActive(): boolean {
return !this._pause;
}
pause(): void {
this.unbind();
this._pause = true;
constructor(name: string, app: GraphicApp) {
super(app, name, InteractionPluginType.App);
}
/**
@ -155,14 +347,6 @@ export abstract class AppInteractionPlugin implements InteractionPlugin {
this.bind();
this._pause = false;
}
abstract bind(): void;
abstract unbind(): void;
destroy(): void {
this.pause();
this.app.removeInteractionPlugin(this);
}
}
/**
@ -198,17 +382,16 @@ export abstract class GraphicInteractionPlugin<G extends JlGraphic>
return !this._pause;
}
pause(): void {
const list = this.filter(...this.app.queryStore.getAllGraphics());
this.unbinds(list);
this._pause = true;
}
resume(): void {
const list = this.filter(...this.app.queryStore.getAllGraphics());
this.binds(list);
this._pause = false;
}
pause(): void {
const list = this.filter(...this.app.queryStore.getAllGraphics());
this.unbinds(list);
this._pause = true;
}
/**
*

View File

@ -219,7 +219,7 @@ export interface KeyListenerOptions {
// 按下操作处理
onPress?: KeyboardKeyHandler;
// 按下操作是否每次触发,默认一次
pressTriggerEveryTime?: boolean;
pressTriggerAsOriginalEvent?: boolean;
// 释放/抬起操作处理
onRelease?: KeyboardKeyHandler;
}
@ -233,7 +233,7 @@ export interface ICompleteKeyListenerOptions {
global: boolean;
// 按下操作处理
onPress?: KeyboardKeyHandler;
pressTriggerEveryTime: boolean;
pressTriggerAsOriginalEvent: boolean;
// 释放/抬起操作处理
onRelease?: KeyboardKeyHandler;
}
@ -243,7 +243,7 @@ const DefaultKeyListenerOptions: ICompleteKeyListenerOptions = {
combinations: [],
global: false,
onPress: undefined,
pressTriggerEveryTime: false,
pressTriggerAsOriginalEvent: false,
onRelease: undefined,
};
@ -293,11 +293,11 @@ export class KeyListener {
}
public get pressTriggerEveryTime(): boolean {
return this.options.pressTriggerEveryTime;
return this.options.pressTriggerAsOriginalEvent;
}
public set pressTriggerEveryTime(v: boolean) {
this.options.pressTriggerEveryTime = v;
this.options.pressTriggerAsOriginalEvent = v;
}
press(e: KeyboardEvent, app: GraphicApp): void {

View File

@ -2,3 +2,4 @@ export * from './InteractionPlugin';
export * from './CommonMousePlugin';
export * from './KeyboardPlugin';
export * from './CopyPlugin';
export * from './GraphicTransformPlugin';