# Extend rendered geometry

Use a geometry extension when the shipped figures, arrowheads or line styles cannot express your notation. Register the geometry by name, then use that name in the specs you pass to your mounted diagram. The renderer owns the geometry as nodes move; your extension supplies an outline, a path or the SVG elements that paint it.

Start with the built-ins: figures include `diamond`, `cylinder`, `document` and `actor`; markers include `open-arrow`, `crow-foot` and `hollow-diamond`. For routing and corner treatment, see [Route and label edges](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/route-and-label-edges). Go below the component's props only to define geometry the renderer does not already provide.

## 1. Register a silhouette, pipe and marker

This example draws a built-in rectangle connected to a custom chevron by a two-stroke pipe, then connects the chevron to a sink with a feather marker. The pipe follows the routed path rather than a fixed picture.

Use [`registerPathShape`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-svg-functions-p-w#registerpathshape) for an SVG-path silhouette. It derives boundary points and port anchors from the outline and reuses the outline for the body, selection and shadow. Use [`registerShape`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-svg-functions-p-w#registershape) only when you need to implement the full [`ShapeDefinition`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-svg-shapedefinition#shapedefinition) contract yourself: `outline`, `boundaryPoint` and `portAnchor`, with an optional `innerRect` for the label.

[`registerLinkTemplate`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-svg-functions-p-w#registerlinktemplate) receives the current routed points, `pathData` and selection state. Its output replaces the visible edge rendering; the renderer retains the link wrapper and hit area. [`registerMarker`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-svg-functions-p-w#registermarker) defines an endpoint glyph. Put the supplied `transform` on its root element and declare `tipOffset` so its visual tip meets the endpoint.

Create this shared file in your browser application. Its nodes use the library's [`NodeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-nodespec) type and its edges use [`EdgeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-edgespec#edgespec).

```ts title="geometry.ts"
import {
  registerPathShape,
  registerLinkTemplate,
  registerMarker,
  type NodeSpec,
  type EdgeSpec,
} from '@grafloria/renderer';

export function registerGeometry(): void {
  registerPathShape(
    'notation-chevron',
    'M2,4 L14,4 L22,12 L14,20 L2,20 L10,12 Z',
    {
      viewBox: { x: 0, y: 0, w: 24, h: 24 },
      innerRect: (w, h) => ({ x: w * 0.42, y: h * 0.35, w: w * 0.2, h: h * 0.3 }),
    },
  );

  registerLinkTemplate('notation-pipe', (ctx) => {
    const stroke = ctx.selected ? '#2563eb' : '#0ea5e9';
    return [
      {
        type: 'path',
        props: {
          d: ctx.pathData, fill: 'none', stroke,
          'stroke-width': 10, 'stroke-opacity': 0.35,
          'stroke-linecap': 'round',
        },
      },
      {
        type: 'path',
        props: { d: ctx.pathData, fill: 'none', stroke, 'stroke-width': 2.5 },
      },
    ];
  });

  registerMarker('notation-feather', {
    tipOffset: (style) => style.size,
    render: (ctx) => ({
      type: 'path',
      props: {
        d: `M0,0 L${ctx.size},0 M${ctx.size * 0.4},-4 L${ctx.size},0 L${ctx.size * 0.4},4`,
        stroke: ctx.color, fill: 'none', 'stroke-width': 1.5,
        transform: ctx.transform,
      },
    }),
  });
}

export const nodes: NodeSpec[] = [
  {
    id: 'source', position: { x: 40, y: 80 },
    size: { width: 120, height: 64 }, label: 'Source',
    shape: { type: 'rect', fill: '#dbeafe', stroke: '#2563eb' },
  },
  {
    id: 'gate', position: { x: 260, y: 160 },
    size: { width: 140, height: 100 }, label: 'Go',
    shape: { type: 'notation-chevron', fill: '#dbeafe', stroke: '#2563eb' },
  },
  {
    id: 'sink', position: { x: 500, y: 80 },
    size: { width: 120, height: 64 }, label: 'Sink',
    shape: { type: 'rect', fill: '#dbeafe', stroke: '#2563eb' },
  },
];

export const edges: EdgeSpec[] = [
  {
    id: 'pipe', source: 'source', target: 'gate', type: 'smooth',
    style: { template: 'notation-pipe' },
  },
  {
    id: 'feather', source: 'gate', target: 'sink',
    style: { arrowHead: { type: 'notation-feather', size: 14, filled: false } },
  },
];
```

The registration function returns no instance. The next step mounts real nodes and edges that consume all three names.

## 2. Mount the diagram in your framework

Install the shared packages and the binding you use:

```bash
npm install @grafloria/element @grafloria/engine @grafloria/renderer
# Add one binding for a framework application:
npm install @grafloria/react
npm install @grafloria/vue
npm install @grafloria/angular
npm install @grafloria/qwik
```

This task adds geometry registration to the existing mounting pattern; see [Edit nodes](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/edit-nodes) for the component and instance entry points.

The browser entry point calls [`render`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core#render) after registration.

```ts title="JavaScript"
import { render } from '@grafloria/element';
import { nodes, edges, registerGeometry } from './geometry';

registerGeometry();
const wrapper = document.createElement('section');
const host = document.createElement('div');
host.style.height = '400px';
const remove = document.createElement('button');
remove.textContent = 'Remove diagram';
wrapper.append(host, remove);
document.body.append(wrapper);
const instance = render({ nodes, edges }, host);
remove.addEventListener('click', () => {
  instance.dispose();
  wrapper.remove();
}, { once: true });
```

For React, register before rendering [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react); see [Route and label edges](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/route-and-label-edges) for the default-spec mounting pattern.

```tsx title="GeometryDiagram.tsx"
import { GrafloriaFlow } from '@grafloria/react';
import { nodes, edges, registerGeometry } from './geometry';

registerGeometry();

export default function GeometryDiagram() {
  return <div style={{ height: '400px' }}>
    <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges} />
  </div>;
}
```

For Vue, register in the setup script before [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-vue) mounts; see [How Grafloria works](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/how-grafloria-works) for default-spec ownership.

```vue title="GeometryDiagram.vue"
<script setup lang="ts">
import { GrafloriaFlow } from '@grafloria/vue';
import { nodes, edges, registerGeometry } from './geometry';

registerGeometry();
</script>

<template>
  <div style="height:400px">
    <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" />
  </div>
</template>
```

For Angular, register in the constructor before [`DiagramCanvasComponent`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-angular-components-diagramcanvascomponent) paints; see [How Grafloria works](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/how-grafloria-works) for the two-way bindings.

```ts title="geometry-diagram.component.ts"
import { Component } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
import { nodes, edges, registerGeometry } from './geometry';

@Component({
  selector: 'app-geometry-diagram',
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
      style="display:block; height:400px" />
  `,
})
export class GeometryDiagramComponent {
  nodes: NodeSpec[] = nodes;
  edges: EdgeSpec[] = edges;

  constructor() {
    registerGeometry();
  }
}
```

For Qwik, register in [`GrafloriaFlow`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik)'s browser initialization callback and repaint its live [`DiagramInstance`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-diagraminstance#diagraminstance); see [Route and label edges](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/route-and-label-edges) for the default-spec mounting pattern.

```tsx title="GeometryDiagram.tsx"
import { component$, $ } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';
import { nodes, edges, registerGeometry } from './geometry';

export default component$(() => (
  <div style={{ height: '400px' }}>
    <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
      onInit$={$((instance: DiagramInstance) => {
        registerGeometry();
        instance.renderNow();
      })} />
  </div>
));
```

The chevron labelled Go connects to Source through the blue two-stroke pipe; its thinner outgoing line ends with the feather marker at Sink.

You see Source, the chevron labelled Go, and Sink. Drag the chevron to change both routes; the pipe repaints from the routed `pathData`, and the feather receives the endpoint's new transform. Select the pipe to switch its strokes to the selection blue.

Register before the first paint in JavaScript, React, Vue and Angular. In Qwik, register from the browser's `onInit$` callback and call `renderNow()` to repaint with the definitions; do not rely on a server-side registration surviving resumption. The specs contain names, not the template functions.

See the live [Shapes](https://grafloria.com/demos/nodes/shapes.html), [Custom edges](https://grafloria.com/demos/edges/custom-edges.html) and [Edge markers](https://grafloria.com/demos/edges/edge-markers.html) demos for the individual extensions.

## Choose the remaining geometry seams

Keep the stages separate: an anchor chooses one endpoint, a connection-point strategy chooses both endpoints, a router computes the polyline, and a connector turns that polyline into an SVG path. An edge template replaces the visible rendering after that path has been computed.

| Extension | Select it with | Input and result |
| --- | --- | --- |
| [`registerAnchor`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-ext-functions#registeranchor) | `metadata.sourceAnchor` or `metadata.targetAnchor` on the edge | Reads this end, the opposite end, the default point and arguments; returns `{ point, side? }`. Use the world-space node rectangle for a notation-specific attachment. |
| [`registerConnectionPoint`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-ext-functions#registerconnectionpoint) | Edge `metadata.connectionPoint`, or the renderer's `connectionPoint` default | Reads both ends and their defaults; returns `{ start, end, sourceDirection?, targetDirection? }`, or `null` to defer to the default pipeline. The context supplies a shape-boundary solver. |
| [`registerConnector`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-ext-functions#registerconnector) | Edge `connector` | Reads the routed world-space points, style and resolved corner radius; returns a complete SVG path string. |
| [`registerLabelTemplate`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-svg-functions-p-w#registerlabeltemplate) | A live link label's `template` | Reads the label, link, world-space anchor, rotation and theme; returns SVG elements or a `foreignObject`, or `null` to suppress the label. |
| [`registerPortLayout`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-svg-functions-p-w#registerportlayout) | Port `layout.strategy`, or a group's layout under node `metadata.portGroups` | Reads node-local width, height, side, rank, count and shape type, plus layout arguments; returns node-local `{ x, y }`. |

For floating attachment, try the shipped `smart` connection-point strategy before implementing a two-ended strategy. For connectors, try `straight`, `rounded`, `smooth` or `bezier`. For port layouts, the shipped choices are `shape`, `absolute`, `line`, `sideLinear`, `ellipse` and `ellipseSpread`; `shape` preserves the silhouette's own anchors.

To select a label template during setup, obtain the live link through `instance.getModel().getLink(id)` and call its `addLabel()` with `text`, `position` or `slot`, and `template`. The top-level edge `label` is the text convenience field, not a label-template selector. Repaint through the instance after setup mutations. For user-facing edits that need undo, use [Commands and history](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/commands-and-history).

## Options that matter

The shape options belong to [`PathShapeOptions`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-svg-interfaces-o-s#pathshapeoptions).

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `viewBox` | `{ x: number; y: number; w: number; h: number }` | `{ x: 0, y: 0, w: 1, h: 1 }` | Scales a static path's reference box into the node box. Set it to the art box you authored. |
| `sampleSteps` | `number` | `24` | Sets curve subdivision when sampling the outline. |
| `portAnchor` | `ShapeDefinition['portAnchor']` | Derived from the path | Supplies exact node-local port anchors instead of sampled anchors. |
| `boundaryPoint` | `ShapeDefinition['boundaryPoint']` | Derived from the path | Supplies exact world-space floating attachments instead of sampled boundaries. |
| `innerRect` | `ShapeDefinition['innerRect']` | Padded bounding box | Gives the label a box inside a slanted or concave silhouette. |

## Pitfalls and registration lifetime

- An edge template owns the visible edge, including any markers and labels you want it to show. Setting `arrowHead` on the pipe edge does not add the built-in marker rendering to its output. The example puts the feather on a separate, non-templated edge.
- Use a distinct custom connector name. The built-in connector names use internal rendering branches; registering one of those names does not replace that branch.
- Global registration functions share their names across diagrams. Prefix your notation's names to avoid replacing another extension. Shape, edge-template, label-template, marker and port-layout registration functions return `void`; anchor, connection-point and connector registration functions return a disposer that restores the previous definition. Run disposers when the owning extension unmounts, not immediately after registration.
- For shape, marker, template and link-pipeline definitions private to one diagram, use the corresponding methods on `instance.registry`. Those methods return restoring disposers and resolve local definitions before global ones. Port layouts use the global port-layout registry.

## Related

- [Validate port connections](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/validate-port-connections) — connection rules rather than port geometry.
- [JavaScript: elements and content](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/javascript-elements-and-content) — HTML content inside engine-positioned node hosts.
- [Save and restore documents](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/save-and-restore-documents) — persist the model; register the named geometry in the application that renders it.
