Skip to content

[pull] main from tldraw:main#508

Merged
pull[bot] merged 3 commits into
code:mainfrom
tldraw:main
Apr 22, 2026
Merged

[pull] main from tldraw:main#508
pull[bot] merged 3 commits into
code:mainfrom
tldraw:main

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented Apr 22, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

mimecuvalo and others added 3 commits April 22, 2026 08:58
In order to allow SDK consumers to add custom geo types without forking
or monkey-patching GeoShapeUtil, this PR adds a `GeoTypeDefinition`
interface and a `customGeoTypes` option to `GeoShapeUtil.configure()`.
Closes #8076.

Custom geo types plug into the existing switch statements and inherit
all standard geo shape behavior — labels, resizing, fill/dash/color
styling, SVG export, hyperlink support, and the full editing UX — while
providing their own path geometry, snap behavior, creation size, style
panel icon, and optional double-click handler.

### Concepts

| Term | Type | Meaning |
|------|------|---------|
| `GeoTypeDefinition` | interface | Defines the behavior bundle for a
single custom geo type |
| `customGeoTypes` | `GeoShapeOptions` field | Map of custom type name →
definition, passed via `configure()` |

### Example

**Registering a custom geo type:**

```ts
import { GeoShapeUtil, PathBuilder } from 'tldraw'

const MyGeoShapeUtil = GeoShapeUtil.configure({
  customGeoTypes: {
    'rounded-rect': {
      getPath: (w, h, shape, strokeWidth) => {
        const r = Math.min(w, h) * 0.2
        return new PathBuilder()
          .moveTo(r, 0, { geometry: { isFilled: shape.props.fill !== 'none' } })
          .lineTo(w - r, 0)
          .circularArcTo(r, false, true, w, r)
          .lineTo(w, h - r)
          .circularArcTo(r, false, true, w - r, h)
          .lineTo(r, h)
          .circularArcTo(r, false, true, 0, h - r)
          .lineTo(0, r)
          .circularArcTo(r, false, true, r, 0)
          .close()
      },
      snapType: 'polygon',
      icon: 'geo-rectangle',
      defaultSize: { w: 200, h: 150 },
    },
  },
})

// Pass to Tldraw
<Tldraw shapeUtils={[MyGeoShapeUtil]} />
```

### `GeoTypeDefinition` interface

| Field | Type | Description |
|-------|------|-------------|
| `getPath` | `(w, h, shape, strokeWidth) => PathBuilder` | Generate the
path geometry for this type |
| `snapType` | `'polygon' \| 'blobby'` | `'polygon'` snaps to vertices +
center; `'blobby'` snaps to center only |
| `icon` | `string` | Icon name for the style panel geo picker |
| `defaultSize` | `{ w: number; h: number }` (optional) | Default size
when clicking (not dragging). Defaults to 200x200 |
| `onDoubleClick` | `(shape) => { props } \| void` (optional) | Custom
double-click handler |

### New examples

- `shapes/custom-geo-types` — Demonstrates registering two custom geo
types (rounded-rect and cross) via `GeoShapeUtil.configure()`, showing
how custom types inherit all standard geo behavior

### Change type

- [x] `feature`

### Test plan

1. Open the examples app (`yarn dev`) and navigate to Shapes > Custom
geo types
2. Verify the two custom shapes (rounded-rect and cross) render
correctly with labels and fill
3. Select the geo tool and open the style panel — custom types should
appear at the bottom of the geo picker
4. Click to create a shape — should use the custom `defaultSize`
5. Drag to create a shape — should resize normally
6. Verify label editing, fill/dash/color styling, and SVG export work
7. Verify snap points match the configured `snapType`

- [x] Unit tests (existing geo tests pass)

### Release notes

- Add extensibility API for custom geo shape types via
`GeoShapeUtil.configure({ customGeoTypes })`. Custom types define their
own path geometry, snap behavior, creation size, style panel icon, and
double-click handler while inheriting all standard geo shape behavior.

### API changes

- Added `GeoTypeDefinition` interface (public)
- Added `customGeoTypes` optional field to `GeoShapeOptions`
- Added `configure()` override on `GeoShapeUtil` that registers custom
types in the geo enum, path system, and style panel

### Code changes

| Section         | LOC change |
| --------------- | ---------- |
| Core code       | +152 / -10 |
| Automated files | +20 / -18  |
| Documentation   | +141 / -0  |

---------

Co-authored-by: Mitja Bezenšek <mitja.bezensek@gmail.com>
## Summary

- adds a `ShapeUtil.isFrameLike()` capability so custom shapes can opt
into frame-like behavior (paste parenting, snapping, selection, erasing,
export)
- adds a `BaseFrameLikeShapeUtil` abstract class — the easiest way to
create a custom frame-like shape, with sensible defaults for
`isFrameLike`, `providesBackgroundForChildren`,
`canReceiveNewChildrenOfType`, `getClipPath`, `onDragShapesIn`, and
`onDragShapesOut`
- routes frame-specific behaviors in the editor and tools through
`editor.getShapeUtil(shape).isFrameLike(shape)` instead of hardcoding
the `frame` shape type
- refactors `FrameShapeUtil` to extend `BaseFrameLikeShapeUtil`,
removing ~80 lines of boilerplate while preserving current frame
behavior
- adds a portal shapes example demonstrating custom frame-like shapes
that teleport children between each other

split off from #8030

Closes #7309
Closes #7518



https://github.com/user-attachments/assets/6bd0791e-c25b-48ec-bad9-37492e5c32fe


## Example

Creating a custom frame-like shape by extending
`BaseFrameLikeShapeUtil`:

```ts
class MyContainerUtil extends BaseFrameLikeShapeUtil<MyContainerShape> {
  static override type = 'my-container' as const
  static override props = myContainerShapeProps

  override getDefaultProps() {
    return { w: 300, h: 200 }
  }

  override component(shape: MyContainerShape) {
    return <SVGContainer>...</SVGContainer>
  }

  override indicator(shape: MyContainerShape) {
    return <rect width={shape.props.w} height={shape.props.h} />
  }
}
```

## Test plan

- [x] pre-commit checks pass locally
- [ ] smoke test paste / duplicate behavior for frame-like shapes
- [ ] smoke test snapping, selection, and erasing interactions
- [ ] verify the portal shapes example reparents and teleports correctly

### Change type

- [x] `feature`

### Release notes

- Add `BaseFrameLikeShapeUtil` abstract class to make it easier to build
custom frame-like shapes.
- Add `ShapeUtil.isFrameLike()` so custom shapes can opt into frame-like
behavior (clipping children, full-brush selection, blocking erasure from
inside, etc.).

### API changes

- Added `BaseFrameLikeShapeUtil` abstract class exported from
`@tldraw/editor`, extending `BaseBoxShapeUtil` with defaults for
frame-like behavior.
- Added `ShapeUtil.isFrameLike(shape)` method (returns `false` by
default) for custom shapes to opt into frame behaviors.
- `FrameShapeUtil` now extends `BaseFrameLikeShapeUtil` instead of
`BaseBoxShapeUtil`.

### Code changes

| Section         | LOC change |
| --------------- | ---------- |
| Core code       | +169 / -97 |
| Tests           | +14 / -2   |
| Automated files | +19 / -13  |
| Documentation   | +452 / -0  |
In order to speed up the `groupUsers` synced query, this PR adds indexes
on `groupId` for the `group_user` and `group_file` tables. The composite
primary keys on these tables start with `userId`/`fileId`, so filtering
by `groupId` alone falls back to full-table scans — visible in the Zero
Cache SQLite plans as `SCAN group_user USING INDEX group_user_pkey` and
`SCAN group_file USING INDEX group_file_pkey`. The new indexes let Zero
replicate them to the replica and use proper index searches.

### Change type

- [x] `improvement`

### Test plan

1. Run the migration against a dev database.
2. Open a user session that has group memberships and file associations.
3. Inspect the `groupUsers` query's `sqlitePlans` — the `group_user` and
`group_file` entries should show `SEARCH … USING INDEX` instead of `SCAN
… USING INDEX *_pkey`.

### Release notes

- Internal: add `groupId` indexes to `group_user` and `group_file` for
faster `groupUsers` sync queries.
@pull pull Bot locked and limited conversation to collaborators Apr 22, 2026
@pull pull Bot added the ⤵️ pull label Apr 22, 2026
@pull pull Bot merged commit 7fbf8e1 into code:main Apr 22, 2026
@pull pull Bot had a problem deploying to deploy-production April 22, 2026 09:13 Failure
@pull pull Bot had a problem deploying to deploy-staging April 22, 2026 09:13 Error
@pull pull Bot had a problem deploying to deploy-staging April 22, 2026 09:13 Error
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants