Skip to content

Commit

Permalink
Ensure hidden bounds calculation works in all browser
Browse files Browse the repository at this point in the history
- Do not use 'display: none' for hidden getBBox() calculation

- Only re-calculate the bounds for certain elements
-- Extend LocalRequestBoundsAction with element IDs
-- Restrict calculation to given set of elements if IDs are present

- Minor: Fix re-export for decorationModule

Fixes eclipse-glsp/glsp#1258
  • Loading branch information
martin-fleck-at authored and tortmayr committed Mar 10, 2024
1 parent c4764a5 commit 645e72f
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 25 deletions.
4 changes: 3 additions & 1 deletion packages/client/css/ghost-element.css
Expand Up @@ -21,5 +21,7 @@
}

.ghost-element.hidden {
display: none;
width: 0;
height: 0;
visibility: hidden;
}
5 changes: 0 additions & 5 deletions packages/client/css/glsp-sprotty.css
Expand Up @@ -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;
Expand Down
30 changes: 26 additions & 4 deletions packages/client/src/features/bounds/glsp-hidden-bounds-updater.ts
Expand Up @@ -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';

/**
Expand All @@ -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<BoundsAwareModelElement, BoundsData> {
return this['element2boundsData'];
}

override decorate(vnode: VNode, element: GModelElement): VNode {
super.decorate(vnode, element);
Expand All @@ -54,13 +57,32 @@ 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))
.forEach(action => this.actionDispatcher.dispatch(this.enhanceAction(action as ComputedBoundsAction, cause)));
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;
Expand Down
29 changes: 23 additions & 6 deletions packages/client/src/features/bounds/local-bounds.ts
Expand Up @@ -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
};
Expand Down
Expand Up @@ -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);
}
}
Expand Up @@ -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 {
Expand Down
19 changes: 15 additions & 4 deletions packages/client/src/utils/gmodel-util.ts
Expand Up @@ -22,6 +22,7 @@ import {
GChildElement,
GModelElement,
GModelElementSchema,
GParentElement,
GRoutableElement,
GRoutingHandle,
ModelIndexImpl,
Expand All @@ -30,13 +31,12 @@ import {
Selectable,
TypeGuard,
distinctAdd,
findParentByFeature,
getAbsoluteBounds,
getZoom,
isBoundsAware,
isMoveable,
isSelectable,
isSelected,
isViewport,
remove
} from '@eclipse-glsp/sprotty';

Expand Down Expand Up @@ -346,8 +346,7 @@ export function findTopLevelElementByFeature<T>(

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;
}
Expand All @@ -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;
}
1 change: 1 addition & 0 deletions packages/glsp-sprotty/src/re-exports.ts
Expand Up @@ -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';
Expand Down

0 comments on commit 645e72f

Please sign in to comment.