Documentation Index
Fetch the complete documentation index at: https://mintlify.com/tldraw/tldraw/llms.txt
Use this file to discover all available pages before exploring further.
GeoShapeUtil handles geometric shapes including rectangles, ellipses, triangles, stars, and more, with support for rich text labels.
Type signature
class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape>
Features
- 17+ geometric shapes
- Rich text labels with auto-sizing
- Vertical text growth (growY)
- Configurable alignment
- Fill patterns and colors
- Optional URLs for linking
Configuration options
interface GeoShapeOptions {
showTextOutline: boolean // default: true
}
Default props
getDefaultProps(): TLGeoShape['props'] {
return {
w: 100,
h: 100,
geo: 'rectangle',
dash: 'draw',
growY: 0,
url: '',
scale: 1,
color: 'black',
labelColor: 'black',
fill: 'none',
size: 'm',
font: 'draw',
align: 'middle',
verticalAlign: 'middle',
richText: toRichText(''),
}
}
Properties
The geometric shape type:
'rectangle'
'ellipse'
'triangle'
'diamond'
'pentagon'
'hexagon'
'octagon'
'star'
'rhombus'
'rhombus-2'
'oval'
'trapezoid'
'arrow-right'
'arrow-left'
'arrow-up'
'arrow-down'
'x-box'
'check-box'
'cloud'
'heart'
Additional vertical space added when text overflows.
Rich text content for the label.
align
'start' | 'middle' | 'end'
default:"'middle'"
Horizontal text alignment.
verticalAlign
'start' | 'middle' | 'end'
default:"'middle'"
Vertical text alignment.
Optional URL for hyperlink.
Scale factor for the shape.
Geometry
Geo shapes have composite geometry including the shape outline and label area:
getGeometry(shape: TLGeoShape) {
const path = getGeoShapePath(shape)
const pathGeometry = path.toGeometry()
const labelBounds = getLabelBounds(/* ... */)
return new Group2d({
children: [
pathGeometry,
new Rectangle2d({
...labelBounds,
isFilled: true,
isLabel: true,
excludeFromShapeBounds: true,
isEmptyLabel: isEmptyRichText(props.richText),
}),
],
})
}
Methods
canEdit()
canEdit() {
return true
}
Geo shapes can be double-clicked to edit text.
getText()
Returns plaintext version of the rich text label:
getText(shape: TLGeoShape) {
return renderPlaintextFromRichText(this.editor, shape.props.richText)
}
getHandleSnapGeometry()
Returns appropriate snap points based on geo type:
getHandleSnapGeometry(shape: TLGeoShape): HandleSnapGeometry {
const geometry = this.getGeometry(shape)
const outline = geometry.children[0]
switch (shape.props.geo) {
case 'rectangle':
case 'diamond':
case 'hexagon':
// Polygons: snap to vertices + center
return {
outline: outline,
points: [...outline.vertices, geometry.bounds.center]
}
case 'ellipse':
case 'cloud':
// Curved shapes: snap to center only
return {
outline: outline,
points: [geometry.bounds.center]
}
}
}
onResize()
Handles resizing with label size constraints:
onResize(
shape: TLGeoShape,
{ handle, newPoint, scaleX, scaleY, initialShape }: TLResizeInfo<TLGeoShape>
) {
// Calculate new dimensions
let unscaledW = unscaledInitial.w * scaleX
let unscaledH = (unscaledInitial.h + unscaledInitial.growY) * scaleY
// If shape has text, ensure it doesn't shrink below label size
if (!isEmptyRichText(shape.props.richText)) {
const unscaledLabelSize = measureUnscaledLabelSize(this.editor, shape)
const constrainedW = Math.max(absUnscaledW, unscaledLabelSize.w)
const constrainedH = Math.max(absUnscaledH, unscaledLabelSize.h)
// ... apply constraints
}
return { x, y, props: { w, h, growY: 0 } }
}
onBeforeCreate()
Ensures label fits when creating:
onBeforeCreate(shape: TLGeoShape) {
if (isEmptyRichText(props.richText)) {
return props.growY !== 0 ? { ...shape, props: { ...props, growY: 0 } } : undefined
}
const unscaledGrowY = calculateGrowY(unscaledShapeH, unscaledLabelH, props.growY / props.scale)
if (unscaledGrowY !== null) {
return { ...shape, props: { ...props, growY: unscaledGrowY * props.scale } }
}
}
onBeforeUpdate()
Adjusts dimensions when text changes:
onBeforeUpdate(prev: TLGeoShape, next: TLGeoShape) {
// Skip if text unchanged
if (isEqual(prev.props.richText, next.props.richText) &&
prev.props.font === next.props.font &&
prev.props.size === next.props.size) {
return undefined
}
const isEmpty = isEmptyRichText(next.props.richText)
if (isEmpty) {
return next.props.growY !== 0 ? { ...next, props: { ...next.props, growY: 0 } } : undefined
}
// Measure label and adjust dimensions
const unscaledLabelSize = getUnscaledLabelSize(this.editor, next)
// ... calculate new w, h, growY
}
onDoubleClick()
Easter egg: Alt + double-click toggles rectangle ↔ checkbox:
onDoubleClick(shape: TLGeoShape) {
if (this.editor.inputs.getAltKey()) {
switch (shape.props.geo) {
case 'rectangle':
return { ...shape, props: { geo: 'check-box' as const } }
case 'check-box':
return { ...shape, props: { geo: 'rectangle' as const } }
}
}
}
Indicator
Uses canvas-based rendering:
useLegacyIndicator() {
return false
}
getIndicatorPath(shape: TLGeoShape): Path2D | undefined {
const path = getGeoShapePath(shape)
return path.toPath2D({
style: dash === 'draw' ? 'draw' : 'solid',
strokeWidth: 1,
// ...
})
}
Example: Create geo shapes
// Create a triangle
editor.createShape({
type: 'geo',
props: {
geo: 'triangle',
w: 100,
h: 100,
fill: 'solid',
color: 'blue',
}
})
// Create a star with text
editor.createShape({
type: 'geo',
props: {
geo: 'star',
w: 150,
h: 150,
richText: toRichText('Important!'),
color: 'yellow',
fill: 'pattern',
}
})
Example: Custom configuration
import { GeoShapeUtil } from 'tldraw'
const CustomGeoUtil = GeoShapeUtil.configure({
showTextOutline: false,
})