From c9acbcfb78c9faa3f01860e7c73f26dd00ad0fe3 Mon Sep 17 00:00:00 2001 From: Ralph Soika Date: Wed, 17 Jan 2024 16:00:49 +0100 Subject: [PATCH] experimental implemenation Issue #315 --- .../open-bpmn-glsp/css/helper-lines.css | 33 +++ .../bpmn-helper-line-feedback.ts | 237 ++++++++++-------- .../bpmn-helper-line-manager-default.ts | 112 ++++++--- .../bpmn-helper-line-manager.ts | 9 +- .../bpmn-helper-line-module.ts | 12 +- .../src/bpmn-helper-lines/model.ts | 9 +- .../open-bpmn-glsp/src/bpmn-helperlines.tsx | 60 ++++- .../open-bpmn-glsp/src/di.config.ts | 36 +-- 8 files changed, 333 insertions(+), 175 deletions(-) create mode 100644 open-bpmn.glsp-client/open-bpmn-glsp/css/helper-lines.css diff --git a/open-bpmn.glsp-client/open-bpmn-glsp/css/helper-lines.css b/open-bpmn.glsp-client/open-bpmn-glsp/css/helper-lines.css new file mode 100644 index 00000000..07a7de0c --- /dev/null +++ b/open-bpmn.glsp-client/open-bpmn-glsp/css/helper-lines.css @@ -0,0 +1,33 @@ +/******************************************************************************** + * Copyright (c) 2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +.helper-line { + pointer-events: none; + stroke: red; + stroke-width: 1px; + opacity: 1; +} + +.selection-bounds { + pointer-events: none; + fill: blue; + fill-opacity: 0.05; + stroke-linejoin: miter; + stroke-linecap: round; + stroke: darkblue; + stroke-width: 1px; + stroke-dasharray: 2; +} diff --git a/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-feedback.ts b/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-feedback.ts index e828aa8e..477a805d 100644 --- a/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-feedback.ts +++ b/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-feedback.ts @@ -13,55 +13,64 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ - import { Action, Bounds, + BoundsAwareModelElement, CommandExecutionContext, CommandReturn, + FeedbackCommand, + GChildElement, + GLabel, GModelElement, GModelRoot, + ILogger, + Point, TYPES, Viewport, - findParentByFeature, - isBoundsAware, - isViewport -} from '@eclipse-glsp/sprotty'; -import { inject, injectable } from 'inversify'; -import { partition } from 'lodash'; -//import '../../../css/helper-lines.css'; - -import { - BoundsAwareModelElement, - FeedbackCommand, bottom, bottomCenter, bottomLeft, bottomRight, center, - findTopLevelElementByFeature, forEachElement, getMatchingElements, + findParentByFeature, + findTopLevelElementByFeature, + forEachElement, + getMatchingElements, getViewportBounds, isAbove, isBefore, + isBoundsAware, + isDecoration, + isRoutable, + isViewport, + isVisibleOnCanvas, left, middle, middleLeft, middleRight, right, sortBy, + toAbsoluteBounds, top, topCenter, topLeft, topRight } from '@eclipse-glsp/client'; -// import { BoundsAwareModelElement, findTopLevelElementByFeature, forEachElement, getMatchingElements } from '../../utils/gmodel-util'; -//import { getViewportBounds } from '../../utils/viewpoint-util'; + + +import { inject, injectable } from 'inversify'; +import { partition } from 'lodash'; +//import './helper-lines.css'; +import '../../css/diagram.css'; import { HelperLine, HelperLineType, SelectionBounds, isHelperLine, isSelectionBounds } from './model'; export type ViewportLineType = typeof HelperLineType.Center | typeof HelperLineType.Middle | string; -export const ALL_ELEMENT_LINE_TYPES = Object.values(HelperLineType); -export const ALL_VIEWPORT_LINE_TYPES = [HelperLineType.Center, HelperLineType.Middle]; +export type AlignmentElementFilter = (element: BoundsAwareModelElement, referenceElementIds: string[]) => boolean; + +export const isTopLevelBoundsAwareElement: AlignmentElementFilter = element => + findTopLevelElementByFeature(element, isBoundsAware, isViewport) === element; export interface DrawHelperLinesFeedbackAction extends Action { kind: typeof DrawHelperLinesFeedbackAction.KIND; @@ -69,8 +78,20 @@ export interface DrawHelperLinesFeedbackAction extends Action { elementLines?: HelperLineType[]; viewportLines?: ViewportLineType[]; alignmentEpsilon?: number; + alignmentElementFilter?: AlignmentElementFilter; + debug?: boolean; } +export const ALL_ELEMENT_LINE_TYPES = Object.values(HelperLineType); +export const ALL_VIEWPORT_LINE_TYPES = [HelperLineType.Center, HelperLineType.Middle]; + +export const DEFAULT_ELEMENT_LINES = ALL_ELEMENT_LINE_TYPES; +export const DEFAULT_VIEWPORT_LINES = ALL_VIEWPORT_LINE_TYPES; +export const DEFAULT_EPSILON = 1; +export const DEFAULT_ALIGNABLE_ELEMENT_FILTER = (element: BoundsAwareModelElement): boolean => + isVisibleOnCanvas(element) && !isRoutable(element) && !(element instanceof GLabel) && !isDecoration(element); +export const DEFAULT_DEBUG = true; + export namespace DrawHelperLinesFeedbackAction { export const KIND = 'drawHelperLines'; @@ -86,199 +107,208 @@ export namespace DrawHelperLinesFeedbackAction { export class DrawHelperLinesFeedbackCommand extends FeedbackCommand { static readonly KIND = DrawHelperLinesFeedbackAction.KIND; - constructor(@inject(TYPES.Action) public action: DrawHelperLinesFeedbackAction) { + protected elementIds: string[]; + protected elementLines: HelperLineType[]; + protected viewportLines: ViewportLineType[]; + protected alignmentEpsilon: number; + protected alignableElementFilter: AlignmentElementFilter; + protected isAlignableElementPredicate: (element: GModelElement) => element is BoundsAwareModelElement; + protected debug: boolean; + + constructor( + @inject(TYPES.Action) action: DrawHelperLinesFeedbackAction, + @inject(TYPES.ILogger) protected logger: ILogger + ) { super(); + this.elementIds = action.elementIds; + this.elementLines = action.elementLines ?? DEFAULT_ELEMENT_LINES; + this.viewportLines = action.viewportLines ?? DEFAULT_VIEWPORT_LINES; + this.alignmentEpsilon = action.alignmentEpsilon ?? DEFAULT_EPSILON; + this.alignableElementFilter = action.alignmentElementFilter ?? DEFAULT_ALIGNABLE_ELEMENT_FILTER; + this.isAlignableElementPredicate = this.isAlignableElement.bind(this); + this.debug = action.debug ?? DEFAULT_DEBUG; } execute(context: CommandExecutionContext): CommandReturn { removeHelperLines(context.root); removeSelectionBounds(context.root); - const boundsAwareElements = getMatchingElements(context.root.index, this.isCompareElement); + const alignableElements = getMatchingElements(context.root.index, this.isAlignableElementPredicate); - // for (const element of boundsAwareElements) { - // console.log(' BEFORE boundsAwareElements : ' + element.type); - // } - - // Add a filter to exclude elements with specified types - const filteredMatchElements = boundsAwareElements.filter(element => - element.type !== 'BPMNLabel' && - element.type !== 'sequenceFlow' && - element.type !== 'messageFlow' && - element.type !== 'association' && - element.type !== 'lane-divider' - ); - - // for (const element of filteredMatchElements) { - // console.log(' AFTER boundsAwareElements : ' + element.type); - // } - + console.log('Halllo Welt folgende Elementw sind allignambe :'); + for (const element of alignableElements) { + console.log('....... Element : ' + element.id + ' type=' + element.type); + } - const [referenceElements, elements] = partition(filteredMatchElements, element => this.action.elementIds.includes(element.id)); + //console.log('All alignable elements: ', alignableElements); + const [referenceElements, elements] = partition(alignableElements, element => this.elementIds.includes(element.id)); + // console.log('Split alignable elements into reference elements and other elements: ', referenceElements, elements); if (referenceElements.length === 0) { + this.log('--> No helper lines as we do not have any reference elements.'); return context.root; } const referenceBounds = this.calcReferenceBounds(referenceElements); + // console.log('Bounds encompasing all reference elements: ', referenceBounds); const helperLines = this.calcHelperLines(elements, referenceBounds, context); if (referenceElements.length > 1) { context.root.add(new SelectionBounds(referenceBounds)); + this.log('Render selection bounds for more than one reference element:', referenceBounds); } helperLines.forEach(helperLine => context.root.add(helperLine)); + if (helperLines.length > 0) { + this.log(`--> Add ${helperLines.length} helper lines to root:`, helperLines); + } else { + this.log('--> Add no helper lines to root.'); + } return context.root; } - protected isCompareElement(element: GModelElement): element is BoundsAwareModelElement { - return isBoundsAware(element) && findTopLevelElementByFeature(element, isBoundsAware, isViewport) === element; + protected isAlignableElement(element: GModelElement): element is BoundsAwareModelElement { + return isBoundsAware(element) && this.alignableElementFilter(element, this.elementIds); } protected calcReferenceBounds(referenceElements: BoundsAwareModelElement[]): Bounds { + return referenceElements.map(element => this.calcBounds(element)).reduce(Bounds.combine, Bounds.EMPTY); + } - - //return referenceElements.map(element => element.bounds).reduce((combined, next) => Bounds.combine(combined, next), Bounds.EMPTY); - // for (const element of referenceElements) { - // console.log(' BEFORE: included element : ' + element.type); - // } - - // Add a filter to exclude elements with specified types - const filteredElements = referenceElements.filter(element => - element.type !== 'BPMNLabel' && - element.type !== 'sequenceFlow' && - element.type !== 'messageFlow' && - element.type !== 'association' && - element.type !== 'lane-divider' - ); - - // for (const element of filteredElements) { - // console.log(' AFTER: included element : ' + element.type); - // } - - - - // Use map and reduce on the filtered elements - return filteredElements.map(element => element.bounds).reduce((combined, next) => Bounds.combine(combined, next), Bounds.EMPTY); - - + protected calcBounds(element: BoundsAwareModelElement): Bounds { + return toAbsoluteBounds(element); } protected calcHelperLines(elements: BoundsAwareModelElement[], bounds: Bounds, context: CommandExecutionContext): HelperLine[] { const helperLines: HelperLine[] = []; const viewport = findParentByFeature(context.root, isViewport); if (viewport) { - helperLines.push(...this.calcHelperLinesForViewport(viewport, bounds)); + helperLines.push(...this.calcHelperLinesForViewport(viewport, bounds, this.viewportLines)); } - elements.flatMap(element => this.calcHelperLinesForElement(element, bounds)).forEach(line => helperLines.push(line)); + elements + .flatMap(element => this.calcHelperLinesForElement(element, bounds, this.elementLines)) + .forEach(line => helperLines.push(line)); return helperLines; } - protected calcHelperLinesForViewport( - root: Viewport & GModelRoot, - bounds: Bounds, - lineTypes: HelperLineType[] = this.action.viewportLines ?? ALL_VIEWPORT_LINE_TYPES - ): HelperLine[] { + protected calcHelperLinesForViewport(root: Viewport & GModelRoot, bounds: Bounds, lineTypes: HelperLineType[]): HelperLine[] { const helperLines: HelperLine[] = []; + this.log('Find helperlines for viewport:', root); const viewportBounds = getViewportBounds(root, root.canvasBounds); if (lineTypes.includes(HelperLineType.Center) && this.isAligned(center, viewportBounds, bounds, 2)) { helperLines.push(new HelperLine(topCenter(viewportBounds), bottomCenter(viewportBounds), HelperLineType.Center)); + this.log('- Reference bounds center align with viewport.', viewportBounds); } if (lineTypes.includes(HelperLineType.Middle) && this.isAligned(middle, viewportBounds, bounds, 2)) { helperLines.push(new HelperLine(middleLeft(viewportBounds), middleRight(viewportBounds), HelperLineType.Middle)); + this.log('- Reference bounds middle align with viewport.', viewportBounds); + } + if (helperLines.length > 0) { + this.log(`--> Add ${helperLines.length} helperlines for viewport:`, helperLines); } return helperLines; } - protected calcHelperLinesForElement( - element: BoundsAwareModelElement, - bounds: Bounds, - lineTypes: HelperLineType[] = this.action.elementLines ?? ALL_ELEMENT_LINE_TYPES - ): HelperLine[] { - return this.calcHelperLinesForBounds(element.bounds, bounds, lineTypes); + protected calcHelperLinesForElement(element: BoundsAwareModelElement, bounds: Bounds, lineTypes: HelperLineType[]): HelperLine[] { + this.log('Find helperlines for element:', element); + return this.calcHelperLinesForBounds(this.calcBounds(element), bounds, lineTypes); } - protected calcHelperLinesForBounds( - elementBounds: Bounds, - bounds: Bounds, - lineTypes: HelperLineType[] = this.action.elementLines ?? ALL_ELEMENT_LINE_TYPES - ): HelperLine[] { + protected calcHelperLinesForBounds(elementBounds: Bounds, bounds: Bounds, lineTypes: HelperLineType[]): HelperLine[] { const helperLines: HelperLine[] = []; - if (lineTypes.includes(HelperLineType.Left) && this.isAligned(left, elementBounds, bounds)) { + if (lineTypes.includes(HelperLineType.Left) && this.isAligned(left, elementBounds, bounds, this.alignmentEpsilon)) { const [above, below] = sortBy(top, elementBounds, bounds); // higher top-value ==> lower helperLines.push(new HelperLine(bottomLeft(below), topLeft(above), HelperLineType.Left)); + this.log('- Reference bounds left align with element', elementBounds); } - if (lineTypes.includes(HelperLineType.Center) && this.isAligned(center, elementBounds, bounds)) { + if (lineTypes.includes(HelperLineType.Center) && this.isAligned(center, elementBounds, bounds, this.alignmentEpsilon)) { const [above, below] = sortBy(top, elementBounds, bounds); // higher top-value ==> lower helperLines.push(new HelperLine(topCenter(above), bottomCenter(below), HelperLineType.Center)); + this.log('- Reference bounds center align with element', elementBounds); } - if (lineTypes.includes(HelperLineType.Right) && this.isAligned(right, elementBounds, bounds)) { + if (lineTypes.includes(HelperLineType.Right) && this.isAligned(right, elementBounds, bounds, this.alignmentEpsilon)) { const [above, below] = sortBy(top, elementBounds, bounds); // higher top-value ==> lower helperLines.push(new HelperLine(bottomRight(below), topRight(above), HelperLineType.Right)); + this.log('- Reference bounds right align with element', elementBounds); } - if (lineTypes.includes(HelperLineType.Bottom) && this.isAligned(bottom, elementBounds, bounds)) { + if (lineTypes.includes(HelperLineType.Bottom) && this.isAligned(bottom, elementBounds, bounds, this.alignmentEpsilon)) { const [before, after] = sortBy(left, elementBounds, bounds); // higher left-value ==> more to the right helperLines.push(new HelperLine(bottomLeft(before), bottomRight(after), HelperLineType.Bottom)); + this.log('- Reference bounds bottom align with element', elementBounds); } - if (lineTypes.includes(HelperLineType.Middle) && this.isAligned(middle, elementBounds, bounds)) { + if (lineTypes.includes(HelperLineType.Middle) && this.isAligned(middle, elementBounds, bounds, this.alignmentEpsilon)) { const [before, after] = sortBy(left, elementBounds, bounds); // higher left-value ==> more to the right helperLines.push(new HelperLine(middleLeft(before), middleRight(after), HelperLineType.Middle)); + this.log('- Reference bounds middle align with element', elementBounds); } - if (lineTypes.includes(HelperLineType.Top) && this.isAligned(top, elementBounds, bounds)) { + if (lineTypes.includes(HelperLineType.Top) && this.isAligned(top, elementBounds, bounds, this.alignmentEpsilon)) { const [before, after] = sortBy(left, elementBounds, bounds); // higher left-value ==> more to the right helperLines.push(new HelperLine(topLeft(before), topRight(after), HelperLineType.Top)); + this.log('- Reference bounds top align with element', elementBounds); } - if (lineTypes.includes(HelperLineType.LeftRight) && this.isMatch(left(elementBounds), right(bounds), 2)) { + if (lineTypes.includes(HelperLineType.LeftRight) && this.isMatch(left(elementBounds), right(bounds), this.alignmentEpsilon)) { if (isAbove(bounds, elementBounds)) { helperLines.push(new HelperLine(bottomLeft(elementBounds), topRight(bounds), HelperLineType.RightLeft)); + this.log('- Reference bounds right aligns with element left', elementBounds); } else { helperLines.push(new HelperLine(topLeft(elementBounds), bottomRight(bounds), HelperLineType.RightLeft)); + this.log('- Reference bounds right aligns with element left', elementBounds); } } - if (lineTypes.includes(HelperLineType.LeftRight) && this.isMatch(right(elementBounds), left(bounds), 2)) { + if (lineTypes.includes(HelperLineType.LeftRight) && this.isMatch(right(elementBounds), left(bounds), this.alignmentEpsilon)) { if (isAbove(bounds, elementBounds)) { helperLines.push(new HelperLine(bottomRight(elementBounds), topLeft(bounds), HelperLineType.LeftRight)); + this.log('- Reference bounds left aligns with element right', elementBounds); } else { helperLines.push(new HelperLine(topRight(elementBounds), bottomLeft(bounds), HelperLineType.LeftRight)); + this.log('- Reference bounds left aligns with element right', elementBounds); } } - if (lineTypes.includes(HelperLineType.TopBottom) && this.isMatch(top(elementBounds), bottom(bounds), 2)) { + if (lineTypes.includes(HelperLineType.TopBottom) && this.isMatch(top(elementBounds), bottom(bounds), this.alignmentEpsilon)) { if (isBefore(bounds, elementBounds)) { helperLines.push(new HelperLine(topRight(elementBounds), bottomLeft(bounds), HelperLineType.BottomTop)); + this.log('- Reference bounds bottom aligns with element top', elementBounds); } else { helperLines.push(new HelperLine(topLeft(elementBounds), bottomRight(bounds), HelperLineType.BottomTop)); + this.log('- Reference bounds bottom aligns with element top', elementBounds); } } - if (lineTypes.includes(HelperLineType.TopBottom) && this.isMatch(bottom(elementBounds), top(bounds), 2)) { + if (lineTypes.includes(HelperLineType.TopBottom) && this.isMatch(bottom(elementBounds), top(bounds), this.alignmentEpsilon)) { if (isBefore(bounds, elementBounds)) { helperLines.push(new HelperLine(bottomRight(elementBounds), topLeft(bounds), HelperLineType.TopBottom)); + this.log('- Reference bounds top aligns with element bottom', elementBounds); } else { helperLines.push(new HelperLine(bottomLeft(elementBounds), topRight(bounds), HelperLineType.TopBottom)); + this.log('- Reference bounds top aligns with element bottom', elementBounds); } } + if (helperLines.length > 0) { + this.log(`--> Add ${helperLines.length} helperlines for element:`, helperLines); + } return helperLines; } - protected isAligned( - coordinate: (elem: Bounds) => number, - leftBounds: Bounds, - rightBounds: Bounds, - epsilon = this.action.alignmentEpsilon ?? 1 - ): boolean { + protected isAligned(coordinate: (elem: Bounds) => number, leftBounds: Bounds, rightBounds: Bounds, epsilon: number): boolean { return this.isMatch(coordinate(leftBounds), coordinate(rightBounds), epsilon); } - protected isMatch(leftCoordinate: number, rightCoordinate: number, epsilon = this.action.alignmentEpsilon ?? 1): boolean { + protected isMatch(leftCoordinate: number, rightCoordinate: number, epsilon: number): boolean { return Math.abs(leftCoordinate - rightCoordinate) < epsilon; } + + protected log(message: string, ...params: any[]): void { + if (this.debug) { + this.logger.log(this, message, params); + } + } } export interface RemoveHelperLinesFeedbackAction extends Action { @@ -303,6 +333,7 @@ export class RemoveHelperLinesFeedbackCommand extends FeedbackCommand { constructor(@inject(TYPES.Action) public action: RemoveHelperLinesFeedbackAction) { super(); } + override execute(context: CommandExecutionContext): CommandReturn { removeHelperLines(context.root); removeSelectionBounds(context.root); @@ -317,3 +348,11 @@ export function removeHelperLines(root: GModelRoot): void { export function removeSelectionBounds(root: GModelRoot): void { forEachElement(root.index, isSelectionBounds, line => root.remove(line)); } + +export function boundsInViewport(element: GModelElement, bounds: Bounds | Point): Bounds | Point { + if (element instanceof GChildElement && !isViewport(element.parent)) { + return boundsInViewport(element.parent, element.parent.localToParent(bounds) as Bounds); + } else { + return bounds; + } +} diff --git a/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-manager-default.ts b/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-manager-default.ts index 93f4ca7d..e6256b23 100644 --- a/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-manager-default.ts +++ b/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-manager-default.ts @@ -13,57 +13,100 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { - GridSnapper, - IFeedbackActionDispatcher, - ISelectionListener, - MoveFinishedEventAction, MoveInitializedEventAction, - SelectionService, - SetBoundsFeedbackAction -} from '@eclipse-glsp/client'; import { Action, DisposableCollection, GModelElement, GModelRoot, IActionHandler, - ISnapper, + IFeedbackActionDispatcher, + ISelectionListener, MoveAction, + MoveFinishedEventAction, MoveInitializedEventAction, + Point, + SelectionService, SetBoundsAction, + SetBoundsFeedbackAction, TYPES -} from '@eclipse-glsp/sprotty'; +} from '@eclipse-glsp/client'; + + import { inject, injectable, optional, postConstruct } from 'inversify'; -// import { IFeedbackActionDispatcher } from '../../base/feedback/feedback-action-dispatcher'; -// import { ISelectionListener, SelectionService } from '../../base/selection-service'; -// import { SetBoundsFeedbackAction } from '../bounds/set-bounds-feedback-command'; -// import { GridSnapper } from '../change-bounds/snap'; -// import { MoveFinishedEventAction, MoveInitializedEventAction } from '../tools/change-bounds/change-bounds-tool-feedback'; -import { DrawHelperLinesFeedbackAction, RemoveHelperLinesFeedbackAction, ViewportLineType } from './bpmn-helper-line-feedback'; + +import { + AlignmentElementFilter, + DEFAULT_ALIGNABLE_ELEMENT_FILTER, + DEFAULT_DEBUG, + DEFAULT_ELEMENT_LINES, + DEFAULT_EPSILON, + DEFAULT_VIEWPORT_LINES, + DrawHelperLinesFeedbackAction, + RemoveHelperLinesFeedbackAction, + ViewportLineType +} from './bpmn-helper-line-feedback'; import { IHelperLineManager } from './bpmn-helper-line-manager'; import { Direction, HelperLineType } from './model'; +export interface IHelperLineOptions { + /** + * A list of helper line types that should be rendered when elements are aligned. + * Defaults to all possible alignments. + */ + elementLines?: HelperLineType[]; + /** + * A list of helper line types that should be rendered when an element is aligned with the viewport. + * Defaults to middle and center alignment. + */ + viewportLines?: ViewportLineType[]; + /** + * The minimum difference between two coordinates + * Defaults to 1. + */ + alignmentEpsilon?: number; + /** + * A filter that is applied to determine on which elements the alignment calculation is performed. + * By default all top-level bounds-aware, non-routable elements that are visible on the canvas are considered. + */ + alignmentElementFilter?: AlignmentElementFilter; + /** + * The minimum move delta that is necessary for an element to break through a helper line. + * Defaults to { x: 1, y: 1 } whereas the x represents the horizontal distance and y represents the vertical distance. + */ + minimumMoveDelta?: Point; + + /** + * Produces debug output. + * Defaults to false. + */ + debug?: boolean; +} + +export const DEFAULT_MOVE_DELTA = { x: 1, y: 1 }; + +export const DEFAULT_HELPER_LINE_OPTIONS: Required = { + elementLines: DEFAULT_ELEMENT_LINES, + viewportLines: DEFAULT_VIEWPORT_LINES, + alignmentEpsilon: DEFAULT_EPSILON, + alignmentElementFilter: DEFAULT_ALIGNABLE_ELEMENT_FILTER, + minimumMoveDelta: DEFAULT_MOVE_DELTA, + debug: DEFAULT_DEBUG +}; + @injectable() export class HelperLineManager implements IActionHandler, ISelectionListener, IHelperLineManager { @inject(TYPES.IFeedbackActionDispatcher) protected feedbackDispatcher: IFeedbackActionDispatcher; @inject(SelectionService) protected selectionService: SelectionService; - @optional() @inject(TYPES.ISnapper) protected snapper?: ISnapper; + @optional() @inject(TYPES.IHelperLineOptions) protected userOptions?: IHelperLineOptions; - protected snapSize = { x: 10, y: 10 }; + protected options: Required; protected feedback: DisposableCollection = new DisposableCollection(); - protected elementLines?: HelperLineType[]; - protected viewportLines?: ViewportLineType[]; - protected alignmentEpsilon?: number; - @postConstruct() protected init(): void { + this.options = { ...DEFAULT_HELPER_LINE_OPTIONS, ...this.userOptions }; this.selectionService.onSelectionChanged(change => this.selectionChanged(change.root, change.selectedElements, change.deselectedElements) ); - if (this.snapper instanceof GridSnapper) { - console.log('--------__> NOT helloe ich setzte irgendwas mit snap'); - //this.snapSize = { x: this.snapper.grid.x * 2, y: this.snapper.grid.y * 2 }; - } } handle(action: Action): void { @@ -99,16 +142,10 @@ export class HelperLineManager implements IActionHandler, ISelectionListener, IH } protected createHelperLineFeedback(elementIds: string[]): DrawHelperLinesFeedbackAction { - return DrawHelperLinesFeedbackAction.create({ - elementIds, - elementLines: this.elementLines, - viewportLines: this.viewportLines, - alignmentEpsilon: this.alignmentEpsilon - }); + return DrawHelperLinesFeedbackAction.create({ elementIds, ...this.options }); } protected handleSetBoundsAction(action: SetBoundsAction | SetBoundsFeedbackAction): void { - console.log('set bounds action'); const elementIds = action.bounds.map(bound => bound.elementId); const feedback = this.createHelperLineFeedback(elementIds); this.feedback.push(this.feedbackDispatcher.registerFeedback(this, [feedback], [RemoveHelperLinesFeedbackAction.create()])); @@ -118,7 +155,12 @@ export class HelperLineManager implements IActionHandler, ISelectionListener, IH this.feedback.dispose(); } - getHelperLineSnapping(target: GModelElement, isSnap: boolean, direction: Direction): number { - return direction === Direction.Left || direction === Direction.Right ? this.snapSize.x : this.snapSize.y; + getMinimumMoveDelta(element: GModelElement, isSnap: boolean, direction: Direction): number { + if (!isSnap) { + return 0; + } + return direction === Direction.Left || direction === Direction.Right + ? this.options.minimumMoveDelta.x + : this.options.minimumMoveDelta.y; } } diff --git a/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-manager.ts b/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-manager.ts index be260925..f74266b2 100644 --- a/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-manager.ts +++ b/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-manager.ts @@ -17,5 +17,12 @@ import { GModelElement } from '@eclipse-glsp/sprotty'; import { Direction } from './model'; export interface IHelperLineManager { - getHelperLineSnapping(target: GModelElement, isSnap: boolean, direction: Direction): number; + /** + * Calculates the minimum move delta that is necessary to break through a helper line. + * + * @param element element that is being moved + * @param isSnap whether snapping is active or not + * @param direction direction in which the target element is moving + */ + getMinimumMoveDelta(element: GModelElement, isSnap: boolean, direction: Direction): number; } diff --git a/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-module.ts b/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-module.ts index c92d899d..0f14c5bb 100644 --- a/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-module.ts +++ b/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/bpmn-helper-line-module.ts @@ -13,24 +13,20 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ - -import { - MoveFinishedEventAction, MoveInitializedEventAction, - SetBoundsFeedbackAction -} from '@eclipse-glsp/client'; import { FeatureModule, MoveAction, + MoveFinishedEventAction, MoveInitializedEventAction, SetBoundsAction, + SetBoundsFeedbackAction, TYPES, bindAsService, configureActionHandler, configureCommand, configureModelElement -} from '@eclipse-glsp/sprotty'; +} from '@eclipse-glsp/client'; + -// import { SetBoundsFeedbackAction } from '../bounds/set-bounds-feedback-command'; -// import { MoveFinishedEventAction, MoveInitializedEventAction } from '../tools/change-bounds/change-bounds-tool-feedback'; import { DrawHelperLinesFeedbackCommand, RemoveHelperLinesFeedbackCommand } from './bpmn-helper-line-feedback'; import { HelperLineManager } from './bpmn-helper-line-manager-default'; import { HELPER_LINE, HelperLine, SELECTION_BOUNDS, SelectionBounds } from './model'; diff --git a/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/model.ts b/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/model.ts index 6162860a..d5f7891b 100644 --- a/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/model.ts +++ b/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helper-lines/model.ts @@ -14,15 +14,16 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Args, Bounds, GChildElement, GModelElement, GShapeElement, Point } from '@eclipse-glsp/sprotty'; -import { v4 as uuid } from 'uuid'; -// import { ArgsAware } from '../../base/args-feature'; -// import { ResizeHandleLocation } from '../change-bounds/model'; import { + Args, ArgsAware, + Bounds, GChildElement, GModelElement, GShapeElement, Point, ResizeHandleLocation } from '@eclipse-glsp/client'; + + +import { v4 as uuid } from 'uuid'; export const HelperLineType = { Left: 'left', Right: 'right', diff --git a/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helperlines.tsx b/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helperlines.tsx index 739fe01d..92467174 100644 --- a/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helperlines.tsx +++ b/open-bpmn.glsp-client/open-bpmn-glsp/src/bpmn-helperlines.tsx @@ -28,11 +28,11 @@ import { } from '@eclipse-glsp/client'; import { EventNode, - LabelNode, TaskNode, - isBPMNLabelNode, isBPMNNode, isBoundaryEvent, + isEventNode, + isGatewayNode, isLaneDivider, isTaskNode } from '@open-bpmn/open-bpmn-model'; @@ -117,6 +117,40 @@ export class BPMNElementSnapper implements ISnapper { if (isBoundaryEvent(element)) { snapPoint = this.findBoundarySnapPoint(element, position); } else { + + + if (isTaskNode(element)) { + + snapPoint = { + x: Math.round(position.x / 10) * 10, + y: Math.round(position.y / 10) * 10 + }; + console.log('.. snap task: position= ' + position.x + ',' + position.y + ' snap= ' + snapPoint.x + ',' + snapPoint.y); + return snapPoint; + } + if (isGatewayNode(element)) { + // center snap... + return { + x: Math.round((position.x + 0.5 * element.bounds.width) / 5) * 5 - 0.5 * element.bounds.width, + y: Math.round((position.y + 0.5 * element.bounds.height) / 5) * 5 - 0.5 * element.bounds.height + }; + + } + + if (isEventNode(element)) { + // center snap... + snapPoint = { + x: Math.round((position.x + 0.5 * element.bounds.width) / 5) * 5 - 0.5 * element.bounds.width, + y: Math.round((position.y + 0.5 * element.bounds.height) / 5) * 5 - 0.5 * element.bounds.height + }; + console.log('.. snap event: position= ' + position.x + ',' + position.y + ' snap= ' + snapPoint.x + ',' + snapPoint.y); + return snapPoint; + + } + + + + // find default snap position snapPoint = this.findSnapPoint(element); // if a snapPoint was found and this snapPoint is still in the snapRange, @@ -133,17 +167,17 @@ export class BPMNElementSnapper implements ISnapper { } // fix BPMNLabel offset (only needed or Elements with a separate label)? - if (isBPMNLabelNode(element)) { - const xOffset = snapPoint.x - position.x; - const yOffset = snapPoint.y - position.y; - const label: any = element.root.index.getById(element.id + '_bpmnlabel'); - if (label instanceof LabelNode) { - // fix offset of the lable position.... - const ly = label.position.y + yOffset; - const lx = label.position.x + xOffset; - label.position = { x: lx, y: ly }; - } - } + // if (isBPMNLabelNode(element)) { + // const xOffset = snapPoint.x - position.x; + // const yOffset = snapPoint.y - position.y; + // const label: any = element.root.index.getById(element.id + '_bpmnlabel'); + // if (label instanceof LabelNode) { + // // fix offset of the lable position.... + // const ly = label.position.y + yOffset; + // const lx = label.position.x + xOffset; + // label.position = { x: lx, y: ly }; + // } + // } return snapPoint; } diff --git a/open-bpmn.glsp-client/open-bpmn-glsp/src/di.config.ts b/open-bpmn.glsp-client/open-bpmn-glsp/src/di.config.ts index 69ea3c69..fc28c77b 100644 --- a/open-bpmn.glsp-client/open-bpmn-glsp/src/di.config.ts +++ b/open-bpmn.glsp-client/open-bpmn-glsp/src/di.config.ts @@ -23,16 +23,13 @@ import { GCompartmentView, GLabel, GLabelView, - GridSnapper, LogLevel, RectangularNodeView, RoundedCornerNodeView, TYPES, - alignmentElementFilter, configureDefaultModelElements, configureModelElement, editLabelFeature, - helperLineModule, initializeDiagramContainer, moveFeature, selectFeature @@ -65,12 +62,22 @@ import { TaskNodeView, TextAnnotationNodeView } from './bpmn-element-views'; +import { IHelperLineOptions } from './bpmn-helper-lines/bpmn-helper-line-manager-default'; +import { + BPMNElementSnapper +} from './bpmn-helperlines'; import { BPMNEdgeView } from './bpmn-routing-views'; +import { + isBPMNNode +} from '@open-bpmn/open-bpmn-model'; import { BPMNPropertiesMouseListener, BPMNPropertyModule } from '@open-bpmn/open-bpmn-properties'; +import { + bpmnHelperLineModule +} from './bpmn-helper-lines/bpmn-helper-line-module'; import { BPMNLabelNodeSelectionListener, BPMNMultiNodeSelectionListener @@ -81,15 +88,21 @@ const bpmnDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) => rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); rebind(TYPES.LogLevel).toConstantValue(LogLevel.warn); - //bind(TYPES.ISnapper).to(BPMNElementSnapper); + //bind(TYPES.ISnapper).to(GridSnapper); - bind(TYPES.ISnapper).toConstantValue(new GridSnapper({ x: 1, y: 1 })); - //bind(TYPES.ISnapper).toConstantValue(new BPMNElementSnapper({ x: 10, y: 10 })); + //bind(TYPES.ISnapper).toConstantValue(new GridSnapper({ x: 10, y: 10 })); + bind(TYPES.ISnapper).toConstantValue(new BPMNElementSnapper()); + bind(TYPES.IHelperLineOptions).toConstantValue({ + alignmentElementFilter: element => + isBPMNNode(element), + minimumMoveDelta: { x: 10, y: 10 } + }); + + - //bind(TYPES.ISnapper).toConstantValue(new CenterGridSnapper()); // We do not whant a reveal action in BPMN // ???? bind(TYPES.ICommandPaletteActionProvider).to(RevealNamedElementActionProvider); @@ -181,16 +194,9 @@ export function initializeBPMNDiagramContainer(container: Container, ...containerConfiguration: ContainerConfiguration): Container { - const filteredElements = alignmentElementFilter.filter(element => - element.type !== 'BPMNLabel' && - element.type !== 'sequenceFlow' && - element.type !== 'messageFlow' && - element.type !== 'association' && - element.type !== 'lane-divider' - ); // return initializeDiagramContainer(container, bpmnDiagramModule, ...containerConfiguration); - return initializeDiagramContainer(container, bpmnDiagramModule, helperLineModule, BPMNPropertyModule, ...containerConfiguration); + return initializeDiagramContainer(container, bpmnDiagramModule, bpmnHelperLineModule, BPMNPropertyModule, ...containerConfiguration); // return initializeDiagramContainer(container, bpmnDiagramModule, bpmnHelperLineModule, BPMNPropertyModule, ...containerConfiguration);