tldraw 3.16.0-canary.d3a23ebd1b0b → 3.16.0-canary.d3f0c2d5313c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist-cjs/index.d.ts +3 -2
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js +3 -0
  4. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js.map +2 -2
  5. package/dist-cjs/lib/ui/components/Toolbar/AltTextEditor.js +3 -2
  6. package/dist-cjs/lib/ui/components/Toolbar/AltTextEditor.js.map +2 -2
  7. package/dist-cjs/lib/ui/components/Toolbar/DefaultImageToolbarContent.js +38 -9
  8. package/dist-cjs/lib/ui/components/Toolbar/DefaultImageToolbarContent.js.map +2 -2
  9. package/dist-cjs/lib/ui/components/Toolbar/DefaultVideoToolbarContent.js +15 -3
  10. package/dist-cjs/lib/ui/components/Toolbar/DefaultVideoToolbarContent.js.map +2 -2
  11. package/dist-cjs/lib/ui/components/Toolbar/LinkEditor.js +3 -3
  12. package/dist-cjs/lib/ui/components/Toolbar/LinkEditor.js.map +2 -2
  13. package/dist-cjs/lib/ui/components/primitives/TldrawUiContextualToolbar.js +10 -1
  14. package/dist-cjs/lib/ui/components/primitives/TldrawUiContextualToolbar.js.map +2 -2
  15. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js +12 -1
  16. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js.map +2 -2
  17. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js +4 -0
  18. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js.map +2 -2
  19. package/dist-cjs/lib/ui/context/events.js.map +1 -1
  20. package/dist-cjs/lib/ui/hooks/useTranslation/TLUiTranslationKey.js.map +1 -1
  21. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js +2 -0
  22. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js.map +2 -2
  23. package/dist-cjs/lib/ui/version.js +3 -3
  24. package/dist-cjs/lib/ui/version.js.map +1 -1
  25. package/dist-esm/index.d.mts +3 -2
  26. package/dist-esm/index.mjs +1 -1
  27. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs +3 -0
  28. package/dist-esm/lib/shapes/frame/FrameShapeUtil.mjs.map +2 -2
  29. package/dist-esm/lib/ui/components/Toolbar/AltTextEditor.mjs +3 -2
  30. package/dist-esm/lib/ui/components/Toolbar/AltTextEditor.mjs.map +2 -2
  31. package/dist-esm/lib/ui/components/Toolbar/DefaultImageToolbarContent.mjs +38 -9
  32. package/dist-esm/lib/ui/components/Toolbar/DefaultImageToolbarContent.mjs.map +2 -2
  33. package/dist-esm/lib/ui/components/Toolbar/DefaultVideoToolbarContent.mjs +15 -3
  34. package/dist-esm/lib/ui/components/Toolbar/DefaultVideoToolbarContent.mjs.map +2 -2
  35. package/dist-esm/lib/ui/components/Toolbar/LinkEditor.mjs +3 -3
  36. package/dist-esm/lib/ui/components/Toolbar/LinkEditor.mjs.map +2 -2
  37. package/dist-esm/lib/ui/components/primitives/TldrawUiContextualToolbar.mjs +10 -1
  38. package/dist-esm/lib/ui/components/primitives/TldrawUiContextualToolbar.mjs.map +2 -2
  39. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs +12 -1
  40. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs.map +2 -2
  41. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs +4 -0
  42. package/dist-esm/lib/ui/components/primitives/TldrawUiTooltip.mjs.map +2 -2
  43. package/dist-esm/lib/ui/context/events.mjs.map +1 -1
  44. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs +2 -0
  45. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs.map +2 -2
  46. package/dist-esm/lib/ui/version.mjs +3 -3
  47. package/dist-esm/lib/ui/version.mjs.map +1 -1
  48. package/package.json +3 -3
  49. package/src/lib/shapes/frame/FrameShapeUtil.tsx +4 -0
  50. package/src/lib/ui/components/Toolbar/AltTextEditor.tsx +4 -3
  51. package/src/lib/ui/components/Toolbar/DefaultImageToolbarContent.tsx +32 -15
  52. package/src/lib/ui/components/Toolbar/DefaultVideoToolbarContent.tsx +12 -4
  53. package/src/lib/ui/components/Toolbar/LinkEditor.tsx +5 -5
  54. package/src/lib/ui/components/primitives/TldrawUiContextualToolbar.tsx +6 -1
  55. package/src/lib/ui/components/primitives/TldrawUiSlider.tsx +17 -2
  56. package/src/lib/ui/components/primitives/TldrawUiTooltip.tsx +6 -0
  57. package/src/lib/ui/context/events.tsx +1 -1
  58. package/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +2 -0
  59. package/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +2 -0
  60. package/src/lib/ui/version.ts +3 -3
  61. package/src/test/custom-clipping.test.ts +436 -0
@@ -1,3 +1,4 @@
1
+ import { tltime } from '@tldraw/editor'
1
2
  import { Slider as _Slider } from 'radix-ui'
2
3
  import React, { useCallback, useEffect, useState } from 'react'
3
4
  import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
@@ -33,6 +34,7 @@ export const TldrawUiSlider = React.forwardRef<HTMLDivElement, TLUiSliderProps>(
33
34
  ref
34
35
  ) {
35
36
  const msg = useTranslation()
37
+ const [titleAndLabel, setTitleAndLabel] = useState('')
36
38
 
37
39
  // XXX: Radix starts out our slider with a tabIndex of 0
38
40
  // This causes some tab focusing issues, most prevelant in MobileStylePanel,
@@ -54,6 +56,21 @@ export const TldrawUiSlider = React.forwardRef<HTMLDivElement, TLUiSliderProps>(
54
56
  onHistoryMark('click slider')
55
57
  }, [onHistoryMark])
56
58
 
59
+ // N.B. This is a bit silly. The Radix slider auto-focuses which
60
+ // triggers TldrawUiTooltip handleFocus when we dbl-click to edit an image,
61
+ // which in turn makes the tooltip display prematurely.
62
+ // This makes it wait until we've focused to show the tooltip.
63
+ useEffect(() => {
64
+ const timeout = tltime.setTimeout(
65
+ 'set title and label',
66
+ () => {
67
+ setTitleAndLabel(title + ' — ' + msg(label as TLUiTranslationKey))
68
+ },
69
+ 0
70
+ )
71
+ return () => clearTimeout(timeout)
72
+ }, [label, msg, title])
73
+
57
74
  // N.B. Annoying. For a11y purposes, we need Tab to work.
58
75
  // For some reason, Radix has some custom behavior here
59
76
  // that interferes with tabbing past the slider and then
@@ -64,8 +81,6 @@ export const TldrawUiSlider = React.forwardRef<HTMLDivElement, TLUiSliderProps>(
64
81
  }
65
82
  }, [])
66
83
 
67
- const titleAndLabel = title + ' — ' + msg(label as TLUiTranslationKey)
68
-
69
84
  return (
70
85
  <div className="tlui-slider__container">
71
86
  <TldrawUiTooltip content={titleAndLabel}>
@@ -235,6 +235,8 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
235
235
  const orientationCtx = useTldrawUiOrientation()
236
236
  const sideToUse = side ?? orientationCtx.tooltipSide
237
237
 
238
+ const camera = useValue('camera', () => editor?.getCamera(), [])
239
+
238
240
  useEffect(() => {
239
241
  const currentTooltipId = tooltipId.current
240
242
  return () => {
@@ -244,6 +246,10 @@ export const TldrawUiTooltip = forwardRef<HTMLButtonElement, TldrawUiTooltipProp
244
246
  }
245
247
  }, [editor, hasProvider])
246
248
 
249
+ useEffect(() => {
250
+ tooltipManager.hideTooltip(editor, tooltipId.current, true)
251
+ }, [editor, camera])
252
+
247
253
  // Don't show tooltip if disabled, no content, or UI labels are disabled
248
254
  if (disabled || !content) {
249
255
  return <>{children}</>
@@ -123,7 +123,7 @@ export interface TLUiEventMap {
123
123
  'shrink-shapes': null
124
124
  'flatten-to-image': null
125
125
  'a11y-repeat-shape-announce': null
126
- 'open-url': { url: string }
126
+ 'open-url': { destinationUrl: string }
127
127
  'open-context-menu': null
128
128
  'adjust-shape-styles': null
129
129
  'copy-link': null
@@ -186,6 +186,7 @@ export type TLUiTranslationKey =
186
186
  | 'geo-style.pentagon'
187
187
  | 'geo-style.rectangle'
188
188
  | 'geo-style.rhombus'
189
+ | 'geo-style.rhombus-2'
189
190
  | 'geo-style.star'
190
191
  | 'geo-style.trapezoid'
191
192
  | 'geo-style.triangle'
@@ -260,6 +261,7 @@ export type TLUiTranslationKey =
260
261
  | 'tool.aspect-ratio.wide'
261
262
  | 'tool.image-toolbar-title'
262
263
  | 'tool.image-crop'
264
+ | 'tool.image-crop-confirm'
263
265
  | 'tool.media-alt-text'
264
266
  | 'tool.media-alt-text-desc'
265
267
  | 'tool.media-alt-text-confirm'
@@ -187,6 +187,7 @@ export const DEFAULT_TRANSLATION = {
187
187
  'geo-style.pentagon': 'Pentagon',
188
188
  'geo-style.rectangle': 'Rectangle',
189
189
  'geo-style.rhombus': 'Rhombus',
190
+ 'geo-style.rhombus-2': 'Rhombus left',
190
191
  'geo-style.star': 'Star',
191
192
  'geo-style.trapezoid': 'Trapezoid',
192
193
  'geo-style.triangle': 'Triangle',
@@ -261,6 +262,7 @@ export const DEFAULT_TRANSLATION = {
261
262
  'tool.aspect-ratio.wide': 'Wide (16:9)',
262
263
  'tool.image-toolbar-title': 'Image tools',
263
264
  'tool.image-crop': 'Crop image',
265
+ 'tool.image-crop-confirm': 'Confirm',
264
266
  'tool.media-alt-text': 'Alternative text',
265
267
  'tool.media-alt-text-desc': 'Give a description…',
266
268
  'tool.media-alt-text-confirm': 'Confirm',
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '3.16.0-canary.d3a23ebd1b0b'
4
+ export const version = '3.16.0-canary.d3f0c2d5313c'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-08-26T15:17:10.156Z',
8
- patch: '2025-08-26T15:17:10.156Z',
7
+ minor: '2025-09-01T15:21:11.358Z',
8
+ patch: '2025-09-01T15:21:11.358Z',
9
9
  }
@@ -0,0 +1,436 @@
1
+ import {
2
+ atom,
3
+ BaseBoxShapeUtil,
4
+ Circle2d,
5
+ createShapeId,
6
+ Geometry2d,
7
+ RecordProps,
8
+ resizeBox,
9
+ StateNode,
10
+ T,
11
+ TLBaseShape,
12
+ TLEventHandlers,
13
+ TLGeoShape,
14
+ TLResizeInfo,
15
+ TLShape,
16
+ TLTextShape,
17
+ toRichText,
18
+ Vec,
19
+ } from '@tldraw/editor'
20
+ import { TestEditor } from './TestEditor'
21
+
22
+ // Custom Circle Clip Shape Definition
23
+ export type CircleClipShape = TLBaseShape<
24
+ 'circle-clip',
25
+ {
26
+ w: number
27
+ h: number
28
+ }
29
+ >
30
+
31
+ export const isClippingEnabled$ = atom('isClippingEnabled', true)
32
+
33
+ export class CircleClipShapeUtil extends BaseBoxShapeUtil<CircleClipShape> {
34
+ static override type = 'circle-clip' as const
35
+ static override props: RecordProps<CircleClipShape> = {
36
+ w: T.number,
37
+ h: T.number,
38
+ }
39
+
40
+ override canBind() {
41
+ return false
42
+ }
43
+
44
+ override canReceiveNewChildrenOfType(shape: TLShape) {
45
+ return !shape.isLocked
46
+ }
47
+
48
+ override getDefaultProps(): CircleClipShape['props'] {
49
+ return {
50
+ w: 200,
51
+ h: 200,
52
+ }
53
+ }
54
+
55
+ override getGeometry(shape: CircleClipShape): Geometry2d {
56
+ const radius = Math.min(shape.props.w, shape.props.h) / 2
57
+ return new Circle2d({
58
+ radius,
59
+ x: shape.props.w / 2 - radius,
60
+ y: shape.props.h / 2 - radius,
61
+ isFilled: true,
62
+ })
63
+ }
64
+
65
+ override getClipPath(shape: CircleClipShape): Vec[] | undefined {
66
+ // Generate a polygon approximation of the circle
67
+ const centerX = shape.props.w / 2
68
+ const centerY = shape.props.h / 2
69
+ const radius = Math.min(shape.props.w, shape.props.h) / 2
70
+ const segments = 48 // More segments = smoother circle
71
+
72
+ const points: Vec[] = []
73
+ for (let i = 0; i < segments; i++) {
74
+ const angle = (i / segments) * Math.PI * 2
75
+ const x = centerX + Math.cos(angle) * radius
76
+ const y = centerY + Math.sin(angle) * radius
77
+ points.push(new Vec(x, y))
78
+ }
79
+
80
+ return points
81
+ }
82
+
83
+ override shouldClipChild(_child: TLShape): boolean {
84
+ // For now, clip all children - we removed the onlyClipText feature for simplicity
85
+ return isClippingEnabled$.get()
86
+ }
87
+
88
+ override component(_shape: CircleClipShape) {
89
+ // For testing purposes, we'll just return null
90
+ // In a real implementation, this would return JSX
91
+ return null as any
92
+ }
93
+
94
+ override indicator(_shape: CircleClipShape) {
95
+ // For testing purposes, we'll just return null
96
+ // In a real implementation, this would return JSX
97
+ return null as any
98
+ }
99
+
100
+ override onResize(shape: CircleClipShape, info: TLResizeInfo<CircleClipShape>) {
101
+ return resizeBox(shape, info)
102
+ }
103
+ }
104
+
105
+ export class CircleClipShapeTool extends StateNode {
106
+ static override id = 'circle-clip'
107
+
108
+ override onEnter(): void {
109
+ this.editor.setCursor({ type: 'cross', rotation: 0 })
110
+ }
111
+
112
+ override onPointerDown(info: Parameters<TLEventHandlers['onPointerDown']>[0]) {
113
+ if (info.target === 'canvas') {
114
+ const { originPagePoint } = this.editor.inputs
115
+
116
+ this.editor.createShape<CircleClipShape>({
117
+ type: 'circle-clip',
118
+ x: originPagePoint.x - 100,
119
+ y: originPagePoint.y - 100,
120
+ props: {
121
+ w: 200,
122
+ h: 200,
123
+ },
124
+ })
125
+ }
126
+ }
127
+ }
128
+
129
+ let editor: TestEditor
130
+
131
+ afterEach(() => {
132
+ editor?.dispose()
133
+ })
134
+
135
+ const ids = {
136
+ circleClip1: createShapeId('circleClip1'),
137
+ circleClip2: createShapeId('circleClip2'),
138
+ text1: createShapeId('text1'),
139
+ geo1: createShapeId('geo1'),
140
+ geo2: createShapeId('geo2'),
141
+ }
142
+
143
+ beforeEach(() => {
144
+ editor = new TestEditor({
145
+ shapeUtils: [CircleClipShapeUtil],
146
+ tools: [CircleClipShapeTool],
147
+ })
148
+
149
+ // Reset clipping state
150
+ isClippingEnabled$.set(true)
151
+ })
152
+
153
+ describe('CircleClipShapeUtil', () => {
154
+ describe('shape creation and properties', () => {
155
+ it('should create a circle clip shape with default properties', () => {
156
+ editor.createShape<CircleClipShape>({
157
+ id: ids.circleClip1,
158
+ type: 'circle-clip',
159
+ x: 100,
160
+ y: 100,
161
+ props: {
162
+ w: 200,
163
+ h: 200,
164
+ },
165
+ })
166
+
167
+ const shape = editor.getShape<CircleClipShape>(ids.circleClip1)
168
+ expect(shape).toBeDefined()
169
+ expect(shape!.type).toBe('circle-clip')
170
+ expect(shape!.props.w).toBe(200)
171
+ expect(shape!.props.h).toBe(200)
172
+ })
173
+
174
+ it('should use default props when not specified', () => {
175
+ editor.createShape<CircleClipShape>({
176
+ id: ids.circleClip1,
177
+ type: 'circle-clip',
178
+ x: 100,
179
+ y: 100,
180
+ props: {},
181
+ })
182
+
183
+ const shape = editor.getShape<CircleClipShape>(ids.circleClip1)
184
+ expect(shape!.props.w).toBe(200) // default from getDefaultProps
185
+ expect(shape!.props.h).toBe(200) // default from getDefaultProps
186
+ })
187
+ })
188
+
189
+ describe('geometry and clipping', () => {
190
+ it('should generate correct circle geometry', () => {
191
+ editor.createShape<CircleClipShape>({
192
+ id: ids.circleClip1,
193
+ type: 'circle-clip',
194
+ x: 100,
195
+ y: 100,
196
+ props: {
197
+ w: 200,
198
+ h: 200,
199
+ },
200
+ })
201
+
202
+ const shape = editor.getShape<CircleClipShape>(ids.circleClip1)
203
+ const util = editor.getShapeUtil<CircleClipShape>('circle-clip')
204
+ const geometry = util.getGeometry(shape!)
205
+
206
+ expect(geometry).toBeDefined()
207
+ expect(geometry.bounds).toBeDefined()
208
+ expect(geometry.bounds.width).toBe(200)
209
+ expect(geometry.bounds.height).toBe(200)
210
+ })
211
+
212
+ it('should generate clip path for circle', () => {
213
+ editor.createShape<CircleClipShape>({
214
+ id: ids.circleClip1,
215
+ type: 'circle-clip',
216
+ x: 100,
217
+ y: 100,
218
+ props: {
219
+ w: 200,
220
+ h: 200,
221
+ },
222
+ })
223
+
224
+ const shape = editor.getShape<CircleClipShape>(ids.circleClip1)
225
+ const util = editor.getShapeUtil<CircleClipShape>('circle-clip')
226
+ const clipPath = util.getClipPath?.(shape!)
227
+ if (!clipPath) throw new Error('Clip path is undefined')
228
+
229
+ expect(clipPath).toBeDefined()
230
+ expect(Array.isArray(clipPath)).toBe(true)
231
+ expect(clipPath.length).toBeGreaterThan(0)
232
+
233
+ // Should be a polygon approximation of a circle
234
+ // Check that points are roughly in a circle pattern
235
+ const centerX = 100 // shape.x
236
+ const centerY = 100 // shape.y
237
+ const radius = 100 // min(w, h) / 2
238
+
239
+ clipPath.forEach((point) => {
240
+ const distance = Math.sqrt(Math.pow(point.x - centerX, 2) + Math.pow(point.y - centerY, 2))
241
+ expect(distance).toBeCloseTo(radius, 0)
242
+ })
243
+ })
244
+ })
245
+
246
+ describe('child clipping behavior', () => {
247
+ it('should clip children when clipping is enabled', () => {
248
+ editor.createShape<CircleClipShape>({
249
+ id: ids.circleClip1,
250
+ type: 'circle-clip',
251
+ x: 100,
252
+ y: 100,
253
+ props: {
254
+ w: 200,
255
+ h: 200,
256
+ },
257
+ })
258
+
259
+ editor.createShape<TLTextShape>({
260
+ id: ids.text1,
261
+ type: 'text',
262
+ x: 0,
263
+ y: 0,
264
+ parentId: ids.circleClip1,
265
+ props: {
266
+ richText: toRichText('Test text'),
267
+ },
268
+ })
269
+
270
+ const util = editor.getShapeUtil<CircleClipShape>('circle-clip')
271
+ const textShape = editor.getShape<TLTextShape>(ids.text1)
272
+
273
+ // Clipping should be enabled by default
274
+ expect(isClippingEnabled$.get()).toBe(true)
275
+ expect(util.shouldClipChild?.(textShape!)).toBe(true)
276
+ expect(editor.getShapeClipPath(ids.text1)).toBeDefined()
277
+ })
278
+
279
+ it('should not clip children when clipping is disabled', () => {
280
+ isClippingEnabled$.set(false)
281
+
282
+ editor.createShape<CircleClipShape>({
283
+ id: ids.circleClip1,
284
+ type: 'circle-clip',
285
+ x: 100,
286
+ y: 100,
287
+ props: {
288
+ w: 200,
289
+ h: 200,
290
+ },
291
+ })
292
+
293
+ editor.createShape<TLTextShape>({
294
+ id: ids.text1,
295
+ type: 'text',
296
+ x: 0,
297
+ y: 0,
298
+ parentId: ids.circleClip1,
299
+ props: {
300
+ richText: toRichText('Test text'),
301
+ },
302
+ })
303
+
304
+ const util = editor.getShapeUtil<CircleClipShape>('circle-clip')
305
+ const textShape = editor.getShape<TLTextShape>(ids.text1)
306
+
307
+ expect(isClippingEnabled$.get()).toBe(false)
308
+ expect(util.shouldClipChild?.(textShape!)).toBe(false)
309
+ expect(editor.getShapeClipPath(ids.text1)).toBeUndefined()
310
+ })
311
+ })
312
+ })
313
+
314
+ describe('Integration tests', () => {
315
+ it('should create and manage circle clip shapes with children', () => {
316
+ // Create circle clip shape
317
+ editor.createShape<CircleClipShape>({
318
+ id: ids.circleClip1,
319
+ type: 'circle-clip',
320
+ x: 100,
321
+ y: 100,
322
+ props: {
323
+ w: 200,
324
+ h: 200,
325
+ },
326
+ })
327
+
328
+ // Add text child
329
+ editor.createShape<TLTextShape>({
330
+ id: ids.text1,
331
+ type: 'text',
332
+ x: 50,
333
+ y: 50,
334
+ parentId: ids.circleClip1,
335
+ props: {
336
+ richText: toRichText('Clipped text'),
337
+ },
338
+ })
339
+
340
+ // Add geo child
341
+ editor.createShape<TLGeoShape>({
342
+ id: ids.geo1,
343
+ type: 'geo',
344
+ x: 150,
345
+ y: 150,
346
+ parentId: ids.circleClip1,
347
+ props: {
348
+ w: 50,
349
+ h: 50,
350
+ },
351
+ })
352
+
353
+ const circleClipShape = editor.getShape<CircleClipShape>(ids.circleClip1)
354
+ const textShape = editor.getShape<TLTextShape>(ids.text1)
355
+ const geoShape = editor.getShape<TLGeoShape>(ids.geo1)
356
+
357
+ expect(circleClipShape).toBeDefined()
358
+ expect(textShape!.parentId).toBe(ids.circleClip1)
359
+ expect(geoShape!.parentId).toBe(ids.circleClip1)
360
+
361
+ // Verify clipping behavior
362
+ const util = editor.getShapeUtil<CircleClipShape>('circle-clip')
363
+ expect(util.shouldClipChild?.(textShape!)).toBe(true)
364
+ expect(util.shouldClipChild?.(geoShape!)).toBe(true)
365
+ expect(editor.getShapeClipPath(ids.text1)).toBeDefined()
366
+ expect(editor.getShapeClipPath(ids.geo1)).toBeDefined()
367
+
368
+ // Test clipping toggle
369
+ isClippingEnabled$.set(false)
370
+ expect(util.shouldClipChild?.(textShape!)).toBe(false)
371
+ expect(util.shouldClipChild?.(geoShape!)).toBe(false)
372
+ expect(editor.getShapeClipPath(ids.text1)).toBeUndefined()
373
+ expect(editor.getShapeClipPath(ids.geo1)).toBeUndefined()
374
+ })
375
+
376
+ it('should handle multiple circle clip shapes independently', () => {
377
+ // Create two circle clip shapes
378
+ editor.createShape<CircleClipShape>({
379
+ id: ids.circleClip1,
380
+ type: 'circle-clip',
381
+ x: 100,
382
+ y: 100,
383
+ props: {
384
+ w: 200,
385
+ h: 200,
386
+ },
387
+ })
388
+
389
+ editor.createShape<CircleClipShape>({
390
+ id: ids.circleClip2,
391
+ type: 'circle-clip',
392
+ x: 400,
393
+ y: 100,
394
+ props: {
395
+ w: 150,
396
+ h: 150,
397
+ },
398
+ })
399
+
400
+ // Add children to both
401
+ editor.createShape<TLTextShape>({
402
+ id: ids.text1,
403
+ type: 'text',
404
+ x: 0,
405
+ y: 0,
406
+ parentId: ids.circleClip1,
407
+ props: {
408
+ richText: toRichText('First clip'),
409
+ },
410
+ })
411
+
412
+ editor.createShape<TLTextShape>({
413
+ id: ids.geo1,
414
+ type: 'text',
415
+ x: 0,
416
+ y: 0,
417
+ parentId: ids.circleClip2,
418
+ props: {
419
+ richText: toRichText('Second clip'),
420
+ },
421
+ })
422
+
423
+ const util = editor.getShapeUtil<CircleClipShape>('circle-clip')
424
+ const text1 = editor.getShape<TLTextShape>(ids.text1)
425
+ const text2 = editor.getShape<TLTextShape>(ids.geo1)
426
+
427
+ // Both should be clipped when enabled
428
+ expect(util.shouldClipChild?.(text1!)).toBe(true)
429
+ expect(util.shouldClipChild?.(text2!)).toBe(true)
430
+
431
+ // Both should not be clipped when disabled
432
+ isClippingEnabled$.set(false)
433
+ expect(util.shouldClipChild?.(text1!)).toBe(false)
434
+ expect(util.shouldClipChild?.(text2!)).toBe(false)
435
+ })
436
+ })