Extension

Custom Shapes with Telva Core

Build low-level shape systems with Renderer and TLShapeUtil, then scale to advanced state-machine driven editors.

Custom Shapes with Telva Core

When you need custom behavior beyond stock Telva tools, use telva-core directly.

Core primitives

From telva-core:

  • Renderer
  • TLShapeUtil
  • page and event types (TLPage, TLPageState, TLPointerEventHandler, ...)

Minimal shape utility (core-example pattern)

import { TLBounds, TLShapeUtil } from 'telva-core'
import type { RectShape } from './RectShape'

export class RectUtil extends TLShapeUtil<RectShape, SVGSVGElement> {
  Component = RectComponent
  Indicator = RectIndicator

  getBounds = (shape: RectShape) => {
    const [x, y] = shape.point
    const [width, height] = shape.size

    return {
      minX: x,
      maxX: x + width,
      minY: y,
      maxY: y + height,
      width,
      height,
    } as TLBounds
  }
}

Then wire it into renderer:

<Renderer
  shapeUtils={{ rect: new RectUtil() }}
  page={page}
  pageState={pageState}
  onPointShape={onPointShape}
  onDragShape={onDragShape}
  onPointCanvas={onPointCanvas}
  onPointerMove={onPointerMove}
  meta={{ isDarkMode: false }}
  theme={theme}
/>

Advanced shape architecture (core-example-advanced pattern)

The advanced example introduces:

  • abstract CustomShapeUtil contract (getShape, transform, hit tests, center)
  • multiple shape types (box, arrow, pencil)
  • explicit shape-utils map + getShapeUtils resolver
  • app state machine orchestration (@state-designer/react)

Abstract util contract

export abstract class CustomShapeUtil<T extends TLShape, E extends Element = Element>
  extends TLShapeUtil<T, E> {
  canBind = false
  hideBounds = false

  abstract getCenter(shape: T): number[]
  abstract getShape(shape: Partial<T>): T
  abstract transform(shape: T, bounds: TLBounds, initialShape: T, scale: number[]): void
  abstract hitTestPoint(shape: T, point: number[]): boolean
  abstract hitTestLineSegment(shape: T, A: number[], B: number[]): boolean
}

State-machine driven event routing

In advanced setups, renderer events are forwarded to a state machine:

const onPointShape: TLPointerEventHandler = (info) => {
  machine.send('POINTED_SHAPE', info)
}

const onPointerMove: TLPointerEventHandler = (info) => {
  machine.send('MOVED_POINTER', info)
}

const onPan: TLWheelEventHandler = (info) => {
  machine.send('PANNED', info)
}

This enables complex interaction modes (transforming, brush-selecting, creating tools, undo/redo) outside the renderer internals.

Choosing extension level

GoalPreferred path
Add rich visual blocks in Telva editorreactComponents registry in telva
Keep default tools and add host automationTelvaApp commands + callbacks
Implement entirely custom interaction grammartelva-core + custom shape utils + custom state machine

On this page