Skip to content

[pull] main from tldraw:main#442

Merged
pull[bot] merged 3 commits intocode:mainfrom
tldraw:main
Mar 12, 2026
Merged

[pull] main from tldraw:main#442
pull[bot] merged 3 commits intocode:mainfrom
tldraw:main

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented Mar 12, 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 : )

steveruizok and others added 3 commits March 11, 2026 23:22
In order to fix over-softened corners and end artifacts when
shift-clicking to draw straight line segments, this PR adds an easing
parameter to `Vec.PointsBetween` and reworks the straight segment
interpolation in `getPointsFromDrawSegment`.

<img width="1348" height="990" alt="image"
src="https://github.com/user-attachments/assets/f16522d7-92e9-432f-ae07-d1233ae5cb55"
/>

Closes #7867

### Change type

- [x] `bugfix`

### Test plan

1. Select the draw tool
2. Click to start, then hold shift and click at several points to create
straight line segments
3. Verify corners between segments are sharper and endpoints don't have
rendering artifacts
4. Verify freehand drawing still works normally

### Release notes

- Fix over-softened corners when shift-clicking to draw straight line
segments

### API changes

- Changed `Vec.PointsBetween()` to accept an optional `ease` parameter
for custom easing curves


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes stroke interpolation math for straight draw segments and
updates a shared `Vec.PointsBetween` API, which could subtly affect
rendering/pressure profiles across call sites.
> 
> **Overview**
> Improves rendering of shift-click straight draw segments by changing
how points are interpolated in `getPointsFromDrawSegment`: straight
segments now add small endpoint nudges and interpolate between inner
points with `EASINGS.easeInOutCubic` to reduce softened corners and end
artifacts.
> 
> Extends `Vec.PointsBetween` to accept an optional `ease` function
(defaulting to `EASINGS.easeInQuad`) and updates its internals to use
the provided easing for position interpolation while keeping pressure
(`z`) simulation based on `easeInOutQuad`; the API report is updated
accordingly.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
d527883. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: huppy-bot[bot] <128400622+huppy-bot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Max Drake <maxdrake46@gmail.com>
In order to improve pointer event throughput during hit testing, this PR
reduces allocations, unnecessary square roots, and function call
overhead across the geometry and intersection primitives. These are all
hot paths called 2-4x per shape on every pointer move via
`Editor.getShapeAtPoint()`.

### Change type

- [x] `improvement`

### Benchmark results (original → optimized)

| Function | Before (M ops/s) | After (M ops/s) | Speedup |
|---|---|---|---|
| `Vec.NearestPointOnLineSegment` | 26.6 | 44.1 | **1.7x** |
| `Vec.DistanceToLineSegment` | 25.5 | 43.9 | **1.7x** |
| `Edge2d.nearestPoint` | 34.3 | 42.7 | **1.2x** |
| `Edge2d.distanceToPoint` | 25.6 | 48.6 | **1.9x** |
| `Circle2d.distanceToPoint` | 2.6 | 49.1 | **19x** |
| `Circle2d.hitTestPoint` | 2.8 | 48.7 | **17x** |
| `Arc2d.nearestPoint` | 13.2 | 20.5 | **1.6x** |
| `Polyline2d.nearestPoint` | 2.6 | 17.6 | **6.8x** |
| `Polyline2d.distanceToPoint` | 2.6 | 13.1 | **5x** |
| `Rect.hitTestPoint (outside)` | 5.1 | 23.6 | **4.6x** |
| `Rect.hitTestPoint (inside)` | 9.6 | 23.3 | **2.4x** |
| `Rect.distanceToPoint` | 4.8-5.2 | 21.6-22.2 | **4.3x** |
| `intersectLineSegmentLineSegment (hit)` | 13.3 | 37.7 | **2.8x** |
| `intersectLineSegmentLineSegment (miss)` | 21.7 | 43.5 | **2.0x** |
| `intersectLineSegmentCircle (hit)` | 19.5 | 22.0 | **1.13x** |

### What changed

**Vec.ts** — Rewrite `NearestPointOnLineSegment` and
`DistanceToLineSegment` with parametric t-projection (no intermediate
Vec allocations). Inline `Lrp`, `NearestPointOnLineThroughPoint`,
`DistanceToLineThroughPoint`, and `AngleBetween`.

**Edge2d** — Precompute `dx`, `dy`, `len2` in constructor. Rewrite
`nearestPoint` with parametric clamping. Add `distanceToPoint` override
(0 allocs).

**Circle2d** — Inline `nearestPoint` (1 alloc, 1 sqrt). Add
`distanceToPoint` override (0 allocs). Add `hitTestPoint` override (0
allocs, 0 sqrts).

**Arc2d** — Remove unnecessary A/B/P distance loop when t is in range.

**Polyline2d** — Inline edge math in `nearestPoint` (N allocs → 1). Add
`distanceToPoint` and `hitTestPoint` overrides using scalar fast path.

**CubicBezier2d / CubicSpline2d / Ellipse2d / Stadium2d** — Add
`distanceToPoint` overrides using Edge2d's scalar fast path.

**pointInPolygon** — Remove `Vec.Dist(A,a) + Vec.Dist(A,b) ===
Vec.Dist(a,b)` edge check (3 sqrts/vertex, exact float equality never
fires).

**intersect.ts** — Inline `approximately`/`approximatelyLte` in
`intersectLineSegmentLineSegment` (7+ function calls → inline
comparisons). Simplify `intersectLineSegmentCircle` with precomputed
dx/dy.

### API changes

- Added `Circle2d.distanceToPoint()`, `Circle2d.hitTestPoint()`
- Added `Polyline2d.distanceToPoint()`, `Polyline2d.hitTestPoint()`
- Added `CubicBezier2d.distanceToPoint()`,
`CubicSpline2d.distanceToPoint()`
- Added `Ellipse2d.distanceToPoint()`, `Stadium2d.distanceToPoint()`

### Code changes

| Section | LOC change |
|---|---|
| Core code | +272 / -95 |
| Tests | +179 / -0 |
| Automated files | +18 / -0 |

### Test plan

1. Draw shapes of all types, verify hit testing works correctly
2. Test selection, hovering, brush selection with various shape types
3. Verify arrow binding snaps to shapes properly

- [x] Unit tests (764 editor + 131 tldraw pass)
- [x] Vitest benchmarks

### Release notes

- Improve hit testing performance across all geometry types (up to 19x
faster for circles, 5-7x for polylines/rectangles, 2-3x for line segment
intersections)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches core geometry hit-testing math
(distance/nearest-point/intersections) across multiple primitives, so
subtle numerical/edge-case regressions are possible despite being
performance-oriented.
> 
> **Overview**
> **Optimizes geometry and intersection hot paths used during hit
testing** by rewriting vector/segment math to use scalar projections and
fewer allocations/square roots.
> 
> Adds fast-path `distanceToPoint`/`hitTestPoint` overrides for key
shapes (`Circle2d`, `Polyline2d`, `Edge2d`, plus `CubicBezier2d`,
`CubicSpline2d`, `Ellipse2d`, `Stadium2d`) and simplifies
`Arc2d.nearestPoint` and segment/circle & segment/segment intersection
computations.
> 
> Cleans up `pointInPolygon` to remove an expensive edge-distance
equality check, updates the public API report accordingly, and
introduces a new `vitest` benchmark suite (`geometry.bench.ts`) to track
these performance-critical operations.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
1f53610. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…ditor-controller package (#7952)

In order to make the imperative editor-driving API available for REPL,
agent/MCP, scripting, and automation use cases (not just tests), this PR
extracts the framework-agnostic imperative surface from `TestEditor`
into a new `@tldraw/editor-controller` package. Closes #7950.

The `EditorController` class wraps an `Editor` instance (composition,
not inheritance) and provides:
- **Event dispatch** — `pointerMove/Down/Up`, `click`, `rightClick`,
`doubleClick`, `keyPress/Down/Up/Repeat`, `wheel`, `pan`,
`pinchStart/To/End`, `forceTick`
- **Selection transforms** — `rotateSelection`, `translateSelection`,
`resizeSelection`
- **Clipboard** — `copy`, `cut`, `paste` (internal, not browser)
- **Shape queries** — `getViewportPageCenter`, `getSelectionPageCenter`,
`getPageCenter`, `getPageRotation(ById)`, `getArrowsBoundTo`
- **ID helpers** — `testShapeID`, `testPageID`

All methods use only public Editor APIs and return `this` for fluent
chaining.

`TestEditor` now delegates all extracted methods to a composed
`EditorController` instance while keeping test-specific concerns (vitest
mocks, assertions, DOM setup, `createShapesFromJsx`) local.

A **Scripter example** is included to demonstrate the package: a
Scripter-style scripting environment with a resizable script list, code
editor, run button (⌘+Enter), and console output panel. Ships with
starter scripts for creating shapes, translating selections, randomizing
colors, and more.

### Change type

- [x] `feature`

### Test plan

- All 2020+ existing tldraw tests pass unchanged (the delegation is
transparent)
- Run the Scripter example at `localhost:5420` and execute starter
scripts against the canvas

- [x] Unit tests

### Release notes

- Add `@tldraw/editor-controller` package: an imperative API for driving
the tldraw editor programmatically, useful for scripting, automation,
and agent workflows.
- Add Scripter example demonstrating `EditorController` usage with an
interactive script editor.

### API changes

- Added `@tldraw/editor-controller` package with `EditorController`
class, `PointerEventInit` type, and `EventModifiers` type

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds a new public package and rewires `TestEditor` input/clipboard
helpers to delegate to it, which could subtly affect test interactions
and event simulation behavior. The change is mostly additive but touches
core test harness utilities used broadly across the suite.
> 
> **Overview**
> Introduces a new `@tldraw/driver` package that exposes a fluent,
imperative API for driving an `Editor` (pointer/keyboard/wheel/pinch
events, camera pan, clipboard ops, selection transforms, and
shape/selection queries), including API extractor config/report and
docs.
> 
> Updates `tldraw` and `examples` to depend on and reference the new
package, and refactors `TestEditor` to compose a `Driver` instance and
delegate the previously inlined event/clipboard/selection helper methods
to it. As follow-ups, a large set of tests are adjusted to stop using
`test-jsx` shape creation helpers (switching to `editor.createShapes`)
and to access clipboard via `getClipboard()` instead of a direct
`clipboard` field, plus a few test helper imports are moved to
`lib/utils/test-helpers`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
60b2432. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
@pull pull Bot locked and limited conversation to collaborators Mar 12, 2026
@pull pull Bot added the ⤵️ pull label Mar 12, 2026
@pull pull Bot merged commit 84ac7a3 into code:main Mar 12, 2026
@pull pull Bot had a problem deploying to deploy-production March 12, 2026 03:13 Failure
@pull pull Bot had a problem deploying to deploy-staging March 12, 2026 03:13 Error
@pull pull Bot had a problem deploying to deploy-staging March 12, 2026 03:13 Failure
@pull pull Bot had a problem deploying to vsce publish March 12, 2026 03:13 Failure
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.

1 participant