@mocanvas/editor · class
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.
On this page
Methods · 28
canBind(_opts: BindingCanBindOptions): booleanWhether 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): booleancanDropShapes(_shape: T, _shapes: UnknownShape[]): booleancanEdit(_shape: T, _info?: TLEditStartInfo): booleanWhether 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): booleancanRemoveChildrenOfType(_shape: T, _type: string): booleanWhether 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): booleancanScroll(_shape: T): booleancanSnap(_shape: T): booleanabstract component(shape: T): ReactNodestatic configure<C extends ShapeUtilClass>(this: C, options: ShapeUtilOptionsPatch<C>): CThis 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[] | undefinedThe 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): Geometry2dgetRenderStyle(_shape: T): StyleWords | nullGPU 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 | undefinedThe 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): booleanWhether 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): booleanhideRotateHandle(_shape: T): booleanhideSelectionBoundsBg(_shape: T): booleanhideSelectionBoundsFg(_shape: T): booleanisAspectRatioLocked(_shape: T): booleanisClipShape(_shape: T): booleanWhether 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): booleanWhether 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): booleanWhether the shape should be drawn by the DOM overlay even if it has a render style (e.g. while editing).
providesBackgroundForChildren(_shape?: T): booleanWhether 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: EditorgetAriaDescriptor?(shape: T): string | undefinedA short description of the shape for assistive technology, when its ShapeUtil.getText is not already the whole story.
getBoundsSnapGeometry?(shape: T): BoundsSnapGeometryThe 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 | undefinedA 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> | undefinedOverride *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 | undefinedDescribe 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): HandleSnapGeometryWhere 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): TLIndicatorPathResultThe 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?: PropsMigrationsHow 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 | voidonBeforeUpdate?(prev: T, next: T): T | voidonChildrenChange?(shape: T): Partial<UnknownShape>[] | voidonClick?(shape: T): Partial<T> | voidA 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> | voidonDoubleClickEdge?(shape: T): Partial<T> | voidonDoubleClickHandle?(shape: T, handle: ShapeHandle): Partial<T> | voidonDragShapesIn?(shape: T, shapes: UnknownShape[], info?: TLDragShapesInInfo): voidShapes were dragged *into* this container and should be adopted.
onDragShapesOut?(shape: T, shapes: UnknownShape[], info?: TLDragShapesOutInfo): voidonDragShapesOver?(shape: T, shapes: UnknownShape[], info?: TLDragShapesOverInfo): voidonDropShapesOver?(shape: T, shapes: UnknownShape[], info?: TLDropShapesOverInfo): voidonDuplicate?(source: T, duplicate: T): Partial<T> | voidThe 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): voidonHandleDrag?(shape: T, info: TLHandleDragInfo<T>): Partial<T> | voidonReleaseContentElement?(shape: T, element: HTMLElement): voidThe 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> | voidonResizeEnd?(initial: T, current: T): voidonResizeStart?(shape: T): voidonRotate?(initial: T, current: T): Partial<T> | voidonRotateEnd?(initial: T, current: T): voidonRotateStart?(shape: T): voidonTranslate?(initial: T, current: T): Partial<T> | voidonTranslateEnd?(initial: T, current: T): Partial<T> | voidonTranslateStart?(shape: T): Partial<T> | voidstatic options?: objectThe 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?: UnknownRecordPropsOne 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): ShapeSvgResultExtra 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): ShapeSvgResultThe 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: stringmocanvas 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.