Skip to content

Commit 7c39483

Browse files
committed
rendering
1 parent cd6c006 commit 7c39483

File tree

16 files changed

+937
-3
lines changed

16 files changed

+937
-3
lines changed

frontend/webEditor/dependencyGraph.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@ stateDiagram-v2
1515
serialize --> constraint
1616
serialize --> editorMode
1717
[*] --> serialize
18-
1918
serialize --> commonModule: logger
2019
20+
[*] --> editorMode
21+
serialize --> editorMode
22+
23+
[*] --> diagram
24+
2125
classDef diLess font-style:italic,stroke:#0fa
2226
class accordionUiExtension,editorTypes,utils diLess
2327
```
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface DfdNodeAnnotation {
2+
message: string;
3+
color?: string;
4+
icon?: string;
5+
tfg?: number;
6+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { injectable } from "inversify";
2+
import { SChildElementImpl, SModelElementImpl, SModelFactory, SParentElementImpl } from "sprotty";
3+
import { DfdNode } from "./nodes/common";
4+
import { SLabel, SModelElement } from "sprotty-protocol";
5+
6+
@injectable()
7+
export class CustomModelFactory extends SModelFactory {
8+
override createElement(schema: SModelElement | SModelElementImpl, parent?: SParentElementImpl): SChildElementImpl {
9+
if (
10+
(schema.type === "node:storage" ||
11+
schema.type === "node:function" ||
12+
schema.type === "node:input-output") &&
13+
!(schema instanceof SModelElementImpl)
14+
) {
15+
const dfdSchema = schema as DfdNode;
16+
schema.children = schema.children ?? [];
17+
for (const port of dfdSchema.ports) {
18+
if ("features" in port) {
19+
delete port.features
20+
}
21+
}
22+
schema.children.push(...dfdSchema.ports, {
23+
type: "label:positional",
24+
text: dfdSchema.text ?? "",
25+
id: schema.id + "-label",
26+
} as SLabel);
27+
}
28+
29+
return super.createElement(schema, parent);
30+
}
31+
}
Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,41 @@
11
import { ContainerModule } from "inversify";
2-
import { configureModelElement, SGraphImpl, SGraphView } from "sprotty";
2+
import { configureModelElement, editLabelFeature, SGraphImpl, SGraphView, SLabelImpl, SLabelView, SRoutingHandleImpl, TYPES, withEditLabelFeature } from "sprotty";
3+
import { ArrowEdgeImpl, ArrowEdgeView, CustomRoutingHandleView } from "./edges/ArrowEdge";
4+
import { DfdInputPortImpl, DfdInputPortView } from "./ports/DfdInputPort";
5+
import { DfdOutputPortImpl, DfdOutputPortView } from "./ports/DfdOutputPort";
6+
import { StorageNodeImpl, StorageNodeView } from "./nodes/DfdStorageNode";
7+
import { FunctionNodeImpl, FunctionNodeView } from "./nodes/DfdFunctionNode";
8+
import { IONodeImpl, IONodeView } from "./nodes/DfdIONode";
9+
import './style.css'
10+
import { DfdPositionalLabelView } from "./labels/DfdPositionalLabel";
11+
import { CustomModelFactory } from "./ModelFactory";
12+
import { DfdNodeLabelRenderer } from "./nodes/DfdNodeLabels";
313

414
export const diagramModule = new ContainerModule((bind, unbind, isBound, rebind) => {
515
const context = { bind, unbind, isBound, rebind };
616

717
configureModelElement(context, "graph", SGraphImpl, SGraphView);
18+
19+
configureModelElement(context, "node:storage", StorageNodeImpl, StorageNodeView);
20+
configureModelElement(context, "node:function", FunctionNodeImpl, FunctionNodeView);
21+
configureModelElement(context, "node:input-output", IONodeImpl, IONodeView);
22+
23+
configureModelElement(context, "edge:arrow", ArrowEdgeImpl, ArrowEdgeView, {
24+
enable: [withEditLabelFeature],
25+
});
26+
configureModelElement(context, "routing-point", SRoutingHandleImpl, CustomRoutingHandleView);
27+
configureModelElement(context, "volatile-routing-point", SRoutingHandleImpl, CustomRoutingHandleView);
28+
29+
configureModelElement(context, "port:dfd-input", DfdInputPortImpl, DfdInputPortView);
30+
configureModelElement(context, "port:dfd-output", DfdOutputPortImpl, DfdOutputPortView);
31+
32+
configureModelElement(context, "label", SLabelImpl, SLabelView, {
33+
enable: [editLabelFeature],
34+
});
35+
configureModelElement(context, "label:positional", SLabelImpl, DfdPositionalLabelView, {
36+
enable: [editLabelFeature],
37+
});
38+
39+
rebind(TYPES.IModelFactory).to(CustomModelFactory);
40+
bind(DfdNodeLabelRenderer).toSelf().inSingletonScope()
841
});
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/** @jsx svg */
2+
import { injectable } from "inversify";
3+
import {
4+
PolylineEdgeViewWithGapsOnIntersections,
5+
SEdgeImpl,
6+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7+
svg,
8+
RenderingContext,
9+
IViewArgs,
10+
WithEditableLabel,
11+
isEditableLabel,
12+
SRoutingHandleView,
13+
} from "sprotty";
14+
import { VNode } from "snabbdom";
15+
import { Point, angleOfPoint, toDegrees, SEdge } from "sprotty-protocol";
16+
17+
export interface ArrowEdge extends SEdge {
18+
text?: string;
19+
}
20+
21+
export class ArrowEdgeImpl extends SEdgeImpl implements WithEditableLabel {
22+
text?: string;
23+
24+
get editableLabel() {
25+
const label = this.children.find((element) => element.type.startsWith("label"));
26+
if (label && isEditableLabel(label)) {
27+
return label;
28+
}
29+
30+
return undefined;
31+
}
32+
}
33+
34+
@injectable()
35+
export class ArrowEdgeView extends PolylineEdgeViewWithGapsOnIntersections {
36+
37+
override render(edge: Readonly<SEdgeImpl>, context: RenderingContext, args?: IViewArgs): VNode | undefined {
38+
// In the default implementation children of the edge are always rendered, because they
39+
// may be visible when the rest of the edge is not.
40+
// We only have the edge label as an children which only must be rendered when the rest of the edge is visible.
41+
// So as an optimization for big diagrams we don't render the label when the rest of the edge is not visible either.
42+
// Otherwise all these labels would be added to the DOM, making it slow..
43+
const route = this.edgeRouterRegistry.route(edge, args);
44+
if (!this.isVisible(edge, route, context)) {
45+
return undefined;
46+
}
47+
48+
return super.render(edge, context, args);
49+
}
50+
51+
52+
/**
53+
* Renders an arrow at the end of the edge.
54+
*/
55+
protected override renderAdditionals(edge: SEdgeImpl, segments: Point[], context: RenderingContext): VNode[] {
56+
const additionals = super.renderAdditionals(edge, segments, context);
57+
const p1 = segments[segments.length - 2];
58+
const p2 = segments[segments.length - 1];
59+
const arrow = (
60+
<path
61+
class-arrow={true}
62+
d="M 0.5,0 L 10,-4 L 10,4 Z"
63+
transform={`rotate(${toDegrees(angleOfPoint({ x: p1.x - p2.x, y: p1.y - p2.y }))} ${p2.x} ${
64+
p2.y
65+
}) translate(${p2.x} ${p2.y})`}
66+
style={{ opacity: edge.opacity.toString() }}
67+
/>
68+
);
69+
additionals.push(arrow);
70+
return additionals;
71+
}
72+
73+
/**
74+
* Renders the edge line.
75+
* In contrast to the default implementation that we override here,
76+
* this implementation makes the edge line 10px shorter at the end to make space for the arrow without any overlap.
77+
*/
78+
protected renderLine(edge: SEdgeImpl, segments: Point[]): VNode {
79+
const firstPoint = segments[0];
80+
let path = `M ${firstPoint.x},${firstPoint.y}`;
81+
for (let i = 1; i < segments.length; i++) {
82+
const p = segments[i];
83+
if (i === segments.length - 1) {
84+
// Make edge line 9.5px shorter to make space for the arrow
85+
// The arrow is 10px long, but we only shorten by 9.5 px to have overlap at the edge between line and arrow.
86+
// Otherwise edges would be exactly next to each other which would result in a small gap and flickering if you zoom in enough.
87+
const prevP = segments[i - 1];
88+
const dx = p.x - prevP.x;
89+
const dy = p.y - prevP.y;
90+
const length = Math.sqrt(dx * dx + dy * dy);
91+
const ratio = (length - 9.5) / length;
92+
path += ` L ${prevP.x + dx * ratio},${prevP.y + dy * ratio}`;
93+
} else {
94+
// Lines between points in between are not shortened
95+
path += ` L ${p.x},${p.y}`;
96+
}
97+
}
98+
return (
99+
<g>
100+
{/* This is the actual path being rendered */}
101+
<path d={path} style={{ opacity: edge.opacity.toString() }} />
102+
{/* This is a transparent path that is rendered on top of the actual path to make it easier to select the edge */}
103+
<path d={path} class-select-path={true} />
104+
</g>
105+
);
106+
}
107+
}
108+
109+
/**
110+
* Smaller version of the default edge routing handle.
111+
*/
112+
@injectable()
113+
export class CustomRoutingHandleView extends SRoutingHandleView {
114+
getRadius(): number {
115+
return 5;
116+
}
117+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/** @jsx svg */
2+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
3+
import { IViewArgs, SLabelImpl, SNodeImpl, ShapeView, RenderingContext, svg } from "sprotty";
4+
import { VNode } from "snabbdom";
5+
import { injectable } from "inversify";
6+
import { Point } from "sprotty-protocol";
7+
8+
export interface DfdPositionalLabelArgs extends IViewArgs {
9+
xPosition: number;
10+
yPosition: number;
11+
}
12+
13+
@injectable()
14+
export class DfdPositionalLabelView extends ShapeView {
15+
private getPosition(label: Readonly<SLabelImpl>, args?: DfdPositionalLabelArgs | IViewArgs): Point {
16+
if (args && "xPosition" in args && "yPosition" in args) {
17+
return { x: args.xPosition, y: args.yPosition };
18+
} else {
19+
const parentSize = (label.parent as SNodeImpl | undefined)?.bounds;
20+
const width = parentSize?.width ?? 0;
21+
const height = parentSize?.height ?? 0;
22+
return { x: width / 2, y: height / 2 };
23+
}
24+
}
25+
26+
render(label: Readonly<SLabelImpl>, _context: RenderingContext, args?: DfdPositionalLabelArgs): VNode | undefined {
27+
const position = this.getPosition(label, args);
28+
29+
return (
30+
<text class-sprotty-label={true} x={position.x} y={position.y}>
31+
{label.text}
32+
</text>
33+
);
34+
}
35+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/** @jsx svg */
2+
import { inject, injectable } from "inversify";
3+
import { DfdNodeImpl } from "./common";
4+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
5+
import { ShapeView, RenderingContext, svg } from "sprotty";
6+
import { VNode } from "snabbdom";
7+
import { DfdPositionalLabelArgs } from "../labels/DfdPositionalLabel";
8+
import { DfdNodeLabelRenderer } from "./DfdNodeLabels";
9+
10+
export class FunctionNodeImpl extends DfdNodeImpl {
11+
static readonly TEXT_HEIGHT = 28;
12+
static readonly SEPARATOR_NO_LABEL_PADDING = 4;
13+
static readonly SEPARATOR_LABEL_PADDING = 4;
14+
static readonly LABEL_START_HEIGHT = this.TEXT_HEIGHT + this.SEPARATOR_LABEL_PADDING;
15+
static readonly BORDER_RADIUS = 5;
16+
17+
protected noLabelHeight(): number {
18+
return FunctionNodeImpl.LABEL_START_HEIGHT + FunctionNodeImpl.SEPARATOR_NO_LABEL_PADDING;
19+
}
20+
protected labelStartHeight(): number {
21+
return FunctionNodeImpl.LABEL_START_HEIGHT
22+
}
23+
24+
25+
}
26+
27+
@injectable()
28+
export class FunctionNodeView extends ShapeView {
29+
constructor(@inject(DfdNodeLabelRenderer) private readonly labelRenderer: DfdNodeLabelRenderer) {
30+
super();
31+
}
32+
33+
render(node: Readonly<FunctionNodeImpl>, context: RenderingContext): VNode | undefined {
34+
if (!this.isVisible(node, context)) {
35+
return undefined;
36+
}
37+
38+
const { width, height } = node.bounds;
39+
const r = FunctionNodeImpl.BORDER_RADIUS;
40+
41+
return (
42+
<g class-sprotty-node={true} class-function={true} style={node.geViewStyleObject()}>
43+
<rect x="0" y="0" width={width} height={height} rx={r} ry={r} />
44+
<line x1="0" y1={FunctionNodeImpl.TEXT_HEIGHT} x2={width} y2={FunctionNodeImpl.TEXT_HEIGHT} />
45+
{context.renderChildren(node, {
46+
xPosition: width / 2,
47+
yPosition: FunctionNodeImpl.TEXT_HEIGHT / 2,
48+
} as DfdPositionalLabelArgs)}
49+
{this.labelRenderer.renderNodeLabels(node, FunctionNodeImpl.LABEL_START_HEIGHT)}
50+
</g>
51+
);
52+
}
53+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/** @jsx svg */
2+
import { inject, injectable } from "inversify";
3+
import { DfdNodeImpl } from "./common";
4+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
5+
import { ShapeView, svg, RenderingContext } from "sprotty";
6+
import { VNode } from "snabbdom";
7+
import { DfdPositionalLabelArgs } from "../labels/DfdPositionalLabel";
8+
import { DfdNodeLabelRenderer } from "./DfdNodeLabels";
9+
10+
@injectable()
11+
export class IONodeImpl extends DfdNodeImpl {
12+
static readonly TEXT_HEIGHT = 32;
13+
static readonly LABEL_START_HEIGHT = 28;
14+
15+
protected noLabelHeight(): number {
16+
return IONodeImpl.TEXT_HEIGHT;
17+
}
18+
protected labelStartHeight(): number {
19+
return IONodeImpl.LABEL_START_HEIGHT
20+
}
21+
}
22+
23+
@injectable()
24+
export class IONodeView extends ShapeView {
25+
26+
constructor(@inject(DfdNodeLabelRenderer) private readonly labelRenderer: DfdNodeLabelRenderer) {
27+
super();
28+
}
29+
30+
render(node: Readonly<DfdNodeImpl>, context: RenderingContext): VNode | undefined {
31+
if (!this.isVisible(node, context)) {
32+
return undefined;
33+
}
34+
35+
const { width, height } = node.bounds;
36+
37+
return (
38+
<g class-sprotty-node={true} class-io={true} style={node.geViewStyleObject()}>
39+
<rect x="0" y="0" width={width} height={height} />
40+
{context.renderChildren(node, {
41+
xPosition: width / 2,
42+
yPosition: IONodeImpl.TEXT_HEIGHT / 2,
43+
} as DfdPositionalLabelArgs)}
44+
{this.labelRenderer.renderNodeLabels(node, IONodeImpl.LABEL_START_HEIGHT)}
45+
</g>
46+
);
47+
}
48+
}

0 commit comments

Comments
 (0)