Skip to content

Commit

Permalink
Refactored projection feature so it's derived from the model instead …
Browse files Browse the repository at this point in the history
…of the view
  • Loading branch information
spoenemann committed Sep 14, 2021
1 parent b07d5dc commit 46b2d11
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 130 deletions.
14 changes: 13 additions & 1 deletion examples/svg/css/diagram.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,16 @@

.foreign-object {
user-select: none;
}
}

.logo-projection {
background-color: rgba(255, 153, 0, 0.5);
}

.tiger-projection {
background-color: rgba(204, 114, 38, 0.5);
}

.node-projection {
background-color: rgba(255, 140, 0, 0.2);
}
13 changes: 4 additions & 9 deletions examples/svg/src/di.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
ProjectedViewportView, ViewportRootElement, ShapedPreRenderedElement, configureModelElement,
ForeignObjectElement, ForeignObjectView, RectangularNode, RectangularNodeView, moveFeature,
selectFeature, EditableLabel, editLabelFeature, WithEditableLabel, withEditLabelFeature,
isEditableLabel, setProjection, Action, MoveAction, SShapeElementSchema, ViewportRootElementSchema, ActionHandlerRegistry, SetBoundsAction
isEditableLabel, Action, MoveAction, SShapeElementSchema, ViewportRootElementSchema,
ActionHandlerRegistry, SetBoundsAction
} from '../../../src';

export default () => {
Expand All @@ -34,16 +35,10 @@ export default () => {
bind(TYPES.ModelSource).to(SVGModelSource).inSingletonScope();
const context = { bind, unbind, isBound, rebind };
configureModelElement(context, 'svg', ViewportRootElement, ProjectedViewportView);
configureModelElement(context, 'pre-logo', ShapedPreRenderedElement, PreRenderedView, {
viewInit: setProjection('rgba(255, 153, 0, 0.5)')
});
configureModelElement(context, 'pre-tiger', ShapedPreRenderedElement, PreRenderedView, {
viewInit: setProjection('rgba(204, 114, 38, 0.5)')
});
configureModelElement(context, 'pre-rendered', ShapedPreRenderedElement, PreRenderedView);
configureModelElement(context, 'foreign-object', ForeignObjectElement, ForeignObjectView);
configureModelElement(context, 'node', RectangleWithEditableLabel, RectangularNodeView, {
enable: [withEditLabelFeature],
viewInit: setProjection('rgba(255, 140, 0, 0.2)')
enable: [withEditLabelFeature]
});
configureModelElement(context, 'child-foreign-object', EditableForeignObjectElement, ForeignObjectView, {
disable: [moveFeature, selectFeature], // disable move/select as we want the parent node to react to select/move
Expand Down
21 changes: 12 additions & 9 deletions examples/svg/src/standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
********************************************************************************/

import {
TYPES, ShapedPreRenderedElementSchema, ForeignObjectElementSchema, SShapeElementSchema, ViewportRootElementSchema
TYPES, ShapedPreRenderedElementSchema, ForeignObjectElementSchema, SShapeElementSchema, ViewportRootElementSchema, Projectable
} from '../../../src';
import createContainer, { SVGModelSource } from './di.config';

Expand Down Expand Up @@ -44,17 +44,19 @@ export default async function runSVG() {
id: 'root',
children: [
{
type: 'pre-logo',
type: 'pre-rendered',
id: 'logo',
position: { x: 200, y: 200 },
code: svgLogo
} as ShapedPreRenderedElementSchema,
code: svgLogo,
projectionCssClasses: ['logo-projection']
} as ShapedPreRenderedElementSchema & Projectable,
{
type: 'pre-tiger',
type: 'pre-rendered',
id: 'tiger',
position: { x: 400, y: 50 },
code: tiger
} as ShapedPreRenderedElementSchema,
code: tiger,
projectionCssClasses: ['tiger-projection']
} as ShapedPreRenderedElementSchema & Projectable,
{
type: 'foreign-object',
id: 'direct-html',
Expand All @@ -79,8 +81,9 @@ export default async function runSVG() {
id: 'foreign-object-in-shape-contents',
code: '<div>This is <em>HTML</em> within <u>an SVG rectangle</u>!</div>'
} as ForeignObjectElementSchema
]
} as SShapeElementSchema
],
projectionCssClasses: ['node-projection']
} as SShapeElementSchema & Projectable
]
};

Expand Down
48 changes: 12 additions & 36 deletions src/base/views/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { injectable, multiInject, optional, interfaces } from 'inversify';
import { VNode } from 'snabbdom';
import { TYPES } from '../types';
import { InstanceRegistry } from '../../utils/registry';
import { Point, ORIGIN_POINT, Bounds } from '../../utils/geometry';
import { Point, ORIGIN_POINT } from '../../utils/geometry';
import { isInjectable } from '../../utils/inversify';
import { SModelElement, SModelRoot, SParentElement } from '../model/smodel';
import { EMPTY_ROOT, CustomFeatures } from '../model/smodel-factory';
Expand Down Expand Up @@ -54,11 +54,7 @@ export function findArgValue<T>(arg: IViewArgs | undefined, key: string): T | un
* Base interface for the components that turn GModelElements into virtual DOM elements.
*/
export interface IView<A extends IViewArgs = {}> {

render(model: Readonly<SModelElement>, context: RenderingContext, args?: A): VNode | undefined

getProjection?(model: Readonly<SModelElement>, context: RenderingContext, args?: A): ViewProjection | undefined

}

/**
Expand All @@ -68,14 +64,6 @@ export interface IView<A extends IViewArgs = {}> {
*/
export type RenderingTargetKind = 'main' | 'popup' | 'hidden';

/**
* A projection can be shown in a horizontal or vertical bar to display an overview of the diagram.
*/
export interface ViewProjection {
projectedBounds: Bounds;
color: string;
}

/**
* Bundles additional data that is passed to views for VNode creation.
*/
Expand All @@ -89,8 +77,6 @@ export interface RenderingContext {
renderElement(element: Readonly<SModelElement>): VNode | undefined

renderChildren(element: Readonly<SParentElement>, args?: IViewArgs): VNode[]

getProjections(element: Readonly<SParentElement>): ViewProjection[] | undefined
}

/**
Expand Down Expand Up @@ -128,20 +114,18 @@ export class ViewRegistry extends InstanceRegistry<IView> {
/**
* Combines `registerModelElement` and `configureView`.
*/
export function configureModelElement<E extends SModelElement, V extends IView>(context: { bind: interfaces.Bind, isBound: interfaces.IsBound },
type: string, modelConstr: new () => E, viewConstr: interfaces.ServiceIdentifier<V>,
options?: ModelElementOptions<E, V>): void {
registerModelElement(context, type, modelConstr, options);
configureView(context, type, viewConstr, options);
export function configureModelElement(context: { bind: interfaces.Bind, isBound: interfaces.IsBound },
type: string, modelConstr: new () => SModelElement, viewConstr: interfaces.ServiceIdentifier<IView>,
features?: CustomFeatures): void {
registerModelElement(context, type, modelConstr, features);
configureView(context, type, viewConstr);
}

export type ModelElementOptions<E extends SModelElement, V extends IView> = CustomFeatures & ViewOptions<V>;

/**
* Utility function to register a view for a model element type.
*/
export function configureView<V extends IView>(context: { bind: interfaces.Bind, isBound: interfaces.IsBound },
type: string, constr: interfaces.ServiceIdentifier<V>, options?: ViewOptions<V>): void {
export function configureView(context: { bind: interfaces.Bind, isBound: interfaces.IsBound },
type: string, constr: interfaces.ServiceIdentifier<IView>): void {
if (typeof constr === 'function') {
if (!isInjectable(constr)) {
throw new Error(`Views should be @injectable: ${constr.name}`);
Expand All @@ -150,18 +134,10 @@ export function configureView<V extends IView>(context: { bind: interfaces.Bind,
context.bind(constr).toSelf();
}
}
context.bind(TYPES.ViewRegistration).toDynamicValue(ctx => {
const factory = options && options.viewInit ? () => {
const view = ctx.container.get(constr);
options.viewInit!(view);
return view;
} : () => ctx.container.get(constr);
return { type, factory };
});
}

export interface ViewOptions<V> {
viewInit?: (view: V) => void
context.bind(TYPES.ViewRegistration).toDynamicValue(ctx => ({
type,
factory: () => ctx.container.get(constr)
}));
}

/**
Expand Down
30 changes: 1 addition & 29 deletions src/base/views/viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { SModelElement, SModelRoot, SParentElement } from '../model/smodel';
import { EMPTY_ROOT } from '../model/smodel-factory';
import { TYPES } from '../types';
import { isThunk } from './thunk-view';
import { IViewArgs, RenderingContext, RenderingTargetKind, ViewProjection, ViewRegistry } from './view';
import { IViewArgs, RenderingContext, RenderingTargetKind, ViewRegistry } from './view';
import { ViewerOptions } from './viewer-options';
import { IVNodePostprocessor } from './vnode-postprocessor';
import { copyClassesFromElement, copyClassesFromVNode, setAttr, setClass } from './vnode-utils';
Expand Down Expand Up @@ -83,34 +83,6 @@ export class ModelRenderer implements RenderingContext {
.filter(vnode => vnode !== undefined) as VNode[];
}

getProjections(element: Readonly<SParentElement>): ViewProjection[] | undefined {
let result: ViewProjection[] | undefined;
for (const child of element.children) {
const view = this.viewRegistry.get(child.type);
if (view.getProjection) {
const p = view.getProjection(child, this);
if (p) {
if (result) {
result.push(p);
} else {
result = [p];
}
}
}
if (child.children.length > 0) {
const childProj = this.getProjections(child);
if (childProj) {
if (result) {
result.push(...childProj);
} else {
result = childProj;
}
}
}
}
return result;
}

postUpdate(cause?: Action) {
this.postprocessors.forEach(processor => processor.postUpdate(cause));
}
Expand Down
31 changes: 3 additions & 28 deletions src/features/bounds/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,14 @@

import { injectable } from 'inversify';
import { VNode } from 'snabbdom';
import { Bounds, isValidDimension } from '../../utils/geometry';
import { IViewArgs, IView, RenderingContext, ViewProjection } from '../../base/views/view';
import { getAbsoluteBounds, BoundsAware, isBoundsAware } from './model';
import { isValidDimension } from '../../utils/geometry';
import { IViewArgs, IView, RenderingContext } from '../../base/views/view';
import { getAbsoluteBounds, BoundsAware } from './model';
import { SChildElement } from '../../base/model/smodel';

@injectable()
export abstract class ShapeView implements IView {

/**
* Set this property to enable a projection for this view instance.
*/
projectionColor?: string;

/**
* Check whether the given model element is in the current viewport. Use this method
* in your `render` implementation to skip rendering in case the element is not visible.
Expand All @@ -53,24 +48,4 @@ export abstract class ShapeView implements IView {

abstract render(model: Readonly<SChildElement>, context: RenderingContext, args?: IViewArgs): VNode | undefined;

getProjection(model: Readonly<SChildElement>, context: RenderingContext, args?: IViewArgs): ViewProjection | undefined {
if (!this.projectionColor || !isBoundsAware(model)) {
return undefined;
}
return {
projectedBounds: this.getProjectedBounds(model),
color: this.projectionColor
};
}

protected getProjectedBounds(model: Readonly<SChildElement & BoundsAware>): Bounds {
let bounds = model.bounds;
let parent = model.parent;
while (parent instanceof SChildElement) {
bounds = parent.localToParent(bounds);
parent = parent.parent;
}
return bounds;
}

}
85 changes: 85 additions & 0 deletions src/features/projection/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/********************************************************************************
* Copyright (c) 2021 TypeFox 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
********************************************************************************/

import { SChildElement, SParentElement } from '../../base/model/smodel';
import { SModelExtension } from '../../base/model/smodel-extension';
import { Bounds } from '../../utils/geometry';
import { hasOwnProperty } from '../../utils/object';
import { BoundsAware, isBoundsAware } from '../bounds/model';

/**
* Model elements implementing this interface can be displayed on a projection bar.
* _Note:_ Model elements also have to be `BoundsAware` so their projections can be shown.
*/
export interface Projectable extends SModelExtension {
projectionCssClasses: string[]
}

export function isProjectable(arg: unknown): arg is Projectable {
return hasOwnProperty(arg, 'projectionCssClasses');
}

/**
* A projection can be shown in a horizontal or vertical bar to display an overview of the diagram.
*/
export interface ViewProjection {
projectedBounds: Bounds;
cssClasses: string[];
}

/**
* Gather all projections of elements contained in the given parent element.
*/
export function getProjections(parent: Readonly<SParentElement>): ViewProjection[] | undefined {
let result: ViewProjection[] | undefined;
for (const child of parent.children) {
if (isProjectable(child) && isBoundsAware(child) && child.projectionCssClasses.length > 0) {
const projection: ViewProjection = {
projectedBounds: getProjectedBounds(child),
cssClasses: child.projectionCssClasses
};
if (result) {
result.push(projection);
} else {
result = [projection];
}
}
if (child.children.length > 0) {
const childProj = getProjections(child);
if (childProj) {
if (result) {
result.push(...childProj);
} else {
result = childProj;
}
}
}
}
return result;
}

/**
* Compute the projected bounds of the given model element, that is the absolute position in the diagram.
*/
export function getProjectedBounds(model: Readonly<SChildElement & BoundsAware>): Bounds {
let bounds = model.bounds;
let parent = model.parent;
while (parent instanceof SChildElement) {
bounds = parent.localToParent(bounds);
parent = parent.parent;
}
return bounds;
}
Loading

0 comments on commit 46b2d11

Please sign in to comment.