diff --git a/README.md b/README.md index edd6a13..ad75154 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ viewport 使用的 github 开源的 pixi-viewport[pixi-viewport](https://github. - 图形复制功能(完成) - 绘制应用图形外包围框及旋转缩放功能(完成) - 绘制增加吸附功能(移动到特定位置附近吸附)(完成) +- 图形动画抽象 +- 添加公用动画逻辑(如按指定路径位移,按角度旋转、按比例缩放、透明度控制等) - 菜单事件及处理 - 打包 - 添加拖拽轨迹限制功能 diff --git a/src/examples/app/graphics/BaseGraphicData.ts b/src/examples/app/graphics/BaseGraphicData.ts index 628d6ec..c329bf2 100644 --- a/src/examples/app/graphics/BaseGraphicData.ts +++ b/src/examples/app/graphics/BaseGraphicData.ts @@ -27,6 +27,20 @@ export abstract class BaseGraphicData implements GraphicData { this._data = data; } + static defaultCommonInfo(): graphicData.CommonInfo { + return new graphicData.CommonInfo({ + id: '', + graphicType: '', + transform: new graphicData.Transform({ + position: new graphicData.Point({ x: 0, y: 0 }), + scale: new graphicData.Point({ x: 1, y: 1 }), + rotation: 0, + skew: new graphicData.Point({ x: 0, y: 0 }), + }), + childTransforms: [], + }); + } + getData(): D { return this._data as D; } diff --git a/src/examples/app/graphics/IscsFanInteraction.ts b/src/examples/app/graphics/IscsFanInteraction.ts index 347ffe6..6c851af 100644 --- a/src/examples/app/graphics/IscsFanInteraction.ts +++ b/src/examples/app/graphics/IscsFanInteraction.ts @@ -10,7 +10,7 @@ export class IscsFanData extends BaseGraphicData implements IIscsFanData { fan = data; } else { fan = new graphicData.IscsFan({ - common: new graphicData.CommonInfo(), + common: BaseGraphicData.defaultCommonInfo(), }); } super(fan); diff --git a/src/examples/app/graphics/LinkInteraction.ts b/src/examples/app/graphics/LinkInteraction.ts index 73fbb0a..adec883 100644 --- a/src/examples/app/graphics/LinkInteraction.ts +++ b/src/examples/app/graphics/LinkInteraction.ts @@ -9,7 +9,7 @@ export class LinkData extends BaseGraphicData implements ILinkData { let link; if (!data) { link = new graphicData.Link({ - common: new graphicData.CommonInfo(), + common: BaseGraphicData.defaultCommonInfo(), }); } else { link = data; diff --git a/src/graphics/iscs-fan/IscsFanDrawAssistant.ts b/src/graphics/iscs-fan/IscsFanDrawAssistant.ts index 8f4247e..712243a 100644 --- a/src/graphics/iscs-fan/IscsFanDrawAssistant.ts +++ b/src/graphics/iscs-fan/IscsFanDrawAssistant.ts @@ -1,9 +1,5 @@ import { FederatedMouseEvent, Point } from 'pixi.js'; -import { - GraphicDrawAssistant, - GraphicTransform, - JlDrawApp, -} from 'src/jlgraphic'; +import { GraphicDrawAssistant, JlDrawApp } from 'src/jlgraphic'; import { IIscsFanData, IscsFan, IscsFanTemplate } from './IscsFan'; export class IscsFanDraw extends GraphicDrawAssistant< @@ -43,10 +39,7 @@ export class IscsFanDraw extends GraphicDrawAssistant< this.finish(); } prepareData(data: IIscsFanData): boolean { - // 变换设置必须新建变换赋值,不能直接修改变换数据 - const transfrom = GraphicTransform.default(); - transfrom.position = this.iscsFan.position.clone(); - data.transform = transfrom; + data.transform = this.iscsFan.saveTransform(); return true; } onEsc(): void { diff --git a/src/graphics/link/Link.ts b/src/graphics/link/Link.ts index 4ea66fd..da6b481 100644 --- a/src/graphics/link/Link.ts +++ b/src/graphics/link/Link.ts @@ -3,8 +3,9 @@ import { GraphicData, JlGraphic, JlGraphicTemplate, - convertToBezierPoints, + convertToBezierParams, } from 'src/jlgraphic'; +import { ILineGraphic } from 'src/jlgraphic/plugins/GraphicEditPlugin'; export interface ILinkData extends GraphicData { get code(): string; // 编号 @@ -24,7 +25,7 @@ export interface ILinkData extends GraphicData { eq(other: ILinkData): boolean; } -export class Link extends JlGraphic { +export class Link extends JlGraphic implements ILineGraphic { static Type = 'Link'; lineGraphic: Graphics; constructor() { @@ -32,6 +33,7 @@ export class Link extends JlGraphic { this.lineGraphic = new Graphics(); this.addChild(this.lineGraphic); } + get datas(): ILinkData { return this.getDatas(); } @@ -46,7 +48,7 @@ export class Link extends JlGraphic { ); if (this.datas.curve) { // 曲线 - const bps = convertToBezierPoints(this.datas.points); + const bps = convertToBezierParams(this.datas.points); bps.forEach((bp) => { this.lineGraphic.drawBezierCurve( bp.p1, @@ -66,7 +68,14 @@ export class Link extends JlGraphic { } } } - + get linePoints(): IPointData[] { + return this.datas.points; + } + set linePoints(points: IPointData[]) { + this.datas.points = points; + this.repaint(); + this.emit('pointupdate', this, this.datas.points); + } getStartPoint(): IPointData { return this.datas.points[0]; } diff --git a/src/graphics/link/LinkDrawAssistant.ts b/src/graphics/link/LinkDrawAssistant.ts index b995830..df1e2a7 100644 --- a/src/graphics/link/LinkDrawAssistant.ts +++ b/src/graphics/link/LinkDrawAssistant.ts @@ -17,7 +17,7 @@ import { JlGraphic, KeyListener, calculateMirrorPoint, - convertToBezierPoints, + convertToBezierParams, linePoint, pointPolygon, } from 'src/jlgraphic'; @@ -26,6 +26,7 @@ import AbsorbablePoint, { } from 'src/jlgraphic/graphic/AbsorbablePosition'; import { BezierCurveEditPlugin, + ILineGraphic, PolylineEditPlugin, } from 'src/jlgraphic/plugins/GraphicEditPlugin'; import { ILinkData, Link, LinkTemplate } from './Link'; @@ -64,7 +65,7 @@ export class LinkDraw extends GraphicDrawAssistant { this.container.addChild(this.graphic); this.graphicTemplate.curve = true; - new LinkPointsEditPlugin(app); + LinkPointsEditPlugin.init(app); } bind(): void { @@ -132,7 +133,7 @@ export class LinkDraw extends GraphicDrawAssistant { cp.copyFrom(mp); } - const bps = convertToBezierPoints(ps); + const bps = convertToBezierParams(ps); bps.forEach((bp) => this.graphic.drawBezierCurve( bp.p1, @@ -184,7 +185,7 @@ export class LinkGraphicHitArea implements IHitArea { const p = new Point(x, y); if (this.link.datas.curve) { // 曲线 - const bps = convertToBezierPoints(this.link.datas.points); + const bps = convertToBezierParams(this.link.datas.points); for (let i = 0; i < bps.length; i++) { const bp = bps[i]; if ( @@ -244,7 +245,7 @@ function buildAbsorbablePositions(link: Link): AbsorbablePosition[] { * @param index */ function onEditPointCreate( - g: JlGraphic, + g: ILineGraphic, dp: DraggablePoint, index: number ): void { @@ -267,6 +268,9 @@ export class LinkPointsEditPlugin extends GraphicInteractionPlugin { constructor(app: GraphicApp) { super(LinkPointsEditPlugin.Name, app); } + static init(app: GraphicApp): LinkPointsEditPlugin { + return new LinkPointsEditPlugin(app); + } filter(...grahpics: JlGraphic[]): Link[] | undefined { return grahpics.filter((g) => g.type == Link.Type) as Link[]; } @@ -291,11 +295,9 @@ export class LinkPointsEditPlugin extends GraphicInteractionPlugin { BezierCurveEditPlugin.Name ); if (!lep) { - lep = new BezierCurveEditPlugin( - link, - link.datas.points, - onEditPointCreate - ); + lep = new BezierCurveEditPlugin(link, { + onEditPointCreate, + }); link.addAssistantAppend(lep); } } else { @@ -304,11 +306,7 @@ export class LinkPointsEditPlugin extends GraphicInteractionPlugin { PolylineEditPlugin.Name ); if (!lep) { - lep = new PolylineEditPlugin( - link, - link.datas.points, - onEditPointCreate - ); + lep = new PolylineEditPlugin(link, { onEditPointCreate }); link.addAssistantAppend(lep); } } diff --git a/src/jlgraphic/app/JlGraphicApp.ts b/src/jlgraphic/app/JlGraphicApp.ts index f3eae80..feb8b50 100644 --- a/src/jlgraphic/app/JlGraphicApp.ts +++ b/src/jlgraphic/app/JlGraphicApp.ts @@ -41,7 +41,7 @@ import { JlGraphicAppKeyboardPlugin, KeyListener, } from '../plugins/KeyboardPlugin'; -import { ContextMenuPlugin } from '../ui/ContextMenu'; +import { ContextMenu, ContextMenuPlugin } from '../ui/ContextMenu'; import { getRectangleCenter, recursiveChildren } from '../utils/GraphicUtils'; export const AppConsts = { @@ -479,6 +479,14 @@ export class GraphicApp extends EventEmitter { return template as GT; } + /** + * 注册菜单 + * @param menu + */ + registerMenu(menu: ContextMenu) { + this.menuPlugin.registerMenu(menu); + } + /** * 使能websocket Stomp通信 */ @@ -609,7 +617,7 @@ export class GraphicApp extends EventEmitter { for (const item of this.graphicTemplateMap) { await item[1].loadAssets(); } - console.log('开始加载proto数据', protos); + // console.log('开始加载proto数据', protos); // 加载数据到图形存储 protos.forEach((proto) => { const template = this.getGraphicTemplatesByType(proto.graphicType); diff --git a/src/jlgraphic/core/JlGraphic.ts b/src/jlgraphic/core/JlGraphic.ts index 2050234..d2591e7 100644 --- a/src/jlgraphic/core/JlGraphic.ts +++ b/src/jlgraphic/core/JlGraphic.ts @@ -917,14 +917,15 @@ export abstract class JlGraphicTemplate { if (graphic._datas) { // 数据克隆 const datas = graphic.saveData(); - g.updateData(datas); + g.loadData(datas); } if (graphic._states) { // 状态克隆 const state = graphic.getStates().clone(); - g.updateStates(state); + g.loadState(state); } g.id = GraphicIdGenerator.next(); + g.repaint(); return g; } } diff --git a/src/jlgraphic/plugins/CommonMousePlugin.ts b/src/jlgraphic/plugins/CommonMousePlugin.ts index aa2f2f6..e63356a 100644 --- a/src/jlgraphic/plugins/CommonMousePlugin.ts +++ b/src/jlgraphic/plugins/CommonMousePlugin.ts @@ -143,6 +143,8 @@ export class CommonMouseTool extends AppInteractionPlugin { drag = false; graphicSelect = false; + rightTarget: DisplayObject | null = null; + /** * 可吸附位置列表 */ @@ -206,16 +208,22 @@ export class CommonMouseTool extends AppInteractionPlugin { this.clearCache(); } - setCursor() { - if (this.app.app.view.style) { + setCursor(e: FederatedMouseEvent) { + this.rightTarget = e.target as DisplayObject; + if (e.target instanceof JlCanvas && this.app.app.view.style) { this.app.app.view.style.cursor = 'grab'; } } resumeCursor() { - if (this.app.app.view.style) { + if ( + this.rightTarget && + this.rightTarget instanceof JlCanvas && + this.app.app.view.style + ) { this.app.app.view.style.cursor = 'inherit'; } + this.rightTarget = null; } onMouseDown(e: FederatedMouseEvent) { diff --git a/src/jlgraphic/plugins/CopyPlugin.ts b/src/jlgraphic/plugins/CopyPlugin.ts index 17cc15f..b007944 100644 --- a/src/jlgraphic/plugins/CopyPlugin.ts +++ b/src/jlgraphic/plugins/CopyPlugin.ts @@ -69,10 +69,12 @@ export class GraphicCopyPlugin { const template = app.getGraphicTemplatesByType(g.type); const clone = template.clone(g); this.copys.push(clone); + this.container.position.set(0, 0); this.container.addChild(clone); }); this.app.canvas.on('mousemove', this.onPointerMove, this); - this.app.canvas.on('pointertap', this.onFinish, this); + this.app.canvas.on('mouseup', this.onFinish, this); + this.app.canvas.on('rightup', this.cancle, this); this.keyListeners.forEach((kl) => { this.app.addKeyboardListener(kl); }); @@ -86,7 +88,8 @@ export class GraphicCopyPlugin { this.container.removeChildren(); this.app.canvas.removeChild(this.container); this.app.canvas.off('mousemove', this.onPointerMove, this); - this.app.canvas.off('pointertap', this.onFinish, this); + this.app.canvas.off('mouseup', this.onFinish, this); + this.app.canvas.off('rightup', this.cancle, this); this.keyListeners.forEach((kl) => { this.app.removeKeyboardListener(kl); }); diff --git a/src/jlgraphic/plugins/GraphicEditPlugin.ts b/src/jlgraphic/plugins/GraphicEditPlugin.ts index 22a7af7..0951c1f 100644 --- a/src/jlgraphic/plugins/GraphicEditPlugin.ts +++ b/src/jlgraphic/plugins/GraphicEditPlugin.ts @@ -2,6 +2,7 @@ import { Color, Container, DisplayObject, + FederatedMouseEvent, Graphics, IDestroyOptions, IPointData, @@ -10,10 +11,14 @@ 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'; -export abstract class GraphicEditPlugin extends Container { - graphic: JlGraphic; - constructor(g: JlGraphic) { +export abstract class GraphicEditPlugin< + DO extends DisplayObject = DisplayObject +> extends Container { + graphic: DO; + constructor(g: DO) { super(); this.graphic = g; this.zIndex = 2; @@ -40,18 +45,37 @@ export abstract class GraphicEditPlugin extends Container { } } -export abstract class LineEditPlugin extends GraphicEditPlugin { +const addWaypoint: MenuItemOptions = { + name: '添加路径点', +}; +const removeWaypoint: MenuItemOptions = { + name: '移除路径点', +}; +const clearWaypoints: MenuItemOptions = { + name: '清除所有路径点', +}; +const menuOptions: MenuOptions = { + name: '图形编辑点菜单', + groups: [ + { + items: [addWaypoint, removeWaypoint, clearWaypoints], + }, + ], +}; + +const EditPointContextMenu = ContextMenu.init(menuOptions); + +export interface ILineGraphic extends JlGraphic { + get linePoints(): IPointData[]; + set linePoints(points: IPointData[]); +} + +export abstract class LineEditPlugin extends GraphicEditPlugin { linePoints: IPointData[]; editedPoints: DraggablePoint[] = []; - onEditPointCreate?: onEditPointCreate; - constructor( - g: JlGraphic, - linePoints: IPointData[], - onEditPointCreate?: onEditPointCreate - ) { + constructor(g: ILineGraphic) { super(g); - this.linePoints = linePoints; - this.onEditPointCreate = onEditPointCreate; + this.linePoints = g.linePoints; this.graphic.on('pointupdate', this.reset, this); } @@ -71,21 +95,27 @@ export abstract class LineEditPlugin extends GraphicEditPlugin { } export type onEditPointCreate = ( - g: JlGraphic, + g: ILineGraphic, dp: DraggablePoint, index: number ) => void; + +export interface IEditPointOptions { + /** + * 编辑点创建处理 + */ + onEditPointCreate?: onEditPointCreate; +} + /** * 折线编辑(兼容线段) */ export class PolylineEditPlugin extends LineEditPlugin { static Name = 'line_points_edit'; - constructor( - g: JlGraphic, - linePoints: IPointData[], - onEditPointCreate?: onEditPointCreate - ) { - super(g, linePoints, onEditPointCreate); + options: IEditPointOptions; + constructor(g: ILineGraphic, options?: IEditPointOptions) { + super(g); + this.options = Object.assign({}, options); this.name = PolylineEditPlugin.Name; this.initEditPoints(); } @@ -102,8 +132,8 @@ export class PolylineEditPlugin extends LineEditPlugin { cp.y = tlp.y; this.graphic.repaint(); }); - if (this.onEditPointCreate) { - this.onEditPointCreate(this.graphic, dp, i); + if (this.options.onEditPointCreate) { + this.options.onEditPointCreate(this.graphic, dp, i); } this.editedPoints.push(dp); } @@ -120,19 +150,33 @@ export class PolylineEditPlugin extends LineEditPlugin { } } +export interface BezierCurveEditPointOptions extends IEditPointOptions { + // 曲线控制点辅助连线颜色 + auxiliaryLineColor?: string; + // // 拖拽点颜色 + // pointColor?: string; + // 连接点处是否平滑(点左右控制点是否在一条直线),默认true + smooth?: boolean; + // 控制点是否完全对称(对称必平滑) + symmetry?: boolean; +} + +export interface ICompleteBezierCurveEditPointOptions + extends BezierCurveEditPointOptions { + smooth: boolean; +} + /** * 贝塞尔曲线编辑 */ export class BezierCurveEditPlugin extends LineEditPlugin { static Name = 'bezier_curve_points_edit'; + options: ICompleteBezierCurveEditPointOptions; // 曲线控制点辅助线 auxiliaryLines: Graphics[] = []; - constructor( - g: JlGraphic, - linePoints: IPointData[], - onEditPointCreate?: onEditPointCreate - ) { - super(g, linePoints, onEditPointCreate); + constructor(g: ILineGraphic, options?: BezierCurveEditPointOptions) { + super(g); + this.options = Object.assign({}, { smooth: true }, options); this.name = BezierCurveEditPlugin.Name; this.initEditPoints(); } @@ -143,6 +187,7 @@ export class BezierCurveEditPlugin extends LineEditPlugin { } initEditPoints() { + console.log('initEditPoints'); const cps = this.graphic.localToCanvasPoints(...this.linePoints); for (let i = 0; i < cps.length; i++) { const p = cps[i]; @@ -160,37 +205,84 @@ export class BezierCurveEditPlugin extends LineEditPlugin { this.drawAuxiliaryLine(line, p, np); this.auxiliaryLines.push(line); } + dp.on('rightclick', (e: FederatedMouseEvent) => { + // dp.getGraphicApp().openContextMenu(EditPointContextMenu.DefaultMenu); + if (c === 0) { + const app = dp.getGraphicApp(); + app.registerMenu(EditPointContextMenu); + removeWaypoint.onClick = () => { + let points; + if (i === 0) { + // 第一个点 + if (this.linePoints.length > 4) { + 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); + } + }); dp.on('dragmove', (de: GraphicDragEvent) => { const tlp = this.graphic.canvasToLocalPoint(dp.position); const cp = this.linePoints[i]; cp.x = tlp.x; cp.y = tlp.y; - if (c === 0 && !startOrEnd) { - 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; - } else if (c === 1 && i !== 1) { - const bp = this.linePoints[i - 1]; - const fp2 = this.linePoints[i - 2]; - const distance = distance2(bp, fp2); - const mp = calculateMirrorPoint(bp, cp, distance); - fp2.x = mp.x; - fp2.y = mp.y; - } else if (c === 2 && i !== cps.length - 2) { - const bp = this.linePoints[i + 1]; - const np2 = this.linePoints[i + 2]; - const distance = distance2(bp, np2); - const mp = calculateMirrorPoint(bp, cp, distance); - np2.x = mp.x; - np2.y = mp.y; + if (this.options.smooth || this.options.symmetry) { + if (c === 0 && !startOrEnd) { + 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; + } else if (c === 1 && i !== 1) { + const bp = this.linePoints[i - 1]; + const fp2 = this.linePoints[i - 2]; + let mp; + if (this.options.symmetry) { + mp = calculateMirrorPoint(bp, cp); + } else { + const distance = distance2(bp, fp2); + mp = calculateMirrorPoint(bp, cp, distance); + } + fp2.x = mp.x; + fp2.y = mp.y; + } else if (c === 2 && i !== cps.length - 2) { + const bp = this.linePoints[i + 1]; + const np2 = this.linePoints[i + 2]; + let mp; + if (this.options.symmetry) { + mp = calculateMirrorPoint(bp, cp); + } else { + const distance = distance2(bp, np2); + mp = calculateMirrorPoint(bp, cp, distance); + } + np2.x = mp.x; + np2.y = mp.y; + } } this.graphic.repaint(); }); - if (this.onEditPointCreate) { - this.onEditPointCreate(this.graphic, dp, i); + if (this.options.onEditPointCreate) { + this.options.onEditPointCreate(this.graphic, dp, i); } this.editedPoints.push(dp); if (this.auxiliaryLines.length > 0) { @@ -202,7 +294,11 @@ export class BezierCurveEditPlugin extends LineEditPlugin { drawAuxiliaryLine(line: Graphics, p1: IPointData, p2: IPointData) { line.clear(); - line.lineStyle(1, new Color('blue')); + if (this.options.auxiliaryLineColor) { + line.lineStyle(1, new Color(this.options.auxiliaryLineColor)); + } else { + line.lineStyle(1, new Color('blue')); + } line.moveTo(p1.x, p1.y); line.lineTo(p2.x, p2.y); } diff --git a/src/jlgraphic/ui/ContextMenu.ts b/src/jlgraphic/ui/ContextMenu.ts index da0936e..8a0f9da 100644 --- a/src/jlgraphic/ui/ContextMenu.ts +++ b/src/jlgraphic/ui/ContextMenu.ts @@ -1,4 +1,4 @@ -import { Container, Graphics, Point, Rectangle, Text, utils } from 'pixi.js'; +import { Color, Container, Graphics, Point, Rectangle, Text } from 'pixi.js'; import { GraphicApp } from '../app'; import { OutOfBound } from '../utils'; import { @@ -18,41 +18,11 @@ export class ContextMenuPlugin { constructor(app: GraphicApp) { this.app = app; } - /** - * 添加菜单 - */ - addContextMenu(menu: ContextMenu) { - if (this.contextMenuMap.has(menu.menuName)) { - console.log(`已经存在name=${menu.menuName}的菜单`); - } + + registerMenu(menu: ContextMenu) { this.contextMenuMap.set(menu.menuName, menu); menu.plugin = this; } - /** - * 通过菜单名称打开菜单到指定坐标 - * @param menuName - * @param global - */ - openMenu(menuName: string, global: Point) { - const menu = this.contextMenuMap.get(menuName); - if (menu) { - this.open(menu, global); - } else { - console.warn(`不存在name=${menuName}的菜单`); - } - } - /** - * 通过菜单名称关闭菜单 - * @param menuName - */ - closeMenu(menuName: string) { - const menu = this.contextMenuMap.get(menuName); - if (menu) { - this.close(menu); - } else { - console.warn(`不存在name=${menuName}的菜单`); - } - } /** * 获取视口屏幕宽度 @@ -131,13 +101,13 @@ export class ContextMenuPlugin { } /** - * 上下文菜单 + * 上下文菜单,多级嵌套 */ export class ContextMenu extends Container { + _plugin?: ContextMenuPlugin; parentMenuItem?: ContextMenuItem; openedSubMenu?: ContextMenu; menuOptions: MenuCompletionOptions; - _plugin?: ContextMenuPlugin; opened = false; bg: Graphics; title?: Text; @@ -149,20 +119,32 @@ export class ContextMenu extends Container { constructor(menuOptions: MenuOptions, parentMenuItem?: ContextMenuItem) { super(); this.menuOptions = Object.assign({}, DefaultWhiteMenuOptions, menuOptions); + this.name = this.menuOptions.name; this.bg = new Graphics(); + this.addChild(this.bg); this.groups = []; this.init(); this.parentMenuItem = parentMenuItem; } + static init(options: MenuOptions): ContextMenu { + return new ContextMenu(options); + } + public get style(): MenuCompletionStyleOptions { return this.menuOptions.style; } + /** + * 父级菜单 + */ public get parentMenu(): ContextMenu | undefined { return this.parentMenuItem?.menu; } + /** + * 最顶级菜单 + */ public get rootMenu(): ContextMenu { if (this.parentMenu) { return this.parentMenu.rootMenu; @@ -226,7 +208,7 @@ export class ContextMenu extends Container { const options = this.menuOptions; let maxItemWidth = 0; let borderHeight = 0; - options.groups.forEach(group => { + options.groups.forEach((group) => { const menuGroup = new MenuGroup(this, group); this.groups.push(menuGroup); borderHeight += menuGroup.totalHeight; @@ -246,10 +228,10 @@ export class ContextMenu extends Container { if (options.style.border) { this.bg.lineStyle( options.style.borderWidth, - utils.string2hex(options.style.borderColor) + new Color(options.style.borderColor) ); } - this.bg.beginFill(utils.string2hex(options.style.backgroundColor)); + this.bg.beginFill(new Color(options.style.backgroundColor)); if (options.style.borderRoundRadius > 0) { this.bg.drawRoundedRect( 0, @@ -263,10 +245,7 @@ export class ContextMenu extends Container { } this.bg.endFill(); let groupHeight = 0; - this.bg.lineStyle( - splitLineWidth, - utils.string2hex(options.style.borderColor) - ); + this.bg.lineStyle(splitLineWidth, new Color(options.style.borderColor)); for (let i = 0; i < this.groups.length; i++) { const group = this.groups[i]; group.updateItemBox(maxItemWidth); @@ -281,7 +260,6 @@ export class ContextMenu extends Container { groupHeight = splitLineY + splitLineWidth; } - this.addChild(this.bg); this.addChild(...this.groups); this.interactive = true; @@ -293,12 +271,82 @@ export class ContextMenu extends Container { }); } + initGroups() { + this.groups = []; + const options = this.menuOptions; + options.groups.forEach((group) => { + const menuGroup = new MenuGroup(this, group); + this.groups.push(menuGroup); + }); + this.addChild(...this.groups); + } + initTitle() { if (this.menuOptions.title) { this.title = new Text(this.menuOptions.title, { align: 'left' }); } } + updateBg() { + const options = this.menuOptions; + let maxItemWidth = 0; + let borderHeight = 0; + const splitLineWidth = 1; + + this.groups.forEach((menuGroup) => { + borderHeight += menuGroup.totalHeight; + if (menuGroup.maxWidth > maxItemWidth) { + maxItemWidth = menuGroup.maxWidth; + } + }); + + const bgWidth = maxItemWidth + this.padding * 2; + const bgHeight = + borderHeight + + this.groups.length * this.padding * 2 + + (this.groups.length - 1) * splitLineWidth; + + if (options.style.border) { + this.bg.lineStyle( + options.style.borderWidth, + new Color(options.style.borderColor) + ); + } + this.bg.beginFill(new Color(options.style.backgroundColor)); + if (options.style.borderRoundRadius > 0) { + this.bg.drawRoundedRect( + 0, + 0, + bgWidth, + bgHeight, + options.style.borderRoundRadius + ); + } else { + this.bg.drawRect(0, 0, bgWidth, bgHeight); + } + this.bg.endFill(); + let groupHeight = 0; + this.bg.lineStyle(splitLineWidth, new Color(options.style.borderColor)); + for (let i = 0; i < this.groups.length; i++) { + const group = this.groups[i]; + group.updateItemBox(maxItemWidth); + group.position.set(this.padding, groupHeight + this.padding); + if (i === this.groups.length - 1) { + // 最后一个 + break; + } + const splitLineY = groupHeight + group.height + this.padding * 2; + this.bg.moveTo(0, splitLineY); + this.bg.lineTo(bgWidth, splitLineY); + groupHeight = splitLineY + splitLineWidth; + } + } + + update() { + this.groups.forEach((group) => group.update()); + this.updateBg(); + } + public get menuName(): string { return this.menuOptions.name; } @@ -324,6 +372,7 @@ export class ContextMenu extends Container { if (this.parentMenu) { this.parentMenu.openSub(this, global); } else { + this.update(); this.plugin.open(this, global); } } @@ -370,7 +419,7 @@ class MenuGroup extends Container { super(); this.config = config; this.menu = menu; - config.items.forEach(item => { + config.items.forEach((item) => { this.items.push(new ContextMenuItem(menu, item)); }); for (let i = 0; i < this.items.length; i++) { @@ -392,12 +441,12 @@ class MenuGroup extends Container { public get maxWidth(): number { const maxNameWidth = this.items - .map(item => item.nameBounds.width) - .sort() + .map((item) => item.nameBounds.width) + .sort((a, b) => a - b) .reverse()[0]; const maxShortcutWidth = this.items - .map(item => item.shortcutKeyBounds.width) - .sort() + .map((item) => item.shortcutKeyBounds.width) + .sort((a, b) => a - b) .reverse()[0]; const maxWidth = maxNameWidth + @@ -410,12 +459,18 @@ class MenuGroup extends Container { public get totalHeight(): number { let total = 0; - this.items.forEach(item => (total += item.totalHeight)); + this.items.forEach((item) => (total += item.totalHeight)); return total; } + update() { + this.items.forEach((item) => item.update()); + } + updateItemBox(maxItemWidth: number) { - this.items.forEach(item => item.updateBox(maxItemWidth, item.totalHeight)); + this.items.forEach((item) => + item.updateBox(maxItemWidth, item.totalHeight) + ); } } @@ -428,7 +483,7 @@ class ContextMenuItem extends Container { /** * 名称文本 */ - nameText?: Text; + nameText: Text; /** * 快捷键文本 */ @@ -446,7 +501,12 @@ class ContextMenuItem extends Container { this.box = new Graphics(); this.addChild(this.box); - this.initNameText(); + this.nameText = new Text(this.config.name, { + fontSize: this.fontSize, + fill: this.fontColor, + }); + this.addChild(this.nameText); + this.initShortcutKeyText(); this.initSubMenu(); } @@ -553,15 +613,6 @@ class ContextMenuItem extends Container { return this.style.fontColor; } - initNameText(): Text { - this.nameText = new Text(this.config.name, { - fontSize: this.fontSize, - fill: this.fontColor, - }); - this.addChild(this.nameText); - return this.nameText; - } - initShortcutKeyText(): Text | undefined { if (this.config.shortcutKeys && this.config.shortcutKeys.length > 0) { this.shortcutKeyText = new Text(this.config.shortcutKeys.join('+'), { @@ -603,7 +654,7 @@ class ContextMenuItem extends Container { if (this.style && this.style.hoverColor) { hoverColor = this.style.hoverColor; } - box.beginFill(utils.string2hex(hoverColor)); + box.beginFill(new Color(hoverColor)); if (style.borderRoundRadius > 0) { box.drawRoundedRect(0, 0, width, height, style.borderRoundRadius); } else { @@ -665,4 +716,26 @@ class ContextMenuItem extends Container { } }); } + + update() { + this.nameText.text = this.config.name; + this.nameText.style.fontSize = this.fontSize; + this.nameText.style.fill = this.fontColor; + + if (this.shortcutKeyText) { + if (this.config.shortcutKeys && this.config.shortcutKeys.length > 0) { + this.shortcutKeyText.text = this.config.shortcutKeys.join('+'); + this.shortcutKeyText.style.fontSize = this.fontSize; + this.shortcutKeyText.style.fill = this.fontColor; + } else { + this.shortcutKeyText.visible = false; + } + } else { + this.initShortcutKeyText(); + } + + if (this.subMenu) { + this.subMenu.update(); + } + } } diff --git a/src/jlgraphic/ui/Menu.ts b/src/jlgraphic/ui/Menu.ts index 3ffbbd5..fb51314 100644 --- a/src/jlgraphic/ui/Menu.ts +++ b/src/jlgraphic/ui/Menu.ts @@ -81,6 +81,10 @@ export interface MenuItemOptions { * 是否禁用,默认不禁用 */ disabled?: boolean; + /** + * 是否显示,默认显示 + */ + visible?: boolean; /** * 快捷键 */ @@ -89,10 +93,6 @@ export interface MenuItemOptions { * 点击处理 */ onClick?: () => void; - // /** - // * 菜单项字体样式,覆盖MenuOptions中的通用样式 - // */ - // style?: MenuItemStyle; fontColor?: string; /** * 子菜单 @@ -109,10 +109,6 @@ export interface MenuItemStyle { * 字体颜色 */ fontColor?: string; - // /** - // * 字体对齐 - // */ - // align?: TextStyleAlign; /** * hover颜色 */ @@ -127,8 +123,13 @@ export interface MenuItemStyle { padding: number[] | number; // 内边距 } export interface MenuCompletionItemStyle extends MenuItemStyle { + /** + * 文字颜色 + */ fontColor: string; - // align: TextStyleAlign; + /** + * 激活颜色 + */ hoverColor: string; /** * 禁用下字体颜色 @@ -157,7 +158,6 @@ export const DefaultWhiteStyleOptions: MenuCompletionStyleOptions = { fontSize: 16, fontColor: '#000000', padding: [5, 25], - // align: 'left', hoverColor: '#1E78DB', disabledFontColor: '#9D9D9D', }, diff --git a/src/jlgraphic/utils/GraphicUtils.ts b/src/jlgraphic/utils/GraphicUtils.ts index 021ff30..2111ab9 100644 --- a/src/jlgraphic/utils/GraphicUtils.ts +++ b/src/jlgraphic/utils/GraphicUtils.ts @@ -79,18 +79,18 @@ export function recursiveFindChild( return null; } -export interface BezierPoints { +export interface BezierParam { p1: IPointData; p2: IPointData; cp1: IPointData; cp2: IPointData; } -export function convertToBezierPoints(points: IPointData[]): BezierPoints[] { - if (points.length < 4 && points.length % 3 !== 1) { +export function convertToBezierParams(points: IPointData[]): BezierParam[] { + if (points.length < 4 || points.length % 3 !== 1) { throw new Error(`bezierCurve 数据错误: ${points}`); } - const bps: BezierPoints[] = []; + const bps: BezierParam[] = []; for (let i = 0; i < points.length - 3; i += 3) { const p1 = new Point(points[i].x, points[i].y); const p2 = new Point(points[i + 3].x, points[i + 3].y); @@ -106,6 +106,69 @@ export function convertToBezierPoints(points: IPointData[]): BezierPoints[] { return bps; } +/** + * 根据分段数计算贝塞尔曲线所有点坐标 + * @param basePoints + * @param segmentsCount + * @returns + */ +export function calculateBezierPoints( + basePoints: IPointData[], + segmentsCount: number +): Point[] { + const bps = convertToBezierParams(basePoints); + const points: Point[] = []; + bps.forEach((bp) => { + points.push( + ...calculateOneBezierPoints(bp.p1, bp.p2, bp.cp1, bp.cp2, segmentsCount) + ); + }); + return points; +} + +/** + * 根据分段数计算贝塞尔曲线所有点坐标 + * @param basePoints + * @param segmentsCount + * @returns + */ +export function calculateOneBezierPoints( + p1: IPointData, + p2: IPointData, + cp1: IPointData, + cp2: IPointData, + segmentsCount: number +): Point[] { + const points: Point[] = []; + const fromX = p1.x; + const fromY = p1.y; + const n = segmentsCount; + let dt = 0; + let dt2 = 0; + let dt3 = 0; + let t2 = 0; + let t3 = 0; + const cpX = cp1.x; + const cpY = cp1.y; + const cpX2 = cp2.x; + const cpY2 = cp2.y; + const toX = p2.x; + const toY = p2.y; + points.push(new Point(p1.x, p1.y)); + for (let i = 1, j = 0; i <= n; ++i) { + j = i / n; + dt = 1 - j; + dt2 = dt * dt; + dt3 = dt2 * dt; + t2 = j * j; + t3 = t2 * j; + const px = dt3 * fromX + 3 * dt2 * j * cpX + 3 * dt * t2 * cpX2 + t3 * toX; + const py = dt3 * fromY + 3 * dt2 * j * cpY + 3 * dt * t2 * cpY2 + t3 * toY; + points.push(new Point(px, py)); + } + return points; +} + /** * 计算矩形中点 */ @@ -367,7 +430,7 @@ export function calculateIntersectionPointOfCircleAndPoint( * 计算点基于点的镜像点坐标 * @param bp 基准点 * @param p 待镜像的点坐标 - * @param distance 镜像点到基准点的距离,默认为p到基准点的距离 + * @param distance 镜像点到基准点的距离,默认为p到基准点的距离,即对称 * @returns */ export function calculateMirrorPoint( diff --git a/src/layouts/DrawLayout.vue b/src/layouts/DrawLayout.vue index 9c7eda1..a0ed868 100644 --- a/src/layouts/DrawLayout.vue +++ b/src/layouts/DrawLayout.vue @@ -25,13 +25,21 @@ onMounted(() => { drawApp = new JlDrawApp(dom); initDrawApp(drawApp); loadDrawDatas(drawApp); + window.addEventListener('resize', onResize); } }); +function onResize() { + if (drawApp) { + drawApp.onDomResize(document.body.clientWidth, document.body.clientHeight); + } +} + onUnmounted(() => { if (drawApp) { drawApp.destroy(); } + window.removeEventListener('resize', onResize); document.body.style.overflow = 'auto'; });