Skip to content

Commit c3d8c7a

Browse files
refactor(shapes): register all geo shapes through GeoTypeDefinition (tldraw#8730)
In order to test whether the `GeoTypeDefinition` extension API is general enough to act as the canonical registration system for geo shapes, this PR routes every built-in geo type (rectangle, ellipse, cloud, etc.) through the same registry that powers `customGeoTypes`. Built off of tldraw#8543's follow-up branch (`mime/config-geo-followup`). The same registry now drives path generation, handle snapping, the style panel picker, and creation defaults — so consumer-defined custom types and built-ins go through identical code paths. No extension API changes; built-ins just opt in to the existing one. ### Concepts | Term | Type | Meaning | |------|------|---------| | `defaultGeoTypeDefinitions` | `Record<string, GeoTypeDefinition>` | Registry of every built-in geo type's path/snap/icon/size behavior | | `getGeoTypeDefinition(name, customGeoTypes?)` | function | Lookup helper that prefers custom types, falls back to built-ins | ### What this collapses | Before | After | |--------|-------| | `switch` over each `geo` value in `_getGeoPath` | `def.getPath(w, h, shape, sw)` | | Polygon/blobby `switch` in `getHandleSnapGeometry` | `def.snapType === 'blobby' ? center : vertices+center` | | Hardcoded `star`/`cloud` size cases in `Pointing.complete()` | `def.defaultSize ?? { w: 200, h: 200 }` | | `STYLES.geo` array merged with `customItems` in style panel | `Object.entries({ ...defaultGeoTypeDefinitions, ...customGeoTypes })` | | Collision check against `GeoShapeGeoStyle.values` in `configure()` | Collision check against `defaultGeoTypeDefinitions` keys | ### Example A consumer defining a custom geo type uses the same shape as a built-in: ```ts // built-in (now lives in defaultGeoTypeDefinitions) rectangle: { snapType: 'polygon', icon: 'geo-rectangle', getPath: (w, h, shape) => new PathBuilder() .moveTo(0, 0, { geometry: { isFilled: shape.props.fill !== 'none' } }) .lineTo(w, 0).lineTo(w, h).lineTo(0, h).close(), } // consumer-defined (via GeoShapeUtil.configure({ customGeoTypes })) 'rounded-rect': { snapType: 'polygon', icon: 'geo-rounded-rect', getPath: (w, h, shape) => /* ... */, } ``` ### Change type - [x] `improvement` ### Test plan 1. `yarn dev` and exercise every built-in geo type (rectangle, ellipse, triangle, diamond, star, pentagon, hexagon, octagon, rhombus, rhombus-2, oval, trapezoid, arrow-left/up/down/right, cloud, x-box, check-box, heart) — creation by click, creation by drag, label editing, fill/dash/color, snapping. 2. Confirm star and cloud still create at their special default sizes (200x190 and 300x180). 3. Confirm alt+double-click still toggles rectangle ↔ check-box. 4. Open the `Custom geo types` example and confirm both built-ins and custom types render in the style panel picker. 5. Confirm `customGeoTypes` collision warning still fires when a consumer registers a key that matches a built-in. - [x] Unit tests (existing geo tests pass — `GeoShapeUtil.test.tsx`, `GeoShapeTool.test.ts`) - [ ] End to end tests ### Release notes - The built-in geo types are now registered through the same `GeoTypeDefinition` system as `customGeoTypes`. Public-facing behavior is unchanged. ### API changes - Added `defaultGeoTypeDefinitions` (public) — the registry of built-in geo type definitions. - Added `getGeoTypeDefinition(name, customGeoTypes?)` (public) — lookup helper. - Removed the internal `getCustomGeoType` helper (replaced by `getGeoTypeDefinition`). ### Code changes | Section | LOC change | | --------------- | ---------- | | Core code | +319 / -225 | | Automated files | +115 / -0 | --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
1 parent 5405c3c commit c3d8c7a

8 files changed

Lines changed: 433 additions & 228 deletions

File tree

packages/tldraw/api-report.api.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,118 @@ export function DefaultFollowingIndicator(): JSX.Element | null;
11491149
// @public (undocumented)
11501150
export const DefaultFontFaces: TLDefaultFonts;
11511151

1152+
// @public
1153+
export const defaultGeoTypeDefinitions: {
1154+
readonly 'arrow-down': {
1155+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1156+
readonly icon: "geo-arrow-down";
1157+
readonly snapType: "polygon";
1158+
};
1159+
readonly 'arrow-left': {
1160+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1161+
readonly icon: "geo-arrow-left";
1162+
readonly snapType: "polygon";
1163+
};
1164+
readonly 'arrow-right': {
1165+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1166+
readonly icon: "geo-arrow-right";
1167+
readonly snapType: "polygon";
1168+
};
1169+
readonly 'arrow-up': {
1170+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1171+
readonly icon: "geo-arrow-up";
1172+
readonly snapType: "polygon";
1173+
};
1174+
readonly 'check-box': {
1175+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1176+
readonly icon: "geo-check-box";
1177+
readonly snapType: "polygon";
1178+
};
1179+
readonly 'rhombus-2': {
1180+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1181+
readonly icon: "geo-rhombus-2";
1182+
readonly snapType: "polygon";
1183+
};
1184+
readonly 'x-box': {
1185+
readonly getPath: (w: number, h: number, shape: TLGeoShape, strokeWidth: number) => PathBuilder;
1186+
readonly icon: "geo-x-box";
1187+
readonly snapType: "polygon";
1188+
};
1189+
readonly cloud: {
1190+
readonly defaultSize: {
1191+
readonly h: 180;
1192+
readonly w: 300;
1193+
};
1194+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1195+
readonly icon: "geo-cloud";
1196+
readonly snapType: "blobby";
1197+
};
1198+
readonly diamond: {
1199+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1200+
readonly icon: "geo-diamond";
1201+
readonly snapType: "polygon";
1202+
};
1203+
readonly ellipse: {
1204+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1205+
readonly icon: "geo-ellipse";
1206+
readonly snapType: "blobby";
1207+
};
1208+
readonly heart: {
1209+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1210+
readonly icon: "geo-heart";
1211+
readonly snapType: "blobby";
1212+
};
1213+
readonly hexagon: {
1214+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1215+
readonly icon: "geo-hexagon";
1216+
readonly snapType: "polygon";
1217+
};
1218+
readonly octagon: {
1219+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1220+
readonly icon: "geo-octagon";
1221+
readonly snapType: "polygon";
1222+
};
1223+
readonly oval: {
1224+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1225+
readonly icon: "geo-oval";
1226+
readonly snapType: "blobby";
1227+
};
1228+
readonly pentagon: {
1229+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1230+
readonly icon: "geo-pentagon";
1231+
readonly snapType: "polygon";
1232+
};
1233+
readonly rectangle: {
1234+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1235+
readonly icon: "geo-rectangle";
1236+
readonly snapType: "polygon";
1237+
};
1238+
readonly rhombus: {
1239+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1240+
readonly icon: "geo-rhombus";
1241+
readonly snapType: "polygon";
1242+
};
1243+
readonly star: {
1244+
readonly defaultSize: {
1245+
readonly h: 190;
1246+
readonly w: 200;
1247+
};
1248+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1249+
readonly icon: "geo-star";
1250+
readonly snapType: "polygon";
1251+
};
1252+
readonly trapezoid: {
1253+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1254+
readonly icon: "geo-trapezoid";
1255+
readonly snapType: "polygon";
1256+
};
1257+
readonly triangle: {
1258+
readonly getPath: (w: number, h: number, shape: TLGeoShape) => PathBuilder;
1259+
readonly icon: "geo-triangle";
1260+
readonly snapType: "polygon";
1261+
};
1262+
};
1263+
11521264
// @public (undocumented)
11531265
export function defaultHandleExternalEmbedContent<T>(editor: Editor, { point, url, embed }: {
11541266
embed: T;
@@ -2240,6 +2352,9 @@ export function getFontFamily(theme: TLTheme, font: string): string;
22402352
// @public
22412353
export function getFontStyleItems(theme: TLTheme): StyleValuesForUi<string>;
22422354

2355+
// @public
2356+
export function getGeoTypeDefinition(name: string, customGeoTypes?: Record<string, GeoTypeDefinition>): GeoTypeDefinition | undefined;
2357+
22432358
// @public (undocumented)
22442359
export function getHitShapeOnCanvasPointerDown(editor: Editor, hitLabels?: boolean): TLShape | undefined;
22452360

packages/tldraw/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,11 @@ export {
212212
type GeoShapeOptions,
213213
type GeoShapeUtilDisplayValues,
214214
} from './lib/shapes/geo/GeoShapeUtil'
215-
export { type GeoTypeDefinition } from './lib/shapes/geo/getGeoShapePath'
215+
export {
216+
defaultGeoTypeDefinitions,
217+
getGeoTypeDefinition,
218+
type GeoTypeDefinition,
219+
} from './lib/shapes/geo/getGeoShapePath'
216220
export { HighlightShapeTool } from './lib/shapes/highlight/HighlightShapeTool'
217221
export {
218222
HighlightShapeUtil,

packages/tldraw/src/lib/shapes/geo/GeoShapeUtil.tsx

Lines changed: 18 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ import { RichTextLabel, RichTextSVG } from '../shared/RichTextLabel'
5555
import { useIsReadyForEditing } from '../shared/useEditablePlainText'
5656
import { useEfficientZoomThreshold } from '../shared/useEfficientZoomThreshold'
5757
import { GeoShapeBody } from './GeoShapeBody'
58-
import { type GeoTypeDefinition, getCustomGeoType, getGeoShapePath } from './getGeoShapePath'
58+
import {
59+
defaultGeoTypeDefinitions,
60+
type GeoTypeDefinition,
61+
getGeoShapePath,
62+
getGeoTypeDefinition,
63+
} from './getGeoShapePath'
5964

6065
// imperfect but good enough, should be the width of the W in the font / size combo
6166
const GEO_SHAPE_MIN_WIDTHS = Object.freeze({
@@ -90,7 +95,7 @@ const GEO_SHAPE_EMPTY_LABEL_SIZE = Object.freeze({ w: 0, h: 0 })
9095
// by previous `configure()` calls. This lets repeat `configure()` calls reuse
9196
// the same custom key (e.g. when wrapping/extending the util) without having
9297
// the entry stripped from `options.customGeoTypes`.
93-
const BUILTIN_GEO_TYPES: ReadonlySet<string> = new Set(GeoShapeGeoStyle.values)
98+
const BUILTIN_GEO_TYPES: ReadonlySet<string> = new Set(Object.keys(defaultGeoTypeDefinitions))
9499

95100
/** @public */
96101
export interface GeoShapeUtilDisplayValues {
@@ -326,42 +331,15 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
326331
const geometry = this.getGeometry(shape)
327332
// we only want to snap handles to the outline of the shape - not to its label etc.
328333
const outline = geometry.children[0]
329-
switch (shape.props.geo) {
330-
case 'arrow-down':
331-
case 'arrow-left':
332-
case 'arrow-right':
333-
case 'arrow-up':
334-
case 'check-box':
335-
case 'diamond':
336-
case 'hexagon':
337-
case 'octagon':
338-
case 'pentagon':
339-
case 'rectangle':
340-
case 'rhombus':
341-
case 'rhombus-2':
342-
case 'star':
343-
case 'trapezoid':
344-
case 'triangle':
345-
case 'x-box':
346-
// poly-line type shapes hand snap points for each vertex & the center
347-
return { outline: outline, points: [...outline.vertices, geometry.bounds.center] }
348-
case 'cloud':
349-
case 'ellipse':
350-
case 'heart':
351-
case 'oval':
352-
// blobby shapes only have a snap point in their center
353-
return { outline: outline, points: [geometry.bounds.center] }
354-
default: {
355-
const customType = getCustomGeoType(shape.props.geo, this.options.customGeoTypes)
356-
if (customType) {
357-
if (customType.snapType === 'blobby') {
358-
return { outline: outline, points: [geometry.bounds.center] }
359-
}
360-
return { outline: outline, points: [...outline.vertices, geometry.bounds.center] }
361-
}
362-
throw new Error(`Unknown geo type: ${shape.props.geo}`)
363-
}
334+
const def = getGeoTypeDefinition(shape.props.geo, this.options.customGeoTypes)
335+
if (!def) {
336+
throw new Error(`Unknown geo type: ${shape.props.geo}`)
337+
}
338+
// blobby shapes only snap to the center; polygon shapes snap to vertices + center.
339+
if (def.snapType === 'blobby') {
340+
return { outline: outline, points: [geometry.bounds.center] }
364341
}
342+
return { outline: outline, points: [...outline.vertices, geometry.bounds.center] }
365343
}
366344

367345
override getText(shape: TLGeoShape) {
@@ -738,9 +716,9 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
738716
}
739717
}
740718

741-
const customType = getCustomGeoType(shape.props.geo, this.options.customGeoTypes)
742-
if (customType?.onDoubleClick) {
743-
const result = customType.onDoubleClick(shape)
719+
const def = getGeoTypeDefinition(shape.props.geo, this.options.customGeoTypes)
720+
if (def?.onDoubleClick) {
721+
const result = def.onDoubleClick(shape)
744722
if (result) {
745723
return { ...shape, props: { ...shape.props, ...result.props } }
746724
}

0 commit comments

Comments
 (0)