From 51eaf2c1f38aa8ca4f40f71649576c46cbe71ceb Mon Sep 17 00:00:00 2001 From: Kazuho Okui Date: Fri, 30 Jan 2026 17:24:54 -0800 Subject: [PATCH 1/2] Add grid and artifact shape --- packages/editor/src/lib/editor/Editor.ts | 48 ++++++++++++++---------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index 09dcc8509a9b..9404d6049a6c 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -5294,7 +5294,12 @@ export class Editor extends EventEmitter { : this.getCurrentPageShapesSorted() ).filter((shape) => { // Frames have labels positioned above the shape (outside bounds), so always include them - if (!candidateIds.has(shape.id) && !this.isShapeOfType(shape, 'frame')) return false + if ( + !candidateIds.has(shape.id) && + !this.isShapeOfType(shape, 'frame') && + (shape as any).type !== 'grid' + ) + return false if ( (shape.isLocked && !hitLocked) || @@ -5316,13 +5321,7 @@ export class Editor extends EventEmitter { const pointInShapeSpace = this.getPointInShapeSpace(shape, point) // Check labels first - if ( - this.isShapeOfType(shape, 'frame') || - ((this.isShapeOfType(shape, 'note') || - this.isShapeOfType(shape, 'arrow') || - (this.isShapeOfType(shape, 'geo') && shape.props.fill === 'none')) && - this.getShapeUtil(shape).getText(shape)?.trim()) - ) { + if (geometry instanceof Group2d) { for (const childGeometry of (geometry as Group2d).children) { if (childGeometry.isLabel && childGeometry.isPointInBounds(pointInShapeSpace)) { return shape @@ -5330,7 +5329,7 @@ export class Editor extends EventEmitter { } } - if (this.isShapeOfType(shape, 'frame')) { + if (this.isShapeOfType(shape, 'frame') || (shape as any).type === 'grid') { // On the rare case that we've hit a frame (not its label), test again hitInside to be forced true; // this prevents clicks from passing through the body of a frame to shapes behind it. @@ -5489,7 +5488,12 @@ export class Editor extends EventEmitter { return this.getCurrentPageShapesSorted() .filter((shape) => { if (this.isShapeHidden(shape)) return false - if (!candidateIds.has(shape.id) && !this.isShapeOfType(shape, 'frame')) return false + if ( + !candidateIds.has(shape.id) && + !this.isShapeOfType(shape, 'frame') && + (shape as any).type !== 'grid' + ) + return false return this.isPointInShape(shape, point, opts) }) .reverse() @@ -9363,7 +9367,7 @@ export class Editor extends EventEmitter { for (const shape of this.getSelectedShapes()) { if (lowestDepth === 0) break - const isFrame = this.isShapeOfType(shape, 'frame') + const isFrame = this.isShapeOfType(shape, 'frame') || (shape as any).type == 'grid' const ancestors = this.getShapeAncestors(shape) if (isFrame) ancestors.push(shape) @@ -9425,11 +9429,17 @@ export class Editor extends EventEmitter { } else { if (rootShapeIds.length === 1) { const rootShape = shapes.find((s) => s.id === rootShapeIds[0])! + + const isParentFrame = + this.isShapeOfType(parent, 'frame') || (parent as any).type == 'grid' + const isRootFrame = + this.isShapeOfType(rootShape, 'frame') || (rootShape as any).type == 'grid' + if ( - this.isShapeOfType(parent, 'frame') && - this.isShapeOfType(rootShape, 'frame') && - rootShape.props.w === parent?.props.w && - rootShape.props.h === parent?.props.h + isParentFrame && + isRootFrame && + (rootShape as any).props.w === (parent as any).props.w && + (rootShape as any).props.h === (parent as any).props.h ) { isDuplicating = true } @@ -9602,13 +9612,13 @@ export class Editor extends EventEmitter { const onlyRoot = rootShapes[0] as TLFrameShape // If the old bounds are in the viewport... // todo: replace frame references with shapes that can accept children - if (this.isShapeOfType(onlyRoot, 'frame')) { + if (this.isShapeOfType(onlyRoot, 'frame') || (onlyRoot as any).type == 'grid') { while ( this.getShapesAtPoint(point).some( (shape) => - this.isShapeOfType(shape, 'frame') && - shape.props.w === onlyRoot.props.w && - shape.props.h === onlyRoot.props.h + (this.isShapeOfType(shape, 'frame') || (shape as any).type == 'grid') && + (shape as any).props.w === (onlyRoot as any).props.w && + (shape as any).props.h === (onlyRoot as any).props.h ) ) { point.x += bounds.w + 16 From 8fe2f688da6f8f45d9391686f0b58b6b6f31972c Mon Sep 17 00:00:00 2001 From: Kazuho Okui Date: Fri, 13 Feb 2026 20:10:05 -0800 Subject: [PATCH 2/2] Export renderer --- packages/editor/api-report.api.md | 12 ++++++++++ packages/editor/src/index.ts | 2 ++ .../default-components/DefaultCanvas.tsx | 24 +++++++++++++------ .../src/lib/hooks/useEditorComponents.tsx | 4 ++++ 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/packages/editor/api-report.api.md b/packages/editor/api-report.api.md index 9ec8d33f8957..5c708f45d362 100644 --- a/packages/editor/api-report.api.md +++ b/packages/editor/api-report.api.md @@ -21,6 +21,7 @@ import { HistoryEntry } from '@tldraw/store'; import { IndexKey } from '@tldraw/utils'; import { JsonObject } from '@tldraw/utils'; import { JSX } from 'react/jsx-runtime'; +import { JSXElementConstructor } from 'react'; import { LegacyMigrations } from '@tldraw/store'; import { MigrationSequence } from '@tldraw/store'; import { NamedExoticComponent } from 'react'; @@ -657,6 +658,9 @@ export const DefaultShapeIndicator: NamedExoticComponent; // @public (undocumented) export const DefaultShapeIndicators: NamedExoticComponent; +// @public (undocumented) +export function DefaultShapeRenderer({ renderShape }: TLShapeRendererProps): ReactElement | string>[]; + // @public (undocumented) export const DefaultShapeWrapper: ForwardRefExoticComponent>; @@ -3618,6 +3622,8 @@ export interface TLEditorComponents { // (undocumented) ShapeIndicators?: ComponentType | null; // (undocumented) + ShapeRenderer?: ComponentType | null; + // (undocumented) ShapeWrapper?: ComponentType> | null; // (undocumented) SnapIndicator?: ComponentType | null; @@ -4342,6 +4348,12 @@ export interface TLShapeIndicatorsProps { showAll?: boolean; } +// @public (undocumented) +export interface TLShapeRendererProps { + // (undocumented) + renderShape(shape: TLRenderingShape): ReactElement; +} + // @public export interface TLShapeUtilCanBeLaidOutOpts { shapes?: TLShape[]; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 5a605ff0f991..038fd3d6b435 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -17,7 +17,9 @@ export { DefaultBackground } from './lib/components/default-components/DefaultBa export { DefaultBrush, type TLBrushProps } from './lib/components/default-components/DefaultBrush' export { DefaultCanvas, + DefaultShapeRenderer, type TLCanvasComponentProps, + type TLShapeRendererProps, } from './lib/components/default-components/DefaultCanvas' export { DefaultCollaboratorHint, diff --git a/packages/editor/src/lib/components/default-components/DefaultCanvas.tsx b/packages/editor/src/lib/components/default-components/DefaultCanvas.tsx index 2e8d0eb22e28..2c3f30fa2a7c 100644 --- a/packages/editor/src/lib/components/default-components/DefaultCanvas.tsx +++ b/packages/editor/src/lib/components/default-components/DefaultCanvas.tsx @@ -3,7 +3,8 @@ import { useQuickReactor, useValue } from '@tldraw/state-react' import { TLHandle, TLShapeId } from '@tldraw/tlschema' import { dedupe, modulate, objectMapValues } from '@tldraw/utils' import classNames from 'classnames' -import { Fragment, JSX, useEffect, useRef, useState } from 'react' +import { Fragment, JSX, ReactElement, useEffect, useRef, useState } from 'react' +import type { TLRenderingShape } from '../../editor/Editor' import { tlenv } from '../../globals/environment' import { useCanvasEvents } from '../../hooks/useCanvasEvents' import { useCoarsePointer } from '../../hooks/useCoarsePointer' @@ -450,21 +451,30 @@ function CullingController() { } function ShapesToDisplay() { - const editor = useEditor() - - const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor]) + const { ShapeRenderer } = useEditorComponents() + const Renderer = ShapeRenderer ?? DefaultShapeRenderer return ( - {renderingShapes.map((result) => ( - - ))} + } /> {tlenv.isSafari && } ) } +/** @public */ +export interface TLShapeRendererProps { + renderShape(shape: TLRenderingShape): ReactElement +} + +/** @public @react */ +export function DefaultShapeRenderer({ renderShape }: TLShapeRendererProps) { + const editor = useEditor() + const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor]) + return renderingShapes.map((shape) => renderShape(shape)) +} + function HintedShapeIndicator() { const editor = useEditor() const { ShapeIndicator } = useEditorComponents() diff --git a/packages/editor/src/lib/hooks/useEditorComponents.tsx b/packages/editor/src/lib/hooks/useEditorComponents.tsx index 161f5e02708f..f52855856eb9 100644 --- a/packages/editor/src/lib/hooks/useEditorComponents.tsx +++ b/packages/editor/src/lib/hooks/useEditorComponents.tsx @@ -3,7 +3,9 @@ import { DefaultBackground } from '../components/default-components/DefaultBackg import { DefaultBrush, TLBrushProps } from '../components/default-components/DefaultBrush' import { DefaultCanvas, + DefaultShapeRenderer, TLCanvasComponentProps, + TLShapeRendererProps, } from '../components/default-components/DefaultCanvas' import { DefaultCollaboratorHint, @@ -72,6 +74,7 @@ export interface TLEditorComponents { SelectionForeground?: ComponentType | null ShapeIndicator?: ComponentType | null ShapeIndicators?: ComponentType | null + ShapeRenderer?: ComponentType | null ShapeWrapper?: ComponentType> | null SnapIndicator?: ComponentType | null Spinner?: ComponentType> | null @@ -119,6 +122,7 @@ export function EditorComponentsProvider({ SelectionForeground: DefaultSelectionForeground, ShapeIndicator: DefaultShapeIndicator, ShapeIndicators: DefaultShapeIndicators, + ShapeRenderer: DefaultShapeRenderer, ShapeWrapper: DefaultShapeWrapper, SnapIndicator: DefaultSnapIndicator, Spinner: DefaultSpinner,