Skip to content

Commit 6c95444

Browse files
authored
example: add custom relative snap handle example (tldraw#7139)
Add an example to demonstrate the new `snapReferenceHandleId` property. ### Change type - [ ] `bugfix` - [ ] `improvement` - [ ] `feature` - [ ] `api` - [x] `other` <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds a new example showcasing `snapReferenceHandleId` via a custom Y-shaped shape and accompanying README. > > - **Examples**: > - **New example** `apps/examples/src/examples/custom-relative-snapping/CustomRelativeSnappingExample.tsx`. > - Defines `YShape` and `YShapeUtil` with four handles; arms use `snapReferenceHandleId: 'center'` for shift-angle snapping. > - Renders three lines from center to arms; updates arm positions on handle drag. > - On mount, creates and selects a `y-shape` at viewport center. > - **Docs**: > - Adds `README.md` with metadata and brief explanation of `snapReferenceHandleId` for control-point angle snapping. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2dd9aaf. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent fa5328c commit 6c95444

2 files changed

Lines changed: 259 additions & 0 deletions

File tree

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import {
2+
Edge2d,
3+
Geometry2d,
4+
Group2d,
5+
HTMLContainer,
6+
RecordProps,
7+
ShapeUtil,
8+
TLBaseShape,
9+
TLHandle,
10+
TLHandleDragInfo,
11+
Tldraw,
12+
Vec,
13+
VecLike,
14+
ZERO_INDEX_KEY,
15+
getIndicesAbove,
16+
vecModelValidator,
17+
} from 'tldraw'
18+
import 'tldraw/tldraw.css'
19+
20+
// [1]
21+
type YShape = TLBaseShape<
22+
'y-shape',
23+
{
24+
center: VecLike
25+
armTop: VecLike
26+
armLeft: VecLike
27+
armRight: VecLike
28+
}
29+
>
30+
31+
// [2]
32+
class YShapeUtil extends ShapeUtil<YShape> {
33+
static override type = 'y-shape' as const
34+
static override props: RecordProps<YShape> = {
35+
center: vecModelValidator,
36+
armTop: vecModelValidator,
37+
armLeft: vecModelValidator,
38+
armRight: vecModelValidator,
39+
}
40+
41+
override getDefaultProps(): YShape['props'] {
42+
return {
43+
center: { x: 100, y: 100 },
44+
armTop: { x: 100, y: 180 },
45+
armLeft: { x: 30, y: 20 },
46+
armRight: { x: 170, y: 20 },
47+
}
48+
}
49+
50+
override canEdit(): boolean {
51+
return true
52+
}
53+
54+
override hideSelectionBoundsBg(): boolean {
55+
return true
56+
}
57+
58+
override hideSelectionBoundsFg(): boolean {
59+
return true
60+
}
61+
62+
override hideResizeHandles(): boolean {
63+
return true
64+
}
65+
66+
override hideRotateHandle(): boolean {
67+
return true
68+
}
69+
70+
// [3]
71+
getGeometry(shape: YShape): Geometry2d {
72+
const { center, armTop, armLeft, armRight } = shape.props
73+
const c = Vec.From(center)
74+
const t = Vec.From(armTop)
75+
const l = Vec.From(armLeft)
76+
const r = Vec.From(armRight)
77+
78+
return new Group2d({
79+
children: [
80+
new Edge2d({ start: c, end: t }),
81+
new Edge2d({ start: c, end: l }),
82+
new Edge2d({ start: c, end: r }),
83+
],
84+
})
85+
}
86+
87+
// [4]
88+
override getHandles(shape: YShape): TLHandle[] {
89+
const indices = [ZERO_INDEX_KEY, ...getIndicesAbove(ZERO_INDEX_KEY, 3)]
90+
91+
return [
92+
{
93+
id: 'center',
94+
type: 'vertex',
95+
x: shape.props.center.x,
96+
y: shape.props.center.y,
97+
index: indices[0],
98+
},
99+
{
100+
id: 'armTop',
101+
type: 'vertex',
102+
x: shape.props.armTop.x,
103+
y: shape.props.armTop.y,
104+
index: indices[1],
105+
// [5]
106+
snapReferenceHandleId: 'center',
107+
},
108+
{
109+
id: 'armLeft',
110+
type: 'vertex',
111+
x: shape.props.armLeft.x,
112+
y: shape.props.armLeft.y,
113+
index: indices[2],
114+
// [6]
115+
snapReferenceHandleId: 'center',
116+
},
117+
{
118+
id: 'armRight',
119+
type: 'vertex',
120+
x: shape.props.armRight.x,
121+
y: shape.props.armRight.y,
122+
index: indices[3],
123+
// [7]
124+
snapReferenceHandleId: 'center',
125+
},
126+
]
127+
}
128+
129+
override onHandleDrag(shape: YShape, info: TLHandleDragInfo<YShape>) {
130+
const { handle } = info
131+
return {
132+
...shape,
133+
props: {
134+
...shape.props,
135+
[handle.id]: { x: handle.x, y: handle.y },
136+
},
137+
}
138+
}
139+
140+
// [8]
141+
component(shape: YShape) {
142+
const { center, armTop, armLeft, armRight } = shape.props
143+
144+
return (
145+
<HTMLContainer>
146+
<svg className="tl-svg-container">
147+
<line
148+
x1={center.x}
149+
y1={center.y}
150+
x2={armTop.x}
151+
y2={armTop.y}
152+
stroke="black"
153+
strokeWidth={2}
154+
/>
155+
<line
156+
x1={center.x}
157+
y1={center.y}
158+
x2={armLeft.x}
159+
y2={armLeft.y}
160+
stroke="black"
161+
strokeWidth={2}
162+
/>
163+
<line
164+
x1={center.x}
165+
y1={center.y}
166+
x2={armRight.x}
167+
y2={armRight.y}
168+
stroke="black"
169+
strokeWidth={2}
170+
/>
171+
</svg>
172+
</HTMLContainer>
173+
)
174+
}
175+
176+
indicator(shape: YShape) {
177+
const { center, armTop, armLeft, armRight } = shape.props
178+
return (
179+
<>
180+
<line x1={center.x} y1={center.y} x2={armTop.x} y2={armTop.y} />
181+
<line x1={center.x} y1={center.y} x2={armLeft.x} y2={armLeft.y} />
182+
<line x1={center.x} y1={center.y} x2={armRight.x} y2={armRight.y} />
183+
</>
184+
)
185+
}
186+
}
187+
188+
const customShapes = [YShapeUtil]
189+
190+
export default function CustomRelativeSnappingYShapeExample() {
191+
return (
192+
<div className="tldraw__editor">
193+
<Tldraw
194+
shapeUtils={customShapes}
195+
onMount={(editor) => {
196+
const viewportPageBounds = editor.getViewportPageBounds()
197+
const centerX = viewportPageBounds.center.x
198+
const centerY = viewportPageBounds.center.y
199+
200+
editor.createShape({
201+
type: 'y-shape',
202+
x: centerX - 100,
203+
y: centerY - 100,
204+
})
205+
206+
const shapeId = editor.getCurrentPageShapeIds().values().next().value
207+
if (shapeId) {
208+
editor.select(shapeId)
209+
}
210+
}}
211+
/>
212+
</div>
213+
)
214+
}
215+
216+
/*
217+
This example demonstrates the `snapReferenceHandleId` property using a Y-shaped connector.
218+
219+
The shape has three arms radiating from a center junction point:
220+
- center (junction point)
221+
- armTop (top arm endpoint)
222+
- armLeft (bottom-left arm endpoint)
223+
- armRight (bottom-right arm endpoint)
224+
225+
[1]
226+
Define the shape type with four points representing a Y-shaped connector.
227+
228+
[2]
229+
The shape util with validators for each point.
230+
231+
[3]
232+
Use Group2d geometry containing three line segments from center to each arm.
233+
234+
[4]
235+
Four handles in array order: [center, armTop, armLeft, armRight]
236+
237+
[5]
238+
With `snapReferenceHandleId: 'center'`, when you shift+drag armTop, it will snap to the center point.
239+
240+
[6]
241+
Similarly, armLeft would snap relative to the center point.
242+
243+
[7]
244+
And armRight would snap to the center point.
245+
246+
*/
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
title: Custom handle snap reference
3+
component: ./CustomRelativeSnappingExample.tsx
4+
category: shapes/tools
5+
priority: 2
6+
keywords: [handles, snapping, snap reference]
7+
---
8+
9+
An example demonstrating `snapReferenceHandleId` for control point angle snapping.
10+
11+
---
12+
13+
This example shows how to use the `snapReferenceHandleId` property to control which handle serves as the reference point for shift-modifier angle snapping.

0 commit comments

Comments
 (0)