菜单功能完善

直线/曲线编辑右键菜单功能完善
添加_rightclick和_leftclick事件(原始事件无法区别拖拽)
This commit is contained in:
walker 2023-05-12 18:17:13 +08:00
parent df3522e205
commit 3454739356
14 changed files with 470 additions and 149 deletions

View File

@ -14,7 +14,7 @@ viewport 使用的 github 开源的 pixi-viewport[pixi-viewport](https://github.
- 绘制增加吸附功能(移动到特定位置附近吸附)(完成) - 绘制增加吸附功能(移动到特定位置附近吸附)(完成)
- 图形动画抽象 - 图形动画抽象
- 添加公用动画逻辑(如按指定路径位移,按角度旋转、按比例缩放、透明度控制等) - 添加公用动画逻辑(如按指定路径位移,按角度旋转、按比例缩放、透明度控制等)
- 菜单事件及处理 - 菜单事件及处理,功能:菜单更新、菜单项显隐控制、菜单执行前后事件回调
- 打包 - 打包
- 添加拖拽轨迹限制功能 - 添加拖拽轨迹限制功能
- 添加图形对象 可编辑属性 定义功能 - 添加图形对象 可编辑属性 定义功能

View File

@ -1,5 +1,7 @@
import { fromUint8Array, toUint8Array } from 'js-base64'; import { fromUint8Array, toUint8Array } from 'js-base64';
import { IPointData, Point } from 'pixi.js'; import { IPointData, Point } from 'pixi.js';
import { IscsFan } from 'src/graphics/iscs-fan/IscsFan';
import { IscsFanDraw } from 'src/graphics/iscs-fan/IscsFanDrawAssistant';
import { Link } from 'src/graphics/link/Link'; import { Link } from 'src/graphics/link/Link';
import { LinkDraw } from 'src/graphics/link/LinkDrawAssistant'; import { LinkDraw } from 'src/graphics/link/LinkDrawAssistant';
import { import {
@ -10,11 +12,11 @@ import {
JlDrawApp, JlDrawApp,
KeyListener, KeyListener,
} from 'src/jlgraphic'; } from 'src/jlgraphic';
import { ContextMenu } from 'src/jlgraphic/ui/ContextMenu';
import { MenuItemOptions } from 'src/jlgraphic/ui/Menu';
import { IscsFanData } from './graphics/IscsFanInteraction';
import { LinkData } from './graphics/LinkInteraction'; import { LinkData } from './graphics/LinkInteraction';
import { graphicData } from './protos/draw_data_storage'; import { graphicData } from './protos/draw_data_storage';
import { IscsFanDraw } from 'src/graphics/iscs-fan/IscsFanDrawAssistant';
import { IscsFanData } from './graphics/IscsFanInteraction';
import { IscsFan } from 'src/graphics/iscs-fan/IscsFan';
export function fromStoragePoint(p: graphicData.Point): Point { export function fromStoragePoint(p: graphicData.Point): Point {
return new Point(p.x, p.y); return new Point(p.x, p.y);
@ -46,6 +48,28 @@ export function toStorageTransform(
}); });
} }
const UndoOptions: MenuItemOptions = {
name: '撤销',
};
const RedoOptions: MenuItemOptions = {
name: '重做',
};
const SelectAllOptions: MenuItemOptions = {
name: '全选',
};
export const DefaultCanvasMenu = new ContextMenu({
name: '绘制-画布菜单',
groups: [
{
items: [UndoOptions, RedoOptions],
},
{
items: [SelectAllOptions],
},
],
});
export function initDrawApp(app: JlDrawApp) { export function initDrawApp(app: JlDrawApp) {
app.setOptions({ app.setOptions({
drawAssistants: [ drawAssistants: [
@ -58,6 +82,23 @@ export function initDrawApp(app: JlDrawApp) {
], ],
}); });
// 画布右键菜单
app.registerMenu(DefaultCanvasMenu);
app.canvas.on('_rightclick', (e) => {
UndoOptions.disabled = !app.opRecord.hasUndo;
RedoOptions.disabled = !app.opRecord.hasRedo;
UndoOptions.handler = () => {
app.opRecord.undo();
};
RedoOptions.handler = () => {
app.opRecord.redo();
};
SelectAllOptions.handler = () => {
app.selectAllGraphics();
};
DefaultCanvasMenu.open(e.global);
});
app.addKeyboardListener( app.addKeyboardListener(
new KeyListener({ new KeyListener({
value: 'KeyL', value: 'KeyL',

View File

@ -29,7 +29,13 @@ import {
BezierCurveEditPlugin, BezierCurveEditPlugin,
ILineGraphic, ILineGraphic,
PolylineEditPlugin, PolylineEditPlugin,
addWayPoint,
addWaypointConfig,
clearWayPoint,
clearWaypointsConfig,
getWaypointRangeIndex,
} from 'src/jlgraphic/plugins/GraphicEditPlugin'; } from 'src/jlgraphic/plugins/GraphicEditPlugin';
import { ContextMenu } from 'src/jlgraphic/ui/ContextMenu';
import { ILinkData, Link, LinkTemplate } from './Link'; import { ILinkData, Link, LinkTemplate } from './Link';
export interface ILinkDrawOptions { export interface ILinkDrawOptions {
@ -263,6 +269,15 @@ function onEditPointCreate(
} }
} }
const LinkEditMenu: ContextMenu = ContextMenu.init({
name: '轨道编辑菜单',
groups: [
{
items: [addWaypointConfig, clearWaypointsConfig],
},
],
});
/** /**
* link路径编辑 * link路径编辑
*/ */
@ -270,6 +285,7 @@ export class LinkPointsEditPlugin extends GraphicInteractionPlugin<Link> {
static Name = 'LinkPointsDrag'; static Name = 'LinkPointsDrag';
constructor(app: GraphicApp) { constructor(app: GraphicApp) {
super(LinkPointsEditPlugin.Name, app); super(LinkPointsEditPlugin.Name, app);
app.registerMenu(LinkEditMenu);
} }
static init(app: GraphicApp): LinkPointsEditPlugin { static init(app: GraphicApp): LinkPointsEditPlugin {
return new LinkPointsEditPlugin(app); return new LinkPointsEditPlugin(app);
@ -281,14 +297,37 @@ export class LinkPointsEditPlugin extends GraphicInteractionPlugin<Link> {
g.lineGraphic.eventMode = 'static'; g.lineGraphic.eventMode = 'static';
g.lineGraphic.cursor = 'pointer'; g.lineGraphic.cursor = 'pointer';
g.lineGraphic.hitArea = new LinkGraphicHitArea(g); g.lineGraphic.hitArea = new LinkGraphicHitArea(g);
g.on('_rightclick', this.onContextMenu, this);
g.on('selected', this.onSelected, this); g.on('selected', this.onSelected, this);
g.on('unselected', this.onUnselected, this); g.on('unselected', this.onUnselected, this);
} }
unbind(g: Link): void { unbind(g: Link): void {
g.off('_rightclick', this.onContextMenu, this);
g.off('selected', this.onSelected, this); g.off('selected', this.onSelected, this);
g.off('unselected', this.onUnselected, this); g.off('unselected', this.onUnselected, this);
} }
onContextMenu(e: FederatedMouseEvent) {
const target = e.target as DisplayObject;
const link = target.getGraphic() as Link;
this.app.updateSelected(link);
addWaypointConfig.handler = () => {
const linePoints = link.linePoints;
const p = link.screenToLocalPoint(e.global);
const { start, end } = getWaypointRangeIndex(
linePoints,
link.datas.curve,
p
);
addWayPoint(link, link.datas.curve, start, end, p);
};
clearWaypointsConfig.handler = () => {
clearWayPoint(link, link.datas.curve);
};
LinkEditMenu.open(e.global);
}
onSelected(g: DisplayObject): void { onSelected(g: DisplayObject): void {
const link = g as Link; const link = g as Link;
let lep; let lep;

View File

@ -35,6 +35,8 @@ import {
ICanvasProperties, ICanvasProperties,
JlCanvas, JlCanvas,
} from './JlGraphicApp'; } from './JlGraphicApp';
import { ContextMenu } from '../ui/ContextMenu';
import { MenuItemOptions, MenuOptions } from '../ui/Menu';
/** /**
* *
@ -240,7 +242,7 @@ export class JlDrawApp extends GraphicApp {
this.selectedData = null; this.selectedData = null;
this.initSelectedDataUpdateListen(); this.initSelectedDataUpdateListen();
// 拖拽操作记录 // 拖拽操作记录
this.appDragRecord(); this.appOperationRecord();
// 绑定通用键盘操作 // 绑定通用键盘操作
this.bindKeyboardOperation(); this.bindKeyboardOperation();
} }
@ -269,7 +271,7 @@ export class JlDrawApp extends GraphicApp {
return sda as DA; return sda as DA;
} }
private appDragRecord() { private appOperationRecord() {
let dragStartDatas: GraphicData[] = []; let dragStartDatas: GraphicData[] = [];
this.on('drag_op_start', (e: AppDragEvent) => { this.on('drag_op_start', (e: AppDragEvent) => {
// 图形拖拽,记录初始数据 // 图形拖拽,记录初始数据
@ -283,7 +285,7 @@ export class JlDrawApp extends GraphicApp {
if (dragStartDatas.length > 0) { if (dragStartDatas.length > 0) {
const newData = this.selectedGraphics.map((g) => g.saveData()); const newData = this.selectedGraphics.map((g) => g.saveData());
this.opRecord.record( this.opRecord.record(
new DragOperation( new GraphicDataUpdateOperation(
this, this,
this.selectedGraphics, this.selectedGraphics,
dragStartDatas, dragStartDatas,
@ -293,6 +295,28 @@ export class JlDrawApp extends GraphicApp {
dragStartDatas = []; dragStartDatas = [];
} }
}); });
// 菜单操作
let preMenuHandleDatas: GraphicData[] = [];
this.on('pre-menu-handle', (menu: MenuItemOptions) => {
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 = [];
}
});
} }
/** /**
@ -346,7 +370,6 @@ export class JlDrawApp extends GraphicApp {
this.app.stage.addChild(this.scaleText); this.app.stage.addChild(this.scaleText);
const bound = this.coordinates.getLocalBounds(); const bound = this.coordinates.getLocalBounds();
this.scaleText.position.set(bound.width + 10, 0); this.scaleText.position.set(bound.width + 10, 0);
this.canvas.interactive = true;
this.canvas.on('mousemove', (e) => { this.canvas.on('mousemove', (e) => {
if (e.target) { if (e.target) {
const { x, y } = this.toCanvasCoordinates(e.global); const { x, y } = this.toCanvasCoordinates(e.global);
@ -517,11 +540,11 @@ export class JlDrawApp extends GraphicApp {
const data = g.saveData(); const data = g.saveData();
g.updateData(this.selectedData as GraphicData); g.updateData(this.selectedData as GraphicData);
this.opRecord.record( this.opRecord.record(
new UpdateDataOperation( new GraphicDataUpdateOperation(
this, this,
g, [g],
data, [data],
this.selectedData as GraphicData [this.selectedData as GraphicData]
) )
); );
} }
@ -604,7 +627,12 @@ function recordGraphicMoveOperation(app: GraphicApp) {
) { ) {
const newData = app.selectedGraphics.map((g) => g.saveData()); const newData = app.selectedGraphics.map((g) => g.saveData());
app.opRecord.record( app.opRecord.record(
new DragOperation(app, app.selectedGraphics, dragStartDatas, newData) new GraphicDataUpdateOperation(
app,
app.selectedGraphics,
dragStartDatas,
newData
)
); );
handleArrowKeyMoveGraphics(app, (g) => { handleArrowKeyMoveGraphics(app, (g) => {
@ -695,39 +723,7 @@ export class GraphicDeleteOperation extends JlOperation {
} }
} }
/** export class GraphicDataUpdateOperation extends JlOperation {
*
*/
export class UpdateDataOperation extends JlOperation {
obj: JlGraphic;
data: GraphicData; // 更新为data的protobuf数据
old: GraphicData; // 更新前data的protobuf数据
description = '';
constructor(
app: GraphicApp,
obj: JlGraphic,
old: GraphicData,
data: GraphicData
) {
super(app, 'update-payload');
this.app = app;
this.obj = obj;
this.old = old;
this.data = data;
}
undo(): JlGraphic[] {
this.obj.updateData(this.old);
return [this.obj];
}
redo(): JlGraphic[] {
this.obj.updateData(this.data);
return [this.obj];
}
}
export class DragOperation extends JlOperation {
obj: JlGraphic[]; obj: JlGraphic[];
oldData: GraphicData[]; oldData: GraphicData[];
newData: GraphicData[]; newData: GraphicData[];
@ -746,7 +742,7 @@ export class DragOperation extends JlOperation {
undo(): void | JlGraphic[] { undo(): void | JlGraphic[] {
for (let i = 0; i < this.obj.length; i++) { for (let i = 0; i < this.obj.length; i++) {
const g = this.obj[i]; const g = this.obj[i];
g.exitChildEdit(); // g.exitChildEdit();
g.updateData(this.oldData[i]); g.updateData(this.oldData[i]);
} }
return this.obj; return this.obj;
@ -754,7 +750,7 @@ export class DragOperation extends JlOperation {
redo(): void | JlGraphic[] { redo(): void | JlGraphic[] {
for (let i = 0; i < this.obj.length; i++) { for (let i = 0; i < this.obj.length; i++) {
const g = this.obj[i]; const g = this.obj[i];
g.exitChildEdit(); // g.exitChildEdit();
g.updateData(this.newData[i]); g.updateData(this.newData[i]);
} }
return this.obj; return this.obj;

View File

@ -45,6 +45,7 @@ import {
} from '../plugins/KeyboardPlugin'; } from '../plugins/KeyboardPlugin';
import { ContextMenu, ContextMenuPlugin } from '../ui/ContextMenu'; import { ContextMenu, ContextMenuPlugin } from '../ui/ContextMenu';
import { getRectangleCenter, recursiveChildren } from '../utils/GraphicUtils'; import { getRectangleCenter, recursiveChildren } from '../utils/GraphicUtils';
import { MenuItemOptions } from '../ui/Menu';
export const AppConsts = { export const AppConsts = {
viewportname: '__viewport', viewportname: '__viewport',
@ -284,6 +285,8 @@ export interface GraphicAppEvents extends GlobalMixins.GraphicAppEvents {
drag_op_start: [event: AppDragEvent]; drag_op_start: [event: AppDragEvent];
drag_op_move: [event: AppDragEvent]; drag_op_move: [event: AppDragEvent];
drag_op_end: [event: AppDragEvent]; drag_op_end: [event: AppDragEvent];
'pre-menu-handle': [menu: MenuItemOptions];
'post-menu-handle': [menu: MenuItemOptions];
destroy: [app: GraphicApp]; destroy: [app: GraphicApp];
} }

View File

@ -305,6 +305,16 @@ DisplayObject.prototype.localToScreenPoints = function localToScreenPoints(
): Point[] { ): Point[] {
return points.map((p) => this.toGlobal(p)); return points.map((p) => this.toGlobal(p));
}; };
DisplayObject.prototype.screenToLocalPoint = function screenToLocalPoint(
p: IPointData
): Point {
return this.toLocal(p);
}
DisplayObject.prototype.screenToLocalPoints = function screenToLocalPoints(
...points: IPointData[]
): Point[] {
return points.map((p) => this.toLocal(p));
}
DisplayObject.prototype.localBoundsToCanvasPoints = DisplayObject.prototype.localBoundsToCanvasPoints =
function localBoundsToCanvasPoints() { function localBoundsToCanvasPoints() {

View File

@ -11,6 +11,7 @@ declare namespace GlobalMixins {
type BoundsGraphic = import('./plugins').BoundsGraphic; type BoundsGraphic = import('./plugins').BoundsGraphic;
type IPointDataType = import('pixi.js').IPointData; type IPointDataType = import('pixi.js').IPointData;
type PointType = import('pixi.js').Point; type PointType = import('pixi.js').Point;
type FederatedMouseEvent = import('pixi.js').FederatedMouseEvent;
type DisplayObjectType = import('pixi.js').DisplayObject; type DisplayObjectType = import('pixi.js').DisplayObject;
type ContainerType = import('pixi.js').Container; type ContainerType = import('pixi.js').Container;
interface DisplayObjectEvents { interface DisplayObjectEvents {
@ -22,6 +23,8 @@ declare namespace GlobalMixins {
transformstart: [e: GraphicTransformEvent]; transformstart: [e: GraphicTransformEvent];
transforming: [e: GraphicTransformEvent]; transforming: [e: GraphicTransformEvent];
transformend: [e: GraphicTransformEvent]; transformend: [e: GraphicTransformEvent];
_rightclick: [e: FederatedMouseEvent];
_leftclick: [e: FederatedMouseEvent];
selected: [DisplayObjectType]; selected: [DisplayObjectType];
unselected: [DisplayObjectType]; unselected: [DisplayObjectType];
childselected: [DisplayObjectType]; childselected: [DisplayObjectType];

View File

@ -46,6 +46,14 @@ export class OperationRecord {
this.maxLen = maxLen; this.maxLen = maxLen;
} }
public get hasUndo(): boolean {
return this.undoStack.length > 0;
}
public get hasRedo(): boolean {
return this.redoStack.length > 0;
}
setMaxLen(v: number) { setMaxLen(v: number) {
this.maxLen = v; this.maxLen = v;
const len = this.undoStack.length; const len = this.undoStack.length;

View File

@ -6,12 +6,20 @@ import {
Graphics, Graphics,
IDestroyOptions, IDestroyOptions,
IPointData, IPointData,
Point,
} from 'pixi.js'; } from 'pixi.js';
import { JlGraphic } from '../core'; import { JlGraphic } from '../core';
import { DraggablePoint } from '../graphic'; import { DraggablePoint } from '../graphic';
import { calculateMirrorPoint, distance2 } from '../utils';
import { MenuItemOptions, MenuOptions } from '../ui/Menu';
import { ContextMenu } from '../ui/ContextMenu'; import { ContextMenu } from '../ui/ContextMenu';
import { MenuItemOptions, MenuOptions } from '../ui/Menu';
import {
calculateFootPointFromPointToLine,
calculateMirrorPoint,
assertBezierPoints,
distance2,
linePoint,
pointPolygon,
} from '../utils';
import { GraphicTransformEvent, ShiftData } from './GraphicTransformPlugin'; import { GraphicTransformEvent, ShiftData } from './GraphicTransformPlugin';
export abstract class GraphicEditPlugin< export abstract class GraphicEditPlugin<
@ -22,6 +30,7 @@ export abstract class GraphicEditPlugin<
super(); super();
this.graphic = g; this.graphic = g;
this.zIndex = 2; this.zIndex = 2;
this.sortableChildren = true;
this.graphic.on('transformstart', this.hideAll, this); this.graphic.on('transformstart', this.hideAll, this);
this.graphic.on('transformend', this.showAll, this); this.graphic.on('transformend', this.showAll, this);
this.graphic.on('repaint', this.showAll, this); this.graphic.on('repaint', this.showAll, this);
@ -45,20 +54,20 @@ export abstract class GraphicEditPlugin<
} }
} }
const addWaypoint: MenuItemOptions = { export const addWaypointConfig: MenuItemOptions = {
name: '添加路径点', name: '添加路径点',
}; };
const removeWaypoint: MenuItemOptions = { export const removeWaypointConfig: MenuItemOptions = {
name: '移除路径点', name: '移除路径点',
}; };
const clearWaypoints: MenuItemOptions = { export const clearWaypointsConfig: MenuItemOptions = {
name: '清除所有路径点', name: '清除所有路径点',
}; };
const menuOptions: MenuOptions = { const menuOptions: MenuOptions = {
name: '图形编辑点菜单', name: '图形编辑点菜单',
groups: [ groups: [
{ {
items: [addWaypoint, removeWaypoint, clearWaypoints], items: [removeWaypointConfig, clearWaypointsConfig],
}, },
], ],
}; };
@ -94,6 +103,42 @@ export abstract class LineEditPlugin extends GraphicEditPlugin<ILineGraphic> {
abstract initEditPoints(): void; abstract initEditPoints(): void;
} }
export function getWaypointRangeIndex(
points: IPointData[],
curve: boolean,
p: IPointData
): { start: number; end: number } {
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 {
// 贝塞尔曲线
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], 1)) {
start = i;
end = i + 3;
}
}
}
return { start, end };
}
export type onEditPointCreate = ( export type onEditPointCreate = (
g: ILineGraphic, g: ILineGraphic,
dp: DraggablePoint, dp: DraggablePoint,
@ -125,6 +170,19 @@ export class PolylineEditPlugin extends LineEditPlugin {
for (let i = 0; i < cps.length; i++) { for (let i = 0; i < cps.length; i++) {
const p = cps[i]; const p = cps[i];
const dp = new DraggablePoint(p); const dp = new DraggablePoint(p);
dp.on('rightclick', (e: FederatedMouseEvent) => {
// dp.getGraphicApp().openContextMenu(EditPointContextMenu.DefaultMenu);
// 路径中的点
const app = dp.getGraphicApp();
app.registerMenu(EditPointContextMenu);
removeWaypointConfig.handler = () => {
removeLineWayPoint(this.graphic, i);
};
clearWaypointsConfig.handler = () => {
clearWayPoint(this.graphic, false);
};
EditPointContextMenu.open(e.global);
});
dp.on('transforming', () => { dp.on('transforming', () => {
const tlp = this.graphic.canvasToLocalPoint(dp.position); const tlp = this.graphic.canvasToLocalPoint(dp.position);
const cp = this.linePoints[i]; const cp = this.linePoints[i];
@ -166,6 +224,133 @@ export interface ICompleteBezierCurveEditPointOptions
smooth: boolean; smooth: boolean;
} }
export function addWayPoint(
graphic: ILineGraphic,
curve: boolean,
start: number,
end: number,
p: IPointData
) {
if (!curve) {
addLineWayPoint(graphic, start, end, p);
} else {
addBezierWayPoint(graphic, start, end, p);
}
}
export function addLineWayPoint(
graphic: ILineGraphic,
start: number,
end: number,
p: IPointData
) {
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 assertBezierWayPoint(i: number) {
const c = i % 3;
if (c !== 0) {
throw new Error(`i=${i}的点不是路径点`);
}
}
export function addBezierWayPoint(
graphic: ILineGraphic,
start: number,
end: number,
p: IPointData
) {
if (start === end) {
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;
}
export function removeWayPoint(
graphic: ILineGraphic,
curve: boolean,
i: number
) {
if (!curve) {
removeLineWayPoint(graphic, i);
} else {
removeBezierWayPoint(graphic, i);
}
}
export function removeLineWayPoint(graphic: ILineGraphic, i: number) {
const linePoints = graphic.linePoints;
if (linePoints.length > 2) {
const points = linePoints.slice(0, i);
points.push(...linePoints.slice(i + 1));
graphic.linePoints = points;
}
}
export function removeBezierWayPoint(graphic: ILineGraphic, i: number) {
let points;
const linePoints = graphic.linePoints;
const c = i % 3;
if (c !== 0) {
throw new Error(`i=${i}的点${linePoints[i]}不是路径点`);
}
if (i === 0) {
// 第一个点
if (linePoints.length > 4) {
points = linePoints.slice(3);
} else {
console.error('不能移除:剩余点数不足');
}
} else if (i === linePoints.length - 1) {
// 最后一个点
if (linePoints.length > 4) {
points = linePoints.slice(0, linePoints.length - 3);
} else {
console.error('无法移除:剩余点数不足');
}
} else {
// 中间点
points = linePoints.slice(0, i - 1);
points.push(...linePoints.slice(i + 2));
}
if (points) {
graphic.linePoints = points;
}
}
/**
* (),线线
* @param graphic
* @param curve
*/
export function clearWayPoint(graphic: ILineGraphic, curve: boolean) {
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;
}
}
}
/** /**
* 线 * 线
*/ */
@ -195,11 +380,15 @@ export class BezierCurveEditPlugin extends LineEditPlugin {
const startOrEnd = i == 0 || i == cps.length - 1; const startOrEnd = i == 0 || i == cps.length - 1;
const c = i % 3; const c = i % 3;
if (c === 1) { if (c === 1) {
// 前一路径点的控制点
dp.zIndex = 2;
const fp = cps[i - 1]; const fp = cps[i - 1];
const line = new Graphics(); const line = new Graphics();
this.drawAuxiliaryLine(line, fp, p); this.drawAuxiliaryLine(line, fp, p);
this.auxiliaryLines.push(line); this.auxiliaryLines.push(line);
} else if (c === 2) { } else if (c === 2) {
// 后一路径点的控制点
dp.zIndex = 3;
const np = cps[i + 1]; const np = cps[i + 1];
const line = new Graphics(); const line = new Graphics();
this.drawAuxiliaryLine(line, p, np); this.drawAuxiliaryLine(line, p, np);
@ -208,34 +397,14 @@ export class BezierCurveEditPlugin extends LineEditPlugin {
dp.on('rightclick', (e: FederatedMouseEvent) => { dp.on('rightclick', (e: FederatedMouseEvent) => {
// dp.getGraphicApp().openContextMenu(EditPointContextMenu.DefaultMenu); // dp.getGraphicApp().openContextMenu(EditPointContextMenu.DefaultMenu);
if (c === 0) { if (c === 0) {
// 路径中的点
const app = dp.getGraphicApp(); const app = dp.getGraphicApp();
app.registerMenu(EditPointContextMenu); app.registerMenu(EditPointContextMenu);
removeWaypoint.onClick = () => { removeWaypointConfig.handler = () => {
let points; removeBezierWayPoint(this.graphic, i);
if (i === 0) { };
// 第一个点 clearWaypointsConfig.handler = () => {
if (this.linePoints.length > 4) { clearWayPoint(this.graphic, true);
points = this.linePoints.slice(3);
} else {
console.error('无法移除');
}
} else if (i === cps.length - 1) {
// 最后一个点
if (this.linePoints.length > 4) {
points = this.linePoints.slice(0, this.linePoints.length - 3);
} else {
console.error('无法移除');
}
} else {
// 中间点
points = [];
points.push(...this.linePoints.slice(0, i - 1));
points.push(...this.linePoints.slice(i + 2));
}
if (points) {
console.log('points', points);
this.graphic.linePoints = points;
}
}; };
EditPointContextMenu.open(e.global); EditPointContextMenu.open(e.global);
} }

View File

@ -225,7 +225,7 @@ export class GraphicTransformPlugin extends BaseInteractionPlugin {
return targets; return targets;
} }
onDragStart(e: AppDragEvent) { onDragStart(e: AppDragEvent) {
if (!e.target.isCanvas()) { if (!e.target.isCanvas() && e.isLeftButton) {
const targets: DisplayObject[] = this.getDraggedTargets(e); const targets: DisplayObject[] = this.getDraggedTargets(e);
if (targets.length > 0) { if (targets.length > 0) {
targets.forEach((target) => { targets.forEach((target) => {
@ -248,7 +248,7 @@ export class GraphicTransformPlugin extends BaseInteractionPlugin {
} }
onDragMove(e: AppDragEvent) { onDragMove(e: AppDragEvent) {
if (!e.target.isCanvas()) { if (!e.target.isCanvas() && e.isLeftButton) {
const targets: DisplayObject[] = this.getDraggedTargets(e); const targets: DisplayObject[] = this.getDraggedTargets(e);
if (targets.length > 0) { if (targets.length > 0) {
// 处理位移 // 处理位移
@ -291,7 +291,7 @@ export class GraphicTransformPlugin extends BaseInteractionPlugin {
} }
onDragEnd(e: AppDragEvent) { onDragEnd(e: AppDragEvent) {
if (!e.target.isCanvas()) { if (!e.target.isCanvas() && e.isLeftButton) {
const targets: DisplayObject[] = this.getDraggedTargets(e); const targets: DisplayObject[] = this.getDraggedTargets(e);
targets.forEach((target) => { targets.forEach((target) => {
if (target.shiftStartPoint) { if (target.shiftStartPoint) {

View File

@ -165,9 +165,10 @@ export class AppDragEvent {
*/ */
export class DragPlugin extends BaseInteractionPlugin { export class DragPlugin extends BaseInteractionPlugin {
static Name = '__drag_operation_plugin'; static Name = '__drag_operation_plugin';
threshold = 3; private threshold = 3;
target: DisplayObject | null = null; target: DisplayObject | null = null;
start: Point | null = null; start: Point | null = null;
startClientPoint: Point | null = null;
drag = false; drag = false;
constructor(app: GraphicApp) { constructor(app: GraphicApp) {
super(app, DragPlugin.Name, InteractionPluginType.Other); super(app, DragPlugin.Name, InteractionPluginType.Other);
@ -193,17 +194,19 @@ export class DragPlugin extends BaseInteractionPlugin {
onPointerDown(e: FederatedPointerEvent) { onPointerDown(e: FederatedPointerEvent) {
this.target = e.target as DisplayObject; this.target = e.target as DisplayObject;
this.start = this.app.toCanvasCoordinates(e.global); this.start = this.app.toCanvasCoordinates(e.global);
this.startClientPoint = e.global.clone();
const canvas = this.app.canvas; const canvas = this.app.canvas;
canvas.on('pointermove', this.onPointerMove, this); canvas.on('pointermove', this.onPointerMove, this);
canvas.on('pointerup', this.onPointerUp, this); canvas.on('pointerup', this.onPointerUp, this);
canvas.on('pointerupoutside', this.onPointerUp, this); canvas.on('pointerupoutside', this.onPointerUp, this);
} }
onPointerMove(e: FederatedPointerEvent) { onPointerMove(e: FederatedPointerEvent) {
if (this.start) { if (this.start && this.startClientPoint) {
const current = this.app.toCanvasCoordinates(e.global); const current = e.global;
const sgp = this.startClientPoint;
const dragStart = const dragStart =
Math.abs(current.x - this.start.x) > this.threshold || Math.abs(current.x - sgp.x) > this.threshold ||
Math.abs(current.y - this.start.y) > this.threshold; Math.abs(current.y - sgp.y) > this.threshold;
if (this.target && this.start && !this.drag && dragStart) { if (this.target && this.start && !this.drag && dragStart) {
this.app.emit( this.app.emit(
'drag_op_start', 'drag_op_start',
@ -230,6 +233,21 @@ export class DragPlugin extends BaseInteractionPlugin {
'drag_op_end', 'drag_op_end',
new AppDragEvent(this.app, 'end', this.target, e, this.start) new AppDragEvent(this.app, 'end', this.target, e, this.start)
); );
} else if (this.target && this.start && !this.drag) {
// this.target.emit('click', this.target);
const ade = new AppDragEvent(this.app, 'end', this.target, e, this.start);
const graphic = this.target.getGraphic();
if (ade.isRightButton) {
this.target.emit('_rightclick', e);
if (graphic != null) {
graphic.emit('_rightclick', e);
}
} else if (ade.isLeftButton) {
this.target.emit('_leftclick', e);
if (graphic != null) {
graphic.emit('_leftclick', e);
}
}
} }
const canvas = this.app.canvas; const canvas = this.app.canvas;
canvas.off('mousemove', this.onPointerMove, this); canvas.off('mousemove', this.onPointerMove, this);
@ -243,6 +261,7 @@ export class DragPlugin extends BaseInteractionPlugin {
clearCache() { clearCache() {
this.drag = false; this.drag = false;
this.start = null; this.start = null;
this.startClientPoint = null;
this.target = null; this.target = null;
} }
} }

View File

@ -17,6 +17,12 @@ export class ContextMenuPlugin {
constructor(app: GraphicApp) { constructor(app: GraphicApp) {
this.app = app; this.app = app;
const canvas = this.app.canvas;
canvas.on('pointerdown', () => {
this.contextMenuMap.forEach((menu) => {
menu.close();
});
});
} }
registerMenu(menu: ContextMenu) { registerMenu(menu: ContextMenu) {
@ -262,7 +268,7 @@ export class ContextMenu extends Container {
this.addChild(...this.groups); this.addChild(...this.groups);
this.interactive = true; this.eventMode = 'static';
this.on('pointerover', () => { this.on('pointerover', () => {
this.active = true; this.active = true;
}); });
@ -288,6 +294,7 @@ export class ContextMenu extends Container {
} }
updateBg() { updateBg() {
this.bg.clear();
const options = this.menuOptions; const options = this.menuOptions;
let maxItemWidth = 0; let maxItemWidth = 0;
let borderHeight = 0; let borderHeight = 0;
@ -422,10 +429,7 @@ class MenuGroup extends Container {
config.items.forEach((item) => { config.items.forEach((item) => {
this.items.push(new ContextMenuItem(menu, item)); this.items.push(new ContextMenuItem(menu, item));
}); });
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i];
item.position.y = i * item.totalHeight;
}
this.addChild(...this.items); this.addChild(...this.items);
} }
@ -464,7 +468,21 @@ class MenuGroup extends Container {
} }
update() { update() {
this.items.forEach((item) => item.update()); let i = 0;
this.items.forEach((item) => {
item.update();
if (item.visible) {
item.position.y = i * item.totalHeight;
i++;
}
});
// this.items.forEach()
// for (let i = 0; i < this.items.length; i++) {
// const item = this.items[i];
// if (item.visible) {
// item.position.y = i * item.totalHeight;
// }
// }
} }
updateItemBox(maxItemWidth: number) { updateItemBox(maxItemWidth: number) {
@ -511,6 +529,42 @@ class ContextMenuItem extends Container {
this.initSubMenu(); this.initSubMenu();
} }
registerEventHandler() {
this.eventMode = 'static';
this.cursor = 'pointer';
this.on('pointerover', () => {
this.active = true;
if (this.config.disabled) {
this.cursor = 'not-allowed';
} else {
this.cursor = 'pointer';
}
if (this.subMenu) {
const p = this.toGlobal(new Point(this.width));
this.subMenu.open(p);
}
});
this.on('pointerout', () => {
this.active = false;
});
this.on('pointertap', () => {
if (this.config.disabled) {
// 禁用,不处理
return;
}
if (this.config.handler) {
this.menu.plugin.app.emit('pre-menu-handle', this.config);
this.config.handler();
this.menu.plugin.app.emit('post-menu-handle', this.config);
}
if (!this.config.subMenu || this.config.subMenu.length === 0) {
this.active = false;
this.menu.active = false;
this.menu.rootMenu.close();
}
});
}
public get active(): boolean { public get active(): boolean {
return this._active || (this.subMenu != undefined && this.subMenu.active); return this._active || (this.subMenu != undefined && this.subMenu.active);
} }
@ -543,7 +597,11 @@ class ContextMenuItem extends Container {
} }
public get totalHeight(): number { public get totalHeight(): number {
return this.paddingTop + this.paddingBottom + this.nameGraphic.height; if (this.config.visible === false) {
return 0;
} else {
return this.paddingTop + this.paddingBottom + this.nameGraphic.height;
}
} }
public get nameBounds(): Rectangle { public get nameBounds(): Rectangle {
@ -683,41 +741,16 @@ class ContextMenuItem extends Container {
} }
// 事件监听 // 事件监听
this.interactive = true;
this.hitArea = new Rectangle(0, 0, width, height); this.hitArea = new Rectangle(0, 0, width, height);
this.cursor = 'pointer'; this.registerEventHandler();
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.onClick) {
this.config.onClick();
}
if (!this.config.subMenu || this.config.subMenu.length === 0) {
this.active = false;
this.menu.active = false;
this.menu.rootMenu.close();
}
});
} }
update() { update() {
if (this.config.visible === false) {
this.visible = false;
return;
}
this.visible = true;
this.nameText.text = this.config.name; this.nameText.text = this.config.name;
this.nameText.style.fontSize = this.fontSize; this.nameText.style.fontSize = this.fontSize;
this.nameText.style.fill = this.fontColor; this.nameText.style.fill = this.fontColor;

View File

@ -23,6 +23,10 @@ export interface MenuOptions {
* *
*/ */
export interface MenuGroupOptions { export interface MenuGroupOptions {
/**
*
*/
name?: string;
/** /**
* *
*/ */
@ -53,7 +57,7 @@ export interface MenuStyleOptions {
*/ */
borderColor?: string; borderColor?: string;
/** /**
* 0 * ,0
*/ */
borderRoundRadius?: number; borderRoundRadius?: number;
/** /**
@ -90,9 +94,9 @@ export interface MenuItemOptions {
*/ */
shortcutKeys?: string[]; shortcutKeys?: string[];
/** /**
* *
*/ */
onClick?: () => void; handler?: () => void;
fontColor?: string; fontColor?: string;
/** /**
* *
@ -151,7 +155,7 @@ export const DefaultWhiteStyleOptions: MenuCompletionStyleOptions = {
borderWidth: 1, borderWidth: 1,
borderColor: '#4C4C4C', borderColor: '#4C4C4C',
/** /**
* *
*/ */
borderRoundRadius: 5, borderRoundRadius: 5,
itemStyle: { itemStyle: {

View File

@ -2,12 +2,11 @@ import {
Container, Container,
DisplayObject, DisplayObject,
IPointData, IPointData,
Matrix,
Point, Point,
Rectangle, Rectangle,
} from 'pixi.js'; } from 'pixi.js';
import Vector2 from '../math/Vector2';
import { floatEquals, isZero } from '../math'; import { floatEquals, isZero } from '../math';
import Vector2 from '../math/Vector2';
/** /**
* *
@ -86,10 +85,23 @@ export interface BezierParam {
cp2: IPointData; cp2: IPointData;
} }
export function convertToBezierParams(points: IPointData[]): BezierParam[] { /**
* 线
* @param points
*/
export function assertBezierPoints(points: IPointData[]) {
if (points.length < 4 || points.length % 3 !== 1) { if (points.length < 4 || points.length % 3 !== 1) {
throw new Error(`bezierCurve 数据错误: ${points}`); throw new Error(`bezierCurve 数据错误: ${points}`);
} }
}
/**
* 线
* @param points
* @returns
*/
export function convertToBezierParams(points: IPointData[]): BezierParam[] {
assertBezierPoints(points);
const bps: BezierParam[] = []; const bps: BezierParam[] = [];
for (let i = 0; i < points.length - 3; i += 3) { for (let i = 0; i < points.length - 3; i += 3) {
const p1 = new Point(points[i].x, points[i].y); const p1 = new Point(points[i].x, points[i].y);
@ -240,22 +252,6 @@ export function deserializeTransformInto(
} }
} }
/**
*
* @param point
* @param transform
* @returns
*/
export function calculateTransformedPoint(
point: IPointData,
transform: Matrix
): Point {
const { a, b, c, d, tx, ty } = transform;
const x = a * point.x + c * point.y + tx;
const y = b * point.x + d * point.y + ty;
return new Point(x, y);
}
/** /**
* 线 * 线
* @param p1 * @param p1