diff --git a/hugo/content/docs/recipes/layout-strategies.md b/hugo/content/docs/recipes/layout-strategies.md new file mode 100644 index 0000000..31cbbf8 --- /dev/null +++ b/hugo/content/docs/recipes/layout-strategies.md @@ -0,0 +1,326 @@ +--- +title: 'Layout Strategies' +--- +{{< toc >}} + +Layout is a fundamental aspect of creating effective diagrams. Sprotty provides flexible layout strategies that can be configured to run on the client, server, or in hybrid combinations. This recipe covers the different layout approaches, their configuration, and best practices for each scenario. + +## Understanding Layout Types + +Sprotty distinguishes between two complementary layout systems that work together to create the final diagram appearance: + +### 1. Micro-Layout + +**Micro-layout** handles the internal arrangement of elements within nodes and compartments. This includes: + +- Label positioning within nodes +- Icon and text alignment +- Compartment content organization +- Child element spacing and sizing + +The micro-layout engine runs in the browser and has access to actual font metrics and CSS styling information, making it ideal for precise text and content positioning. + +### 2. Macro-Layout + +**The macro-layout engine** determines the overall diagram structure and relationships: + +- Node positioning in the diagram space +- Edge routing between nodes +- Global diagram organization +- Cross-node spatial relationships + +Sprotty does not provide a macro-layout engine by default but an external engine running on the client-side or remotely on a server can be easily connected. +The macro-layout engine typically uses sophisticated algorithms (like those in [Eclipse Layout Kernel](https://eclipse.dev/elk/)) to create aesthetically pleasing and readable diagram arrangements. + +## Micro-Layout Only Configuration + +For diagrams where you want only micro-layout control, you must enable the micro-layout engine in the viewer options, and specify layout options and positions directly in the model. + +### Configuration + +```typescript +import { ContainerModule } from 'inversify'; +import { TYPES, configureViewerOptions } from 'sprotty'; + +const layoutModule = new ContainerModule((bind, unbind, isBound, rebind) => { + const context = { bind, unbind, isBound, rebind }; + + // Configure for client-side layout + configureViewerOptions(context, { + needsClientLayout: true, // Enable micro-layout computation + needsServerLayout: false, // Disable macro-layout layout + baseDiv: 'diagram-container' + }); +}); +``` + +### Model Configuration + +```typescript +// Define nodes with layout properties +const clientLayoutModel: SGraph = { + type: 'graph', + id: 'client-layout-demo', + children: [ + { + type: 'node:compartment', + id: 'node1', + position: { x: 50, y: 50 }, // The position of each element has to be set manually + size: { width: 200, height: 120 }, + layout: 'vbox', // Vertical layout for children + layoutOptions: { + paddingTop: 10, + paddingBottom: 10, + paddingLeft: 15, + paddingRight: 15, + vGap: 5 // Gap between vertical elements + }, + children: [ + { + type: 'label', + id: 'node1-title', + text: 'Client Layout Node', + fontSize: 14 + }, + { + type: 'label', + id: 'node1-subtitle', + text: 'Managed locally', + fontSize: 12 + } + ] + } + ] +}; +``` + +### Layout Options Reference + +| Option | Description | Default | +|--------|-------------|---------| +| `paddingTop` | Top padding inside container | 0 | +| `paddingBottom` | Bottom padding inside container | 0 | +| `paddingLeft` | Left padding inside container | 0 | +| `paddingRight` | Right padding inside container | 0 | +| `vGap` | Vertical gap between elements (for `vbox` layout) | 0 | +| `hGap` | Horizontal gap between elements (for `hbox` layout) | 0 | +| `hAlign` | Horizontal alignment: `left`, `center`, `right` (for `vbox` layout only) | `left` | +| `vAlign` | Vertical alignment: `top`, `center`, `bottom` (for `hbox` layout only) | `top` | + +## Macro-Layout Only Configuration + +For complex diagrams requiring sophisticated layout algorithms, delegate layout to an external layout engine. These engines are typically highly performant for layouting diagrams, but may lack micro-layout control beyond label placement. + +### Configuration + +```typescript +import { ContainerModule } from 'inversify'; +import { TYPES, configureViewerOptions } from 'sprotty'; +import { ElkLayoutEngine } from 'sprotty-elk/lib/inversify'; + +const serverLayoutModule = new ContainerModule((bind, unbind, isBound, rebind) => { + const context = { bind, unbind, isBound, rebind }; + + // Configure for server-side layout + configureViewerOptions(context, { + needsClientLayout: false, // Disable micro-layout engine + needsServerLayout: true // Enable macro-layout engine + }); + + // Bind layout engine (example with ELK) + bind(TYPES.IModelLayoutEngine).to(ElkLayoutEngine).inSingletonScope(); +}); +``` + +### Model with Layout Hints + +```typescript +const serverLayoutModel: SGraph = { + type: 'graph', + id: 'server-layout-demo', + layoutOptions: { + 'elk.algorithm': 'layered', + 'elk.direction': 'DOWN', + 'elk.spacing.nodeNode': '50', + 'elk.layered.spacing.nodeNodeBetweenLayers': '80' + }, + children: [ + { + type: 'node', + id: 'A', + // No position - will be computed by server + size: { width: 100, height: 60 }, + layoutOptions: { + 'elk.portConstraints': 'FIXED_SIDE' + } + }, + { + type: 'node', + id: 'B', + size: { width: 120, height: 80 } + }, + { + type: 'edge', + id: 'A-B', + sourceId: 'A', + targetId: 'B' + } + ] +}; +``` + +### ELK Algorithm Options + +ELK provides several layout algorithms optimized for different diagram types, including layered (hierarchical), force-directed, stress-minimization, tree, and radial layouts. Each algorithm supports extensive configuration options for fine-tuning the layout behavior. + +For a complete list of available algorithms and their configuration options, see the [Eclipse Layout Kernel documentation](https://www.eclipse.org/elk/reference.html). + +## Hybrid Layout + +Combine both layout types for maximum flexibility and control. + +### Configuration + +```typescript +const hybridLayoutModule = new ContainerModule((bind, unbind, isBound, rebind) => { + const context = { bind, unbind, isBound, rebind }; + + // Enable both layout types + configureViewerOptions(context, { + needsClientLayout: true, // For micro-layout + needsServerLayout: true // For macro-layout + }); + + bind(TYPES.IModelLayoutEngine).to(ElkLayoutEngine).inSingletonScope(); +}); +``` + +### Layout Workflow + +The hybrid approach follows this sequence: + +1. **Client Layout Phase**: + - Compute bounds for labels and compartment contents + - Calculate actual text dimensions using browser fonts + - Update node sizes based on content + +2. **Server Layout Phase**: + - Use updated node sizes from client layout + - Compute node positions and edge routing + - Apply global layout algorithm + +3. **Final Rendering**: + - Render nodes at server-computed positions + - Render content at client-computed positions within nodes + +```typescript +// Model that benefits from hybrid layout +const hybridModel: SGraph = { + type: 'graph', + id: 'hybrid-demo', + layoutOptions: { + 'elk.algorithm': 'layered', + 'elk.direction': 'RIGHT' + }, + children: [ + { + type: 'node:compartment', + id: 'service1', + // Position computed by server + layout: 'vbox', // Client layout for contents + layoutOptions: { + paddingTop: 8, + paddingLeft: 12, + vGap: 4 + }, + children: [ + { type: 'label', id: 'service1-title', text: 'User Service' }, + { type: 'label', id: 'service1-port', text: 'Port: 8080' }, + { type: 'label', id: 'service1-status', text: 'Status: Running' } + ] + } + ] +}; +``` + +## Bounds Computation Workflow + +Understanding the bounds computation process is crucial for effective layout configuration. + +### The Two-Phase Process + +#### Phase 1: Invisible Rendering + +```typescript +// 1. RequestBoundsAction is dispatched +const requestBounds: RequestBoundsAction = { + kind: 'requestBounds', + newRoot: model +}; + +// 2. Model is rendered with hidden visibility +// Elements get actual font metrics and CSS styling +// Text dimensions are measured accurately +``` + +#### Phase 2: Visible Rendering + +```typescript +// 3. ComputedBoundsAction contains measured sizes +const computedBounds: ComputedBoundsAction = { + kind: 'computedBounds', + bounds: [ + { elementId: 'label1', newPosition: { x: 10, y: 5 }, newSize: { width: 85, height: 16 }}, + { elementId: 'node1', newPosition: { x: 0, y: 0 }, newSize: { width: 105, height: 26 }} + ] +}; + +// 4. Final model update with computed bounds +const updateModel: UpdateModelAction = { + kind: 'updateModel', + newRoot: updatedModelWithBounds, + animate: false +}; +``` + +## Best Practices + +### 1. Choose the Right Strategy + +- **Micro-layout only**: Simple diagrams with mostly static layouts +- **Macro-layout**: Complex network diagrams requiring sophisticated algorithms +- **Hybrid**: Rich content nodes in algorithmically-arranged diagrams + +### 2. Optimize Performance + +```typescript +// Batch layout updates +const batchedUpdates = new Map>(); + +// Collect all changes +batchedUpdates.set('node1', { position: { x: 100, y: 50 } }); +batchedUpdates.set('node2', { size: { width: 120, height: 80 } }); + +// Apply all at once +this.modelSource.updateModel(applyBatchedUpdates(currentModel, batchedUpdates)); +``` + +### 3. Handle Dynamic Content + +```typescript +// Responsive node sizing based on content +@injectable() +export class ResponsiveNodeView implements IView { + render(node: ResponsiveNode, context: RenderingContext): VNode { + const contentLength = node.content?.length || 0; + const dynamicWidth = Math.max(100, contentLength * 8 + 20); + + return + + {node.content} + ; + } +} +``` + +Layout strategies are fundamental to creating effective diagrams. Choose the approach that best fits your use case, and don't hesitate to combine strategies for optimal results. diff --git a/hugo/content/docs/recipes/svg-rendering.md b/hugo/content/docs/recipes/svg-rendering.md deleted file mode 100644 index af99b05..0000000 --- a/hugo/content/docs/recipes/svg-rendering.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: 'SVG Rendering' ---- -{{< toc >}} -Sprotty transforms a given `SModel` to its representation in the DOM in the form of a hierarchy of SVG elements. An `SModel` is composed of `SModelElement`s, and each `SModelElement` has a `type` property that is associated to a single corresponding `View`. The `ViewRegistry` keeps a map of the correspondence between an element type and a view. These model elements are organized in the *virtual DOM* before being rendered as actual SVG elements in the DOM. - -## Virtual DOM - -The virtual DOM is a tree-based, partial or complete representation of the DOM content. In our context, the virtual DOM contains a representation of our diagram SVG elements and their hierarchy. For every update made to the diagram, Sprotty generates a new `SModel` through the `CommandStack` and forwards it to the `Viewer`. Updates are often limited to a specific node or group of nodes and it would be unnecessarily expensive to have to re-render the entire diagram for each update. It is more efficient to apply minimal updates to directly impacted DOM elements. Sprotty relies on [Snabbdom](https://github.com/snabbdom/snabbdom) for handling elements in the *virtual DOM* and applying minimal changes the DOM through *patching*. - -## Views - -Views are at the center of Sprotty's rendering mechanism. They are classes implementing the `IView` interface and have a `render()` method that describes the SVG elements to be rendered for a given `SModelElement` type. Views use the TSX syntax, which allow combining HTML and TypeScript code to define elements, and therefore needs to reside inside of files with the `.tsx` extension. - -Let's have a look at the definition of the `NodeView` from the [Class Diagram example](https://github.com/eclipse-sprotty/sprotty/blob/master/examples/classdiagram/src/views.tsx): - -```typescript -@injectable() -export class NodeView extends RectangularNodeView { - override render(node: Readonly, context: RenderingContext, args?: IViewArgs): VNode | undefined { - if (!this.isVisible(node, context)) { - return undefined; - } - return - - {context.renderChildren(node)} - ; - } -} -``` - -The class `NodeView` extends `RectangularNodeView` which is a default `View` in Sprotty, ultimately implementing `IView`. Don't forget to add the class decorator `@injectable()`, which is necessary for the [Dependency Injection]({{< relref "extension-points">}}#dependency-injection). - -The `render()` method is the core of the `View`. It takes `node` -- that is the model element to be rendered - as an argument, a `RenderingContext`, and an optional `args` object. View implementations should first check whether the `node` should be rendered at all. This is an optimization step, as we only want to render SVG elements that are inside of the visible viewport and not hidden by some other user-defined filter. -Eventually, the `render()` method returns a `VNode` which is [Snabbdom's](https://github.com/snabbdom/snabbdom) virtual representation of a DOM element. This `VNode` can hold one and only one *root element*, therefore we need to group our SVG elements inside of a *container element* `g`. -The SVG elements can be styled using CSS classes using Snabbdom's notation. Classes are dynamically toggled with expressions of the form `class-X={boolean expression}`, with the `class-` prefix followed by the name of the class and a boolean expression to determine if the class should be toggled or not. In our example, the `rect` element created by the `NodeView` could have a class of `sprotty-node`, `node-package`, `node-class`, `mouseover`, `selected`, or any combination of those depending of the respective boolean expressions. It is then easy to style elements with CSS based on class names. - -Other attributes like width, height, and position are also defined here. Please note that the position is set at `x="0" y="0"` since the actual position will be determined by the layout engine later on. - -Finally, we render children of the node with the `renderChildren()` method from the `RenderingContext`. We strongly advocate for keeping the `View`'s responsibility to render only the SVG that is specific for the given node. Any child (e.g. labels, buttons, children nodes) should be rendered in their own `View`. This keeps the code better organized and removes the complexity of having to implement the layout of nested elements such as labels in the `View` itself and instead delegates this responsibility to the layout engine. - -## Layouting - -It is important to distinguish two types of layout: - -1. The [*client layout*](#client-layout) (or *micro-layout*) which deals with the layout of a node's children. -2. The [*server layout*](#server-layout) (or *macro-layout*) which deals with the overall shape of the diagram, placement of nodes and edges. - -### Client Layout - -The *client layout*, a.k.a. *micro-layout*, occurs first. In this phase, Sprotty computes the position and size for elements that add some visual information such as labels to nodes and edges. Different layouts are selected using the `layout` property of a `SNode` or `SCompartment`. The values of the `layout` property can be: - -* `hbox` for an horizontal layout. -* `vbox` for a vertical layout. -* `stack` for a stacking of children elements. - -The micro-layout is computed in two phases: - -1. A `RequestBoundAction` is received and the model is rendered invisibly (e.g. by assigning a width and height of zero to the elements). The locally used fonts and CSS styles are applied during this rendering phase. The resulting size information is used to invoke the selected layouts and the updated bounds are written into a `ComputedBoundAction`. -2. The bounds stored in the `ComputedBoundAction` are applied to the model and initiates the visible rendering of the updated model with `SetModelAction` or `UpdateModelAction`. - -In depth documentation about the micro-layouting can be found [here]({{< relref "micro-layout" >}}) - -## Server Layout - -The *server layout*, a.k.a. *macro-layout* takes place **after** the *client layout*. This is configured with an implementation of the `ILayoutEngine` interface. This takes care of the general shape of the diagram and computes the position of nodes and edges. Sprotty provides a [package](https://github.com/eclipse-sprotty/sprotty/tree/master/packages/sprotty-elk) to use the [Eclipse Layout Kernel(ELK)](https://www.eclipse.org/elk/), a JAVA-based automatic layout engine with several standard algorithms, but it is also of course possible to implement your own. diff --git a/hugo/data/menu/main.yaml b/hugo/data/menu/main.yaml index 81116e4..33cca5e 100644 --- a/hugo/data/menu/main.yaml +++ b/hugo/data/menu/main.yaml @@ -31,8 +31,8 @@ main: sub: - name: Custom Views ref: "docs/recipes/custom-views" - - name: SVG Rendering - ref: "docs/recipes/svg-rendering" + - name: Layout Strategies + ref: "docs/recipes/layout-strategies" - name: Micro-layout ref: "docs/recipes/micro-layout" - name: Styling