@mocanvas/editor · class

ShapeUtil

declare abstract class ShapeUtil<T extends UnknownShape = UnknownShape, D extends object = TLDefaultDisplayValues>

This one ships without a description. The signature above is read from the package, and the guides cover the area it belongs to.


Methods · 28

canBind(_opts: BindingCanBindOptions): boolean

Whether a binding may attach to this shape.

Takes the **records** rather than their type names (v5): read .type off them for the old behaviour, or anything else the decision needs — a locked target, a prop, a parent — without a second lookup.

canCrop(_shape: T): boolean
canDropShapes(_shape: T, _shapes: UnknownShape[]): boolean
canEdit(_shape: T, _info?: TLEditStartInfo): boolean

Whether this shape may be edited in place, and — since v5 — whether it may be edited *for this reason*. See TLEditStartInfo.

info is optional so a caller with no opinion can keep asking the plain question; it then reads as UNKNOWN_EDIT_START_INFO.

canReceiveNewChildrenOfType(_shape: T, _type: string): boolean
canRemoveChildrenOfType(_shape: T, _type: string): boolean

Whether a child of this type may be *removed* from this container.

Deliberately separate from ShapeUtil.canReceiveNewChildrenOfType: admission is allowed to be strict while recovery stays permissive, so a child that arrived from an older snapshot can always be lifted back out.

canResize(_shape: T): boolean
canScroll(_shape: T): boolean
canSnap(_shape: T): boolean
abstract component(shape: T): ReactNode
static configure<C extends ShapeUtilClass>(this: C, options: ShapeUtilOptionsPatch<C>): C

This util with options merged over its defaults, as a new class.

Nothing is mutated: the original util keeps its own options, so two editors on one page can register differently configured copies of the same shape type. type, props and migrations are inherited, so the copy is the same shape as far as the store is concerned.

``ts const utils = [GeoShapeUtil.configure({ customGeoTypes: { cog } })] ``

constructor(editor: Editor)
getClipPath(_shape?: T): VecLike[] | undefined

The region descendants are clipped to, as a polygon in **shape-local** coordinates, or undefined for "clips nothing".

This is the difference between an artboard and a region of the board: a frame crops what hangs over its edge, a section groups what sits inside it and crops nothing. Returning undefined is the whole of the second behaviour.

abstract getDefaultProps(): T["props"]
abstract getGeometry(shape: T): Geometry2d
getRenderStyle(_shape: T): StyleWords | null

GPU style for the shape's geometry. Return null (the default) to render the shape through component in the DOM overlay instead.

getText(_shape: T): string | undefined

The shape's text, for search, accessibility and the selection context. Concrete rather than optional so a caller need not guard every call; the base returns undefined for a shape that carries no text.

hasOverlayLabel(_shape: T): boolean

Whether component should be rendered in the DOM overlay *in addition to* the GPU geometry (e.g. a text label on a filled shape).

hideResizeHandles(_shape: T): boolean
hideRotateHandle(_shape: T): boolean
hideSelectionBoundsBg(_shape: T): boolean
hideSelectionBoundsFg(_shape: T): boolean
isAspectRatioLocked(_shape: T): boolean
isClipShape(_shape: T): boolean

Whether the shape clips its descendants to its own geometry bounds (e.g. frames). Children are then rendered with a scissor rect on the GPU.

isFrameLike(_shape: T): boolean

Whether this shape is a *container*: something that adopts the shapes dragged onto it and carries them when it moves.

The one question every "is this a frame?" test should ask. Hardcoding shape.type === "frame" is what this replaces: an app's own section, phase or artboard shape is frame-like too, and nothing in the editor can know their type names.

Being frame-like says nothing about *clipping* — see ShapeUtil.getClipPath, which a grouping container returns undefined from while staying frame-like.

needsOverlay(shape: T): boolean

Whether the shape should be drawn by the DOM overlay even if it has a render style (e.g. while editing).

providesBackgroundForChildren(_shape?: T): boolean

Whether this shape paints a surface its children sit on, and so must be drawn behind them rather than interleaved by index.

get type(): T["type"]

Properties · 43

readonly editor: Editor
getAriaDescriptor?(shape: T): string | undefined

A short description of the shape for assistive technology, when its ShapeUtil.getText is not already the whole story.

getBoundsSnapGeometry?(shape: T): BoundsSnapGeometry

The points this shape offers when something is dragged near it.

Omit it, or return no points, to contribute the four corners and the centre of the shape's bounds — what almost every shape wants. Returning an *empty* array says something different: this shape is not a snap target at all.

Answered in shape-local coordinates; the snap manager applies the page transform.

getContentElement?(shape: T): HTMLElement | undefined

A DOM element that holds this shape's **stateful** content — a cross-origin iframe, a <video> mid-playback, a third-party widget that authenticated once and cannot cheaply do it again.

component() re-runs whenever React feels like it, and anything it *creates* is destroyed and rebuilt with it: an iframe reloads, a video restarts, a session is lost. An element returned from here is created once per shape, kept by the editor rather than by the React tree, and moved between mount points instead of being recreated — so it survives the shape scrolling out of view, a re-render, and the editor's own unmount.

The element is *positioned and sized* by the editor. Return a container and put the stateful thing inside it; do not read layout off it.

Implement it and component() no longer needs to render the content at all — whatever it returns is rendered *around* the element, which is appended as a child of the shape's node.

getCustomDisplayValues?(editor: unknown, shape: T, theme: TLTheme, colorMode: TLColorMode): Partial<D> | undefined

Override *some* of the display values this util resolves, leaving the rest to ShapeUtilOptions.getDefaultDisplayValues.

The override point for a shape whose paint depends on more than its style props — a note that tints itself by the board's mode, a card that reads a colour off its own props. Return undefined to change nothing.

editor is typed unknown to match TLGetCustomDisplayValues, which is what getDisplayValues reads this through; inside a util this.editor is the same object, fully typed, so the parameter is rarely the one you want.

getEngineGeometry?(shape: T): EngineGeometry | undefined

Describe this shape's outline to the engine by its *parameters*, so the engine builds it, instead of constructing a Geometry2d here and uploading its vertices.

Returning undefined — the default, and what every custom shape does — keeps the getGeometry path. A built-in whose silhouette the engine already knows how to draw returns a descriptor instead: at 20,000 shapes that is 15-25x less work on the JavaScript side and up to 4.6x fewer words copied across the boundary, because a rectangle travels as (kind, w, h) rather than as its vertices.

w, h, isClosed and isFilled must agree with what getGeometry would have reported: they are what the scene is told about the shape.

getFontFaces?(shape: T): TLFontFace[]

The typefaces this shape needs before it can be drawn correctly.

The font manager collects these across the current page and loads them; until a face has loaded the shape is measured and painted with whatever the browser substitutes, so a util that draws text should answer here rather than assume its family is present.

Returning an empty array (the default when unimplemented) means "nothing to load" — a shape drawn entirely from geometry.

getHandles?(shape: T): ShapeHandle[]
getHandleSnapGeometry?(shape: T): HandleSnapGeometry

Where a dragged handle may land on this shape: discrete points to click onto, an outline to slide along, or both.

Also in shape-local coordinates.

getIndicatorPath?(shape: T): TLIndicatorPathResult

The outline drawn over the shape when it is selected, hovered or hinted, in **shape-local** coordinates, as a canvas path.

The compositor supplies everything else: it applies the shape's page transform, strokes in the theme's selection colour, and picks a zoom-independent width (1.5 CSS px selected or hovered, 2.5 hinted). So an indicator is usually three lines:

``ts override getIndicatorPath(shape: MyShape): Path2D { ``

Return a TLIndicatorPath instead of a bare Path2D to punch a hole in the outline (a label strip) or to add strokes outside that hole. Return undefined, or an empty path, to draw no outline at all.

Leaving the method unimplemented falls back to a rectangle around the shape's geometry bounds.

static handledAssetTypes?: readonly string[]

Asset types this shape can be created for — ["image"] on an image shape.

It is how a dropped file finds its shape: the asset utils turn the file into an asset record, and the editor then looks for the shape util that claims that asset's type and asks it to ShapeUtil.createShapeForAsset. Leaving it undefined means "no asset ever produces this shape", which is right for every shape that is drawn rather than dropped.

indicator?(shape: T): ReactNode

@deprecated Implement ShapeUtil.getIndicatorPath instead.

The v4 indicator: an SVG fragment in shape-local coordinates, drawn on a separate SVG layer. Still honoured for a util that has not been ported — but only when that util does *not* implement getIndicatorPath, so a util mid-port is never drawn twice.

static migrations?: PropsMigrations

How this shape's props have changed over time, as a PropsMigrations sequence. A shape that is ever persisted should declare one from the start, even empty: the first prop that is added later then has somewhere to go.

onBeforeCreate?(next: T): T | void
onBeforeUpdate?(prev: T, next: T): T | void
onChildrenChange?(shape: T): Partial<UnknownShape>[] | void
onClick?(shape: T): Partial<T> | void

A single, clean click on the shape.

Its mere *presence* is a semantic switch: a util that implements it moves selection from pointer-down to pointer-up, so the handler runs before the shape is selected and a drag (which becomes a translation before a click can complete) never triggers it. That is why implementing it conditionally — a prototype accessor answering undefined for editors that have no use for it — is a supported way to use it.

It cannot swallow the event: returning nothing lets the pointer-up selection run as usual, and there is no "handled" answer to give. Return a shape partial to change the shape as well.

onDoubleClick?(shape: T): Partial<T> | void
onDoubleClickEdge?(shape: T): Partial<T> | void
onDoubleClickHandle?(shape: T, handle: ShapeHandle): Partial<T> | void
onDragShapesIn?(shape: T, shapes: UnknownShape[], info?: TLDragShapesInInfo): void

Shapes were dragged *into* this container and should be adopted.

onDragShapesOut?(shape: T, shapes: UnknownShape[], info?: TLDragShapesOutInfo): void
onDragShapesOver?(shape: T, shapes: UnknownShape[], info?: TLDragShapesOverInfo): void
onDropShapesOver?(shape: T, shapes: UnknownShape[], info?: TLDropShapesOverInfo): void
onDuplicate?(source: T, duplicate: T): Partial<T> | void

The shape was duplicated. source is the original and duplicate the new record — new id, new parent, offset already applied. Return a props partial to amend the copy: renaming it, clearing an id that must be unique per instance, re-seeding randomness that would otherwise repeat.

onEditEnd?(shape: T): void
onHandleDrag?(shape: T, info: TLHandleDragInfo<T>): Partial<T> | void
onReleaseContentElement?(shape: T, element: HTMLElement): void

The editor has finished with the element getContentElement returned: the shape was deleted, or the editor was disposed. Tear down whatever the element owns — a player, an observer, a message channel.

Not called when the shape merely unmounts, which is the entire point of the pair. shape is the last version of the shape the editor saw, and may already be gone from the store.

onResize?(shape: T, info: ResizeInfo<T>): Partial<T> | void
onResizeEnd?(initial: T, current: T): void
onResizeStart?(shape: T): void
onRotate?(initial: T, current: T): Partial<T> | void
onRotateEnd?(initial: T, current: T): void
onRotateStart?(shape: T): void
onTranslate?(initial: T, current: T): Partial<T> | void
onTranslateEnd?(initial: T, current: T): Partial<T> | void
onTranslateStart?(shape: T): Partial<T> | void
static options?: object

The defaults every instance of this util starts its options from.

readonly options: ShapeUtilOptions<T, D>

This util's settings; see ShapeUtilOptions and ShapeUtil.configure.

static props?: UnknownRecordProps

One validator per prop of the shape this util describes — the contract the store checks a record against before it is written, and what createSchema() reads to build the document schema.

A StyleProp counts as a validator and additionally makes the prop *shared*: changing it on one selected shape changes it on all of them.

toBackgroundSvg?(shape: T, ctx: ShapeSvgContext): ShapeSvgResult

Extra SVG drawn *behind* every exported shape (a backdrop, a drop shadow, a grid). Same coordinate space and wrapping as toSvg.

toSvg?(shape: T, ctx: ShapeSvgContext): ShapeSvgResult

The shape as SVG, in shape-local coordinates: the exporter wraps the result in a <g> carrying the shape's page transform and opacity. Implement it to make a custom shape exportable without registering anything; leave it undefined to fall through to the exporter's own renderers.

static type: string

mocanvas is built by Symbio. Free for non-commercial, non-profit and development use; commercial use needs an agreement. Not affiliated with or endorsed by tldraw.