diff --git a/packages/client/css/ghost-element.css b/packages/client/css/ghost-element.css index 2ced7565..a9acb53a 100644 --- a/packages/client/css/ghost-element.css +++ b/packages/client/css/ghost-element.css @@ -21,5 +21,7 @@ } .ghost-element.hidden { - display: none; + width: 0; + height: 0; + visibility: hidden; } diff --git a/packages/client/css/glsp-sprotty.css b/packages/client/css/glsp-sprotty.css index 8d17f5cb..f5c38627 100644 --- a/packages/client/css/glsp-sprotty.css +++ b/packages/client/css/glsp-sprotty.css @@ -73,11 +73,6 @@ fill: #1d80d1; } -.sprotty-hidden .sprotty-resize-handle { - /** resize handles should not be considered as part of the elements bounds */ - display: none; -} - .sprotty-edge { fill: none; stroke-width: 1.5px; diff --git a/packages/client/src/features/bounds/glsp-hidden-bounds-updater.ts b/packages/client/src/features/bounds/glsp-hidden-bounds-updater.ts index 115776e9..963ae164 100644 --- a/packages/client/src/features/bounds/glsp-hidden-bounds-updater.ts +++ b/packages/client/src/features/bounds/glsp-hidden-bounds-updater.ts @@ -16,20 +16,21 @@ import { Action, + BoundsData, ComputedBoundsAction, Deferred, EdgeRouterRegistry, ElementAndRoutingPoints, GModelElement, - GRoutableElement, HiddenBoundsUpdater, IActionDispatcher, + ModelIndexImpl, RequestAction, ResponseAction } from '@eclipse-glsp/sprotty'; import { inject, injectable, optional } from 'inversify'; import { VNode } from 'snabbdom'; -import { calcElementAndRoute, isRoutable } from '../../utils/gmodel-util'; +import { BoundsAwareModelElement, calcElementAndRoute, getDescendantIds, isRoutable } from '../../utils/gmodel-util'; import { LocalComputedBoundsAction, LocalRequestBoundsAction } from './local-bounds'; /** @@ -42,8 +43,10 @@ export class GLSPHiddenBoundsUpdater extends HiddenBoundsUpdater { @inject(EdgeRouterRegistry) @optional() protected readonly edgeRouterRegistry?: EdgeRouterRegistry; protected element2route: ElementAndRoutingPoints[] = []; - protected edges: GRoutableElement[] = []; - protected nodes: VNode[] = []; + + protected getElement2BoundsData(): Map { + return this['element2boundsData']; + } override decorate(vnode: VNode, element: GModelElement): VNode { super.decorate(vnode, element); @@ -54,6 +57,9 @@ export class GLSPHiddenBoundsUpdater extends HiddenBoundsUpdater { } override postUpdate(cause?: Action): void { + if (LocalRequestBoundsAction.is(cause) && cause.elementIDs) { + this.focusOnElements(cause.elementIDs); + } const actions = this.captureActions(() => super.postUpdate(cause)); actions .filter(action => ComputedBoundsAction.is(action)) @@ -61,6 +67,22 @@ export class GLSPHiddenBoundsUpdater extends HiddenBoundsUpdater { this.element2route = []; } + protected focusOnElements(elementIDs: string[]): void { + const data = this.getElement2BoundsData(); + if (data.size > 0) { + // expand given IDs to their descendent element IDs as we need their bounding boxes as well + const index = [...data.keys()][0].index; + const relevantIds = new Set(elementIDs.flatMap(elementId => this.expandElementId(elementId, index, elementIDs))); + + // ensure we only keep the bounds of the elements we are interested in + data.forEach((_bounds, element) => !relevantIds.has(element.id) && data.delete(element)); + } + } + + protected expandElementId(id: string, index: ModelIndexImpl, elementIDs: string[]): string[] { + return getDescendantIds(index.getById(id)); + } + protected captureActions(call: () => void): Action[] { const capturingActionDispatcher = new CapturingActionDispatcher(); const actualActionDispatcher = this.actionDispatcher; diff --git a/packages/client/src/features/bounds/local-bounds.ts b/packages/client/src/features/bounds/local-bounds.ts index 699e343b..f02c9aee 100644 --- a/packages/client/src/features/bounds/local-bounds.ts +++ b/packages/client/src/features/bounds/local-bounds.ts @@ -26,21 +26,38 @@ import { GModelRootSchema, RequestBoundsAction, TYPES, - ViewerOptions + ViewerOptions, + hasArrayProp } from '@eclipse-glsp/sprotty'; import { inject, injectable } from 'inversify'; import { ServerAction } from '../../base/model/glsp-model-source'; +export interface LocalRequestBoundsAction extends RequestBoundsAction { + elementIDs?: string[]; +} + export namespace LocalRequestBoundsAction { - export function is(object: unknown): object is RequestBoundsAction { - return RequestBoundsAction.is(object) && !ServerAction.is(object); + export function is(object: unknown): object is LocalRequestBoundsAction { + return RequestBoundsAction.is(object) && !ServerAction.is(object) && hasArrayProp(object, 'elementIDs', true); + } + + export function create(newRoot: GModelRootSchema, elementIDs?: string[]): LocalRequestBoundsAction { + return { + ...RequestBoundsAction.create(newRoot), + elementIDs + }; } - export function fromCommand(context: CommandExecutionContext, actionDispatcher: ActionDispatcher, cause?: Action): CommandResult { + export function fromCommand( + { root }: CommandExecutionContext, + actionDispatcher: ActionDispatcher, + cause?: Action, + elementIDs?: string[] + ): CommandResult { // do not modify the main model (modelChanged = false) but request local bounds calculation on hidden model - actionDispatcher.dispatch(RequestBoundsAction.create(context.root as unknown as GModelRootSchema)); + actionDispatcher.dispatch(LocalRequestBoundsAction.create(root as unknown as GModelRootSchema, elementIDs)); return { - model: context.root, + model: root, modelChanged: false, cause }; diff --git a/packages/client/src/features/bounds/set-bounds-feedback-command.ts b/packages/client/src/features/bounds/set-bounds-feedback-command.ts index 5f7f2b02..62b20e4b 100644 --- a/packages/client/src/features/bounds/set-bounds-feedback-command.ts +++ b/packages/client/src/features/bounds/set-bounds-feedback-command.ts @@ -65,6 +65,7 @@ export class SetBoundsFeedbackCommand extends SetBoundsCommand implements Feedba element.layoutOptions = options; } }); - return LocalRequestBoundsAction.fromCommand(context, this.actionDispatcher, this.action); + const elementIDs = this.action.bounds.map(bounds => bounds.elementId); + return LocalRequestBoundsAction.fromCommand(context, this.actionDispatcher, this.action, elementIDs); } } diff --git a/packages/client/src/features/element-template/add-template-element.ts b/packages/client/src/features/element-template/add-template-element.ts index 7bc81bac..e8972698 100644 --- a/packages/client/src/features/element-template/add-template-element.ts +++ b/packages/client/src/features/element-template/add-template-element.ts @@ -64,13 +64,14 @@ export class AddTemplateElementsFeedbackCommand extends FeedbackCommand { } override execute(context: CommandExecutionContext): CommandResult { - this.action.templates + const templateElements = this.action.templates .map(template => templateToSchema(template, context)) .filter(isNotUndefined) .map(schema => context.modelFactory.createElement(schema)) - .map(element => this.applyRootCssClasses(element, this.action.addClasses, this.action.removeClasses)) - .forEach(templateElement => context.root.add(templateElement)); - return LocalRequestBoundsAction.fromCommand(context, this.actionDispatcher, this.action); + .map(element => this.applyRootCssClasses(element, this.action.addClasses, this.action.removeClasses)); + templateElements.forEach(templateElement => context.root.add(templateElement)); + const templateElementIDs = templateElements.map(element => element.id); + return LocalRequestBoundsAction.fromCommand(context, this.actionDispatcher, this.action, templateElementIDs); } protected applyRootCssClasses(element: GChildElement, addClasses?: string[], removeClasses?: string[]): GChildElement { diff --git a/packages/client/src/utils/gmodel-util.ts b/packages/client/src/utils/gmodel-util.ts index 78424270..8e7c25e8 100644 --- a/packages/client/src/utils/gmodel-util.ts +++ b/packages/client/src/utils/gmodel-util.ts @@ -22,6 +22,7 @@ import { GChildElement, GModelElement, GModelElementSchema, + GParentElement, GRoutableElement, GRoutingHandle, ModelIndexImpl, @@ -30,13 +31,12 @@ import { Selectable, TypeGuard, distinctAdd, - findParentByFeature, getAbsoluteBounds, + getZoom, isBoundsAware, isMoveable, isSelectable, isSelected, - isViewport, remove } from '@eclipse-glsp/sprotty'; @@ -346,8 +346,7 @@ export function findTopLevelElementByFeature( export function calculateDeltaBetweenPoints(target: Point, source: Point, element: GModelElement): Point { const delta = Point.subtract(target, source); - const viewport = findParentByFeature(element, isViewport); - const zoom = viewport?.zoom ?? 1; + const zoom = getZoom(element); const adaptedDelta = { x: delta.x / zoom, y: delta.y / zoom }; return adaptedDelta; } @@ -362,3 +361,15 @@ export function isVisibleOnCanvas(model: BoundsAwareModelElement): boolean { modelBounds.y + modelBounds.height >= 0 ); } + +export function getDescendantIds(element?: GModelElement, skip?: (t: GModelElement) => boolean): string[] { + if (!element || skip?.(element)) { + return []; + } + const parent = element; + const ids = [parent.id]; + if (parent instanceof GParentElement) { + ids.push(...parent.children.flatMap(child => getDescendantIds(child, skip))); + } + return ids; +} diff --git a/packages/glsp-sprotty/src/re-exports.ts b/packages/glsp-sprotty/src/re-exports.ts index af6027f5..caf2221d 100644 --- a/packages/glsp-sprotty/src/re-exports.ts +++ b/packages/glsp-sprotty/src/re-exports.ts @@ -173,6 +173,7 @@ export { SIssueSeverity as GIssueSeverity, // Export as is, we extend it glsp-client to `GIssueMarker` SIssueMarker, + decorationFeature, isDecoration } from 'sprotty/lib/features/decoration/model'; export * from 'sprotty/lib/features/decoration/views';