Styles API (#1580)

Removes `propsForNextShape` and replaces it with the new styles API. 

Changes in here:
- New custom style example
- `setProp` is now `setStyle` and takes a `StyleProp` instead of a
string
- `Editor.props` and `Editor.opacity` are now `Editor.sharedStyles` and
`Editor.sharedOpacity`
- They return an object that flags mixed vs shared types instead of
using null to signal mixed types
- `Editor.styles` returns a `SharedStyleMap` - keyed on `StyleProp`
instead of `string`
- `StateNode.shapeType` is now the shape util rather than just a string.
This lets us pull the styles from the shape type directly.
- `color` is no longer a core part of the editor set on the shape
parent. Individual child shapes have to use color directly.
- `propsForNextShape` is now `stylesForNextShape`
- `InstanceRecordType` is created at runtime in the same way
`ShapeRecordType` is. This is so it can pull style validators out of
shape defs for `stylesForNextShape`
- Shape type are now defined by their props rather than having separate
validators & type defs

### Change Type

- [x] `major` — Breaking change

### Test Plan

1. Big time regression testing around styles!
2. Check UI works as intended for all shape/style/tool combos

- [x] Unit Tests
- [ ] End to end tests

### Release Notes

-

---------

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
alex 2023-06-16 11:33:47 +01:00 committed by GitHub
parent f864d0cfbd
commit b88a2370b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
144 changed files with 2829 additions and 3311 deletions

View file

@ -1,12 +1,13 @@
diff --git a/lib/api/ExtractorConfig.js b/lib/api/ExtractorConfig.js
index a37db0d564a5662df78055ded63069a4b2706bb1..158d4b121fa0c7cf15c3a52570218a2fe67fc567 100644
index 31b46f8b51a2a93c2e538d67cd340e3c6897e242..d2cab369fc53cbd35bcb0c465783f851f1fd5f42 100644
--- a/lib/api/ExtractorConfig.js
+++ b/lib/api/ExtractorConfig.js
@@ -669,5 +669,5 @@ ExtractorConfig.FILENAME = 'api-extractor.json';
@@ -668,6 +668,6 @@ ExtractorConfig.FILENAME = 'api-extractor.json';
*/
ExtractorConfig._tsdocBaseFilePath = path.resolve(__dirname, '../../extends/tsdoc-base.json');
ExtractorConfig._defaultConfig = node_core_library_1.JsonFile.load(path.join(__dirname, '../schemas/api-extractor-defaults.json'));
-ExtractorConfig._declarationFileExtensionRegExp = /\.d\.ts$/i;
+ExtractorConfig._declarationFileExtensionRegExp = /\.d\.(m|c)?ts$/i;
exports.ExtractorConfig = ExtractorConfig;
//# sourceMappingURL=ExtractorConfig.js.map
\ No newline at end of file

View file

@ -39,6 +39,7 @@
"@tldraw/assets": "workspace:*",
"@tldraw/tldraw": "workspace:*",
"@tldraw/utils": "workspace:*",
"@tldraw/validate": "workspace:*",
"@vercel/analytics": "^1.0.1",
"lazyrepo": "0.0.0-alpha.27",
"react": "^18.2.0",

View file

@ -0,0 +1,107 @@
import {
BaseBoxShapeTool,
BaseBoxShapeUtil,
DefaultColorStyle,
HTMLContainer,
StyleProp,
TLBaseShape,
TLDefaultColorStyle,
defineShape,
} from '@tldraw/tldraw'
import { T } from '@tldraw/validate'
// Define a style that can be used across multiple shapes. The ID (myApp:filter) must be globally
// unique, so we recommend prefixing it with a namespace.
export const MyFilterStyle = StyleProp.defineEnum('myApp:filter', {
defaultValue: 'none',
values: ['none', 'invert', 'grayscale', 'blur'],
})
export type MyFilterStyle = T.TypeOf<typeof MyFilterStyle>
export type CardShape = TLBaseShape<
'card',
{
w: number
h: number
color: TLDefaultColorStyle
filter: MyFilterStyle
}
>
export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
// Id — the shape util's id
static override type = 'card' as const
// Flags — there are a LOT of other flags!
override isAspectRatioLocked = (_shape: CardShape) => false
override canResize = (_shape: CardShape) => true
override canBind = (_shape: CardShape) => true
// Default props — used for shapes created with the tool
override defaultProps(): CardShape['props'] {
return {
w: 300,
h: 300,
color: 'black',
filter: 'none',
}
}
// Render method — the React component that will be rendered for the shape
render(shape: CardShape) {
const bounds = this.bounds(shape)
return (
<HTMLContainer
id={shape.id}
style={{
border: `4px solid var(--palette-${shape.props.color})`,
borderRadius: 4,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'all',
filter: this.filterStyleToCss(shape.props.filter),
backgroundColor: `var(--palette-${shape.props.color}-semi)`,
}}
>
🍇🫐🍏🍋🍊🍒 {bounds.w.toFixed()}x{bounds.h.toFixed()} 🍒🍊🍋🍏🫐🍇
</HTMLContainer>
)
}
// Indicator — used when hovering over a shape or when it's selected; must return only SVG elements here
indicator(shape: CardShape) {
return <rect width={shape.props.w} height={shape.props.h} />
}
filterStyleToCss(filter: MyFilterStyle) {
if (filter === 'invert') return 'invert(100%)'
if (filter === 'grayscale') return 'grayscale(100%)'
if (filter === 'blur') return 'blur(10px)'
return 'none'
}
}
// Extending the base box shape tool gives us a lot of functionality for free.
export class CardShapeTool extends BaseBoxShapeTool {
static override id = 'card'
static override initial = 'idle'
override shapeType = CardShapeUtil
}
export const CardShape = defineShape('card', {
util: CardShapeUtil,
tool: CardShapeTool,
// to use a style prop, you need to describe all the props in your shape.
props: {
w: T.number,
h: T.number,
// You can re-use tldraw built-in styles...
color: DefaultColorStyle,
// ...or your own custom styles.
filter: MyFilterStyle,
},
})

View file

@ -0,0 +1,85 @@
import { TLUiMenuGroup, Tldraw, menuItem, toolbarItem, useEditor } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
import { TLUiOverrides } from '@tldraw/ui/src/lib/overrides'
import { track } from 'signia-react'
import { CardShape, MyFilterStyle } from './CardShape'
const shapes = [CardShape]
export default function CustomStylesExample() {
return (
<div className="tldraw__editor">
<Tldraw
autoFocus
persistenceKey="custom-styles-example"
shapes={shapes}
overrides={cardToolMenuItems}
>
<FilterStyleUi />
</Tldraw>
</div>
)
}
const FilterStyleUi = track(function FilterStyleUi() {
const editor = useEditor()
const filterStyle = editor.sharedStyles.get(MyFilterStyle)
// if the filter style isn't in sharedStyles, it means it's not relevant to the current tool/selection
if (!filterStyle) return null
return (
<div style={{ position: 'absolute', zIndex: 300, top: 64, left: 12 }}>
filter:{' '}
<select
value={filterStyle.type === 'mixed' ? 'mixed' : filterStyle.value}
onChange={(e) => editor.setStyle(MyFilterStyle, e.target.value)}
>
<option value="mixed" disabled>
Mixed
</option>
<option value="none">None</option>
<option value="invert">Invert</option>
<option value="grayscale">Grayscale</option>
<option value="blur">Blur</option>
</select>
</div>
)
})
const cardToolMenuItems: TLUiOverrides = {
// In order for our custom tool to show up in the UI...
// We need to add it to the tools list. This "toolItem"
// has information about its icon, label, keyboard shortcut,
// and what to do when it's selected.
tools(editor, tools) {
tools.card = {
id: 'card',
icon: 'color',
label: 'Card' as any,
kbd: 'c',
readonlyOk: false,
onSelect: () => {
editor.setSelectedTool('card')
},
}
return tools
},
toolbar(_app, toolbar, { tools }) {
// The toolbar is an array of items. We can add it to the
// end of the array or splice it in, then return the array.
toolbar.splice(4, 0, toolbarItem(tools.card))
return toolbar
},
keyboardShortcutsMenu(_app, keyboardShortcutsMenu, { tools }) {
// Same for the keyboard shortcuts menu, but this menu contains
// both items and groups. We want to find the "Tools" group and
// add it to that before returning the array.
const toolsGroup = keyboardShortcutsMenu.find(
(group) => group.id === 'shortcuts-dialog.tools'
) as TLUiMenuGroup
toolsGroup.children.push(menuItem(tools.card))
return keyboardShortcutsMenu
},
}

View file

@ -1,9 +1,10 @@
import {
createShapeId,
DefaultColorStyle,
Editor,
Tldraw,
TLGeoShape,
TLShapePartial,
Tldraw,
createShapeId,
useEditor,
} from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
@ -92,7 +93,7 @@ const InsideOfEditorContext = () => {
const interval = setInterval(() => {
const selection = [...editor.selectedIds]
editor.selectAll()
editor.setProp('color', i % 2 ? 'blue' : 'light-blue')
editor.setStyle(DefaultColorStyle, i % 2 ? 'blue' : 'light-blue')
editor.setSelectedIds(selection)
i++
}, 1000)

View file

@ -62,7 +62,7 @@ export class CardShapeTool extends BaseBoxShapeTool {
static override id = 'card'
static override initial = 'idle'
override shapeType = 'card'
override shapeType = CardShapeUtil
}
export const CardShape = defineShape('card', {

View file

@ -15,6 +15,7 @@ import UiEventsExample from './12-ui-events/UiEventsExample'
import StoreEventsExample from './13-store-events/StoreEventsExample'
import PersistenceExample from './14-persistence/PersistenceExample'
import ZonesExample from './15-custom-zones/ZonesExample'
import CustomStylesExample from './16-custom-styles/CustomStylesExample'
import ExampleApi from './2-api/APIExample'
import CustomConfigExample from './3-custom-config/CustomConfigExample'
import CustomUiExample from './4-custom-ui/CustomUiExample'
@ -108,6 +109,10 @@ export const allExamples: Example[] = [
path: '/yjs',
element: <YjsExample />,
},
{
path: '/custom-styles',
element: <CustomStylesExample />,
},
]
const router = createBrowserRouter(allExamples)

View file

@ -8,6 +8,7 @@
"references": [
{ "path": "../../packages/tldraw" },
{ "path": "../../packages/utils" },
{ "path": "../../packages/assets" }
{ "path": "../../packages/assets" },
{ "path": "../../packages/validate" }
]
}

View file

@ -47,7 +47,7 @@
"concurrently": "7.0.0",
"create-serve": "1.0.1",
"dotenv": "^16.0.3",
"esbuild": "^0.16.7",
"esbuild": "^0.18.3",
"fs-extra": "^11.1.0",
"lazyrepo": "0.0.0-alpha.27",
"nanoid": "4.0.2",

View file

@ -32,12 +32,12 @@ export async function run() {
const entryPoints = [`${rootDir}src/index.tsx`]
log({ cmd: 'esbuild', args: { entryPoints } })
esbuild.build({
const builder = await esbuild.context({
entryPoints,
outfile: `${rootDir}/dist/index.js`,
minify: false,
bundle: true,
incremental: true,
target: 'es6',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
@ -51,17 +51,23 @@ export async function run() {
define: {
'process.env.NODE_ENV': '"development"',
},
watch: {
onRebuild(err) {
if (err) {
log({ cmd: 'esbuild:error', args: { err } })
} else {
copyEditor({ log })
log({ cmd: 'esbuild:success', args: {} })
}
plugins: [
{
name: 'log-builds',
setup(build) {
build.onEnd((result) => {
if (result.errors.length) {
log({ cmd: 'esbuild:error', args: { err: result.errors } })
} else {
copyEditor({ log })
log({ cmd: 'esbuild:success', args: {} })
}
})
},
},
},
],
})
await builder.watch()
} catch (error) {
log({ cmd: 'esbuild:error', args: { error } })
throw error

View file

@ -139,7 +139,7 @@
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",
"assert": "^2.0.0",
"esbuild": "^0.16.7",
"esbuild": "^0.18.3",
"fs-extra": "^11.1.0",
"lazyrepo": "0.0.0-alpha.27",
"lodash": "^4.17.21",

View file

@ -14,7 +14,7 @@ async function dev() {
log({ cmd: 'esbuild', args: { entryPoints } })
try {
esbuild.build({
const builder = await esbuild.context({
entryPoints,
outdir: join(rootDir, 'dist', 'web'),
minify: false,
@ -28,16 +28,21 @@ async function dev() {
},
tsconfig: './tsconfig.json',
external: ['vscode'],
incremental: true,
watch: {
onRebuild(err) {
if (err) {
log({ cmd: 'esbuild:error', args: { error: err } })
} else {
log({ cmd: 'esbuild:success', args: {} })
}
plugins: [
{
name: 'log-builds',
setup(build) {
build.onEnd((result) => {
if (result.errors.length) {
log({ cmd: 'esbuild:error', args: { err: result.errors } })
} else {
copyEditor({ log })
log({ cmd: 'esbuild:success', args: {} })
}
})
},
},
},
],
loader: {
'.woff2': 'dataurl',
'.woff': 'dataurl',
@ -46,6 +51,7 @@ async function dev() {
'.json': 'file',
},
})
await builder.watch()
} catch (error) {
log({ cmd: 'esbuild:error', args: { error } })
throw error

View file

@ -85,7 +85,7 @@
"typescript": "^5.0.2"
},
"devDependencies": {
"@microsoft/api-extractor": "^7.34.1",
"@microsoft/api-extractor": "^7.35.4",
"@swc/core": "^1.3.55",
"@swc/jest": "^0.2.26",
"@types/glob": "^8.1.0",
@ -97,6 +97,6 @@
"vercel": "^28.16.15"
},
"resolutions": {
"@microsoft/api-extractor@^7.34.1": "patch:@microsoft/api-extractor@npm%3A7.34.1#./.yarn/patches/@microsoft-api-extractor-npm-7.34.1-af268a32f8.patch"
"@microsoft/api-extractor@^7.35.4": "patch:@microsoft/api-extractor@npm%3A7.35.4#./.yarn/patches/@microsoft-api-extractor-npm-7.35.4-5f4f0357b4.patch"
}
}

View file

@ -35,9 +35,9 @@ import { Signal } from 'signia';
import { StoreSchema } from '@tldraw/store';
import { StoreSnapshot } from '@tldraw/store';
import { StrokePoint } from '@tldraw/primitives';
import { TLAlignType } from '@tldraw/tlschema';
import { TLArrowheadType } from '@tldraw/tlschema';
import { StyleProp } from '@tldraw/tlschema';
import { TLArrowShape } from '@tldraw/tlschema';
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema';
import { TLAsset } from '@tldraw/tlschema';
import { TLAssetId } from '@tldraw/tlschema';
import { TLAssetPartial } from '@tldraw/tlschema';
@ -45,13 +45,12 @@ import { TLBaseShape } from '@tldraw/tlschema';
import { TLBookmarkAsset } from '@tldraw/tlschema';
import { TLBookmarkShape } from '@tldraw/tlschema';
import { TLCamera } from '@tldraw/tlschema';
import { TLColorStyle } from '@tldraw/tlschema';
import { TLColorType } from '@tldraw/tlschema';
import { TLCursor } from '@tldraw/tlschema';
import { TLDefaultColorStyle } from '@tldraw/tlschema';
import { TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema';
import { TLDocument } from '@tldraw/tlschema';
import { TLDrawShape } from '@tldraw/tlschema';
import { TLEmbedShape } from '@tldraw/tlschema';
import { TLFontType } from '@tldraw/tlschema';
import { TLFrameShape } from '@tldraw/tlschema';
import { TLGeoShape } from '@tldraw/tlschema';
import { TLGroupShape } from '@tldraw/tlschema';
@ -62,10 +61,8 @@ import { TLImageShape } from '@tldraw/tlschema';
import { TLInstance } from '@tldraw/tlschema';
import { TLInstancePageState } from '@tldraw/tlschema';
import { TLInstancePresence } from '@tldraw/tlschema';
import { TLInstancePropsForNextShape } from '@tldraw/tlschema';
import { TLLineShape } from '@tldraw/tlschema';
import { TLNoteShape } from '@tldraw/tlschema';
import { TLNullableShapeProps } from '@tldraw/tlschema';
import { TLPage } from '@tldraw/tlschema';
import { TLPageId } from '@tldraw/tlschema';
import { TLParentId } from '@tldraw/tlschema';
@ -74,16 +71,9 @@ import { TLScribble } from '@tldraw/tlschema';
import { TLShape } from '@tldraw/tlschema';
import { TLShapeId } from '@tldraw/tlschema';
import { TLShapePartial } from '@tldraw/tlschema';
import { TLShapeProp } from '@tldraw/tlschema';
import { TLShapeProps } from '@tldraw/tlschema';
import { TLSizeStyle } from '@tldraw/tlschema';
import { TLSizeType } from '@tldraw/tlschema';
import { TLStore } from '@tldraw/tlschema';
import { TLStoreProps } from '@tldraw/tlschema';
import { TLStyleCollections } from '@tldraw/tlschema';
import { TLStyleType } from '@tldraw/tlschema';
import { TLTextShape } from '@tldraw/tlschema';
import { TLTextShapeProps } from '@tldraw/tlschema';
import { TLUnknownShape } from '@tldraw/tlschema';
import { TLVideoAsset } from '@tldraw/tlschema';
import { TLVideoShape } from '@tldraw/tlschema';
@ -107,9 +97,6 @@ export const ANIMATION_MEDIUM_MS = 320;
// @internal (undocumented)
export const ANIMATION_SHORT_MS = 80;
// @public (undocumented)
export const ARROW_LABEL_FONT_SIZES: Record<TLSizeType, number>;
// @public (undocumented)
export const ArrowShape: TLShapeInfo<TLArrowShape>;
@ -186,7 +173,7 @@ export abstract class BaseBoxShapeTool extends StateNode {
// (undocumented)
static initial: string;
// (undocumented)
abstract shapeType: string;
abstract shapeType: TLShapeUtilConstructor<any>;
}
// @public (undocumented)
@ -233,9 +220,6 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
static type: "bookmark";
}
// @internal (undocumented)
export const BOUND_ARROW_OFFSET = 10;
// @public (undocumented)
export const Canvas: React_3.MemoExoticComponent<({ onDropOverride, }: {
onDropOverride?: ((defaultOnDrop: (e: React_3.DragEvent<Element>) => Promise<void>) => (e: React_3.DragEvent<Element>) => Promise<void>) | undefined;
@ -289,12 +273,6 @@ export const DEFAULT_ANIMATION_OPTIONS: {
easing: (t: number) => number;
};
// @internal (undocumented)
export const DEFAULT_BOOKMARK_HEIGHT = 320;
// @internal (undocumented)
export const DEFAULT_BOOKMARK_WIDTH = 300;
// @public (undocumented)
export let defaultEditorAssetUrls: TLEditorAssetUrls;
@ -475,7 +453,6 @@ export class Editor extends EventEmitter<TLEventMap> {
getClipPathById(id: TLShapeId): string | undefined;
getContainer: () => HTMLElement;
getContent(ids?: TLShapeId[]): TLContent | undefined;
getCssColor(id: TLColorStyle['id']): string;
getDeltaInParentSpace(shape: TLShape, delta: VecLike): Vec2d;
getDeltaInShapeSpace(shape: TLShape, delta: VecLike): Vec2d;
getDroppingShape(point: VecLike, droppingShapes?: TLShape[]): TLUnknownShape | undefined;
@ -517,7 +494,8 @@ export class Editor extends EventEmitter<TLEventMap> {
getShapeUtil<S extends TLUnknownShape>(shape: S | TLShapePartial<S>): ShapeUtil<S>;
getSortedChildIds(parentId: TLParentId): TLShapeId[];
getStateDescendant(path: string): StateNode | undefined;
getStrokeWidth(id: TLSizeStyle['id']): number;
// @internal (undocumented)
getStyleForNextShape<T>(style: StyleProp<T>): T;
getSvg(ids?: TLShapeId[], opts?: Partial<{
scale: number;
background: boolean;
@ -587,7 +565,6 @@ export class Editor extends EventEmitter<TLEventMap> {
moveShapesToPage(ids: TLShapeId[], pageId: TLPageId): this;
nudgeShapes(ids: TLShapeId[], direction: Vec2dModel, major?: boolean, ephemeral?: boolean): this;
get onlySelectedShape(): null | TLShape;
get opacity(): null | number;
get openMenus(): string[];
packShapes(ids?: TLShapeId[], padding?: number): this;
get pages(): TLPage[];
@ -602,8 +579,6 @@ export class Editor extends EventEmitter<TLEventMap> {
popFocusLayer(): this;
// @internal (undocumented)
get projectName(): string;
// @internal
get props(): null | TLNullableShapeProps;
putContent(content: TLContent, options?: {
point?: VecLike;
select?: boolean;
@ -675,12 +650,12 @@ export class Editor extends EventEmitter<TLEventMap> {
setPenMode(isPenMode: boolean): this;
// @internal (undocumented)
setProjectName(name: string): void;
setProp(key: TLShapeProp, value: any, ephemeral?: boolean, squashing?: boolean): this;
setReadOnly(isReadOnly: boolean): this;
setScribble(scribble?: null | TLScribble): this;
setSelectedIds(ids: TLShapeId[], squashing?: boolean): this;
setSelectedTool(id: string, info?: {}): this;
setSnapMode(isSnapMode: boolean): this;
setStyle<T>(style: StyleProp<T>, value: T, ephemeral?: boolean, squashing?: boolean): this;
setToolLocked(isToolLocked: boolean): this;
setZoomBrush(zoomBrush?: Box2dModel | null): this;
get shapeIds(): Set<TLShapeId>;
@ -688,6 +663,8 @@ export class Editor extends EventEmitter<TLEventMap> {
shapeUtils: {
readonly [K in string]?: ShapeUtil<TLUnknownShape>;
};
get sharedOpacity(): SharedStyle<number>;
get sharedStyles(): ReadonlySharedStyleMap;
slideCamera(opts?: {
speed: number;
direction: Vec2d;
@ -702,7 +679,6 @@ export class Editor extends EventEmitter<TLEventMap> {
stopFollowingUser(): this;
readonly store: TLStore;
stretchShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[]): this;
static styles: TLStyleCollections;
textMeasure: TextManager;
toggleLock(ids?: TLShapeId[]): this;
undo(): HistoryManager<this>;
@ -794,15 +770,6 @@ export const featureFlags: {
// @public
export function fileToBase64(file: Blob): Promise<string>;
// @public (undocumented)
export const FONT_ALIGNMENT: Record<TLAlignType, string>;
// @public (undocumented)
export const FONT_FAMILIES: Record<TLFontType, string>;
// @public (undocumented)
export const FONT_SIZES: Record<TLSizeType, number>;
// @public (undocumented)
export const FrameShape: TLShapeInfo<TLFrameShape>;
@ -870,7 +837,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
dash: "dashed" | "dotted" | "draw" | "solid";
size: "l" | "m" | "s" | "xl";
font: "draw" | "mono" | "sans" | "serif";
align: "end" | "middle" | "start";
align: "end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start";
verticalAlign: "end" | "middle" | "start";
url: string;
w: number;
@ -899,7 +866,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
dash: "dashed" | "dotted" | "draw" | "solid";
size: "l" | "m" | "s" | "xl";
font: "draw" | "mono" | "sans" | "serif";
align: "end" | "middle" | "start";
align: "end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start";
verticalAlign: "end" | "middle" | "start";
url: string;
w: number;
@ -1152,9 +1119,6 @@ export function HTMLContainer({ children, className, ...rest }: HTMLContainerPro
// @public (undocumented)
export type HTMLContainerProps = React_3.HTMLAttributes<HTMLDivElement>;
// @public (undocumented)
export const ICON_SIZES: Record<TLSizeType, number>;
// @public (undocumented)
export const ImageShape: TLShapeInfo<TLImageShape>;
@ -1204,9 +1168,6 @@ export const isValidHttpURL: (url: string) => boolean;
// @public (undocumented)
export function isValidUrl(url: string): boolean;
// @public (undocumented)
export const LABEL_FONT_SIZES: Record<TLSizeType, number>;
// @public (undocumented)
export const LineShape: TLShapeInfo<TLLineShape>;
@ -1269,18 +1230,6 @@ export const MAJOR_NUDGE_FACTOR = 10;
// @public (undocumented)
export function matchEmbedUrl(url: string): {
definition: {
readonly type: "tldraw";
readonly title: "tldraw";
readonly hostnames: readonly ["beta.tldraw.com", "lite.tldraw.com", "www.tldraw.com"];
readonly minWidth: 300;
readonly minHeight: 300;
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly canUnmount: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
} | {
readonly type: "codepen";
readonly title: "Codepen";
readonly hostnames: readonly ["codepen.io"];
@ -1424,6 +1373,18 @@ export function matchEmbedUrl(url: string): {
readonly canUnmount: false;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
} | {
readonly type: "tldraw";
readonly title: "tldraw";
readonly hostnames: readonly ["beta.tldraw.com", "lite.tldraw.com", "www.tldraw.com"];
readonly minWidth: 300;
readonly minHeight: 300;
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly canUnmount: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
} | {
readonly type: "vimeo";
readonly title: "Vimeo";
@ -1457,18 +1418,6 @@ export function matchEmbedUrl(url: string): {
// @public (undocumented)
export function matchUrl(url: string): {
definition: {
readonly type: "tldraw";
readonly title: "tldraw";
readonly hostnames: readonly ["beta.tldraw.com", "lite.tldraw.com", "www.tldraw.com"];
readonly minWidth: 300;
readonly minHeight: 300;
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly canUnmount: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
} | {
readonly type: "codepen";
readonly title: "Codepen";
readonly hostnames: readonly ["codepen.io"];
@ -1612,6 +1561,18 @@ export function matchUrl(url: string): {
readonly canUnmount: false;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
} | {
readonly type: "tldraw";
readonly title: "tldraw";
readonly hostnames: readonly ["beta.tldraw.com", "lite.tldraw.com", "www.tldraw.com"];
readonly minWidth: 300;
readonly minHeight: 300;
readonly width: 720;
readonly height: 500;
readonly doesResize: true;
readonly canUnmount: true;
readonly toEmbedUrl: (url: string) => string | undefined;
readonly fromEmbedUrl: (url: string) => string | undefined;
} | {
readonly type: "vimeo";
readonly title: "Vimeo";
@ -1657,9 +1618,6 @@ export const MAX_SHAPES_PER_PAGE = 2000;
// @internal (undocumented)
export const MAX_ZOOM = 8;
// @internal (undocumented)
export const MIN_ARROW_LENGTH = 48;
// @internal (undocumented)
export const MIN_ZOOM = 0.1;
@ -1708,7 +1666,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
color: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow";
size: "l" | "m" | "s" | "xl";
font: "draw" | "mono" | "sans" | "serif";
align: "end" | "middle" | "start";
align: "end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start";
verticalAlign: "end" | "middle" | "start";
url: string;
text: string;
@ -1731,7 +1689,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
color: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow";
size: "l" | "m" | "s" | "xl";
font: "draw" | "mono" | "sans" | "serif";
align: "end" | "middle" | "start";
align: "end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start";
verticalAlign: "end" | "middle" | "start";
url: string;
text: string;
@ -1796,6 +1754,29 @@ export class PlopManager {
// @public
export function preventDefault(event: Event | React_2.BaseSyntheticEvent): void;
// @public (undocumented)
export class ReadonlySharedStyleMap {
// (undocumented)
[Symbol.iterator](): IterableIterator<[StyleProp<unknown>, SharedStyle<unknown>]>;
constructor(entries?: Iterable<[StyleProp<unknown>, SharedStyle<unknown>]>);
// (undocumented)
entries(): IterableIterator<[StyleProp<unknown>, SharedStyle<unknown>]>;
// (undocumented)
equals(other: ReadonlySharedStyleMap): boolean;
// (undocumented)
get<T>(prop: StyleProp<T>): SharedStyle<T> | undefined;
// (undocumented)
getAsKnownValue<T>(prop: StyleProp<T>): T | undefined;
// (undocumented)
keys(): IterableIterator<StyleProp<unknown>>;
// (undocumented)
protected map: Map<StyleProp<unknown>, SharedStyle<unknown>>;
// (undocumented)
get size(): number;
// (undocumented)
values(): IterableIterator<SharedStyle<unknown>>;
}
// @public (undocumented)
export function refreshPage(): void;
@ -1824,9 +1805,6 @@ export function setDefaultEditorAssetUrls(assetUrls: TLEditorAssetUrls): void;
// @public (undocumented)
export function setPointerCapture(element: Element, event: PointerEvent | React_2.PointerEvent<Element>): void;
// @public (undocumented)
export function setPropsForNextShape(previousProps: TLInstancePropsForNextShape, newProps: Partial<TLShapeProps>): TLInstancePropsForNextShape;
// @public (undocumented)
export function setRuntimeOverrides(input: Partial<typeof runtime>): void;
@ -1834,80 +1812,106 @@ export function setRuntimeOverrides(input: Partial<typeof runtime>): void;
export function setUserPreferences(user: TLUserPreferences): void;
// @public (undocumented)
export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
constructor(editor: Editor, type: T['type']);
bounds(shape: T): Box2d;
canBind: <K>(_shape: T, _otherShape?: K | undefined) => boolean;
canCrop: TLShapeUtilFlag<T>;
canDropShapes(shape: T, shapes: TLShape[]): boolean;
canEdit: TLShapeUtilFlag<T>;
canReceiveNewChildrenOfType(shape: T, type: TLShape['type']): boolean;
canResize: TLShapeUtilFlag<T>;
canScroll: TLShapeUtilFlag<T>;
canSnap: TLShapeUtilFlag<T>;
canUnmount: TLShapeUtilFlag<T>;
center(shape: T): Vec2d;
abstract defaultProps(): T['props'];
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
constructor(editor: Editor, type: Shape['type'], styleProps: ReadonlyMap<StyleProp<unknown>, string>);
bounds(shape: Shape): Box2d;
canBind: <K>(_shape: Shape, _otherShape?: K | undefined) => boolean;
canCrop: TLShapeUtilFlag<Shape>;
canDropShapes(shape: Shape, shapes: TLShape[]): boolean;
canEdit: TLShapeUtilFlag<Shape>;
canReceiveNewChildrenOfType(shape: Shape, type: TLShape['type']): boolean;
canResize: TLShapeUtilFlag<Shape>;
canScroll: TLShapeUtilFlag<Shape>;
canSnap: TLShapeUtilFlag<Shape>;
canUnmount: TLShapeUtilFlag<Shape>;
center(shape: Shape): Vec2d;
abstract defaultProps(): Shape['props'];
// (undocumented)
editor: Editor;
// @internal (undocumented)
expandSelectionOutlinePx(shape: T): number;
protected abstract getBounds(shape: T): Box2d;
abstract getCenter(shape: T): Vec2d;
getEditingBounds: (shape: T) => Box2d;
protected getHandles?(shape: T): TLHandle[];
protected abstract getOutline(shape: T): Vec2d[];
protected getOutlineSegments(shape: T): Vec2d[][];
handles(shape: T): TLHandle[];
hideResizeHandles: TLShapeUtilFlag<T>;
hideRotateHandle: TLShapeUtilFlag<T>;
hideSelectionBoundsBg: TLShapeUtilFlag<T>;
hideSelectionBoundsFg: TLShapeUtilFlag<T>;
hitTestLineSegment(shape: T, A: VecLike, B: VecLike): boolean;
hitTestPoint(shape: T, point: VecLike): boolean;
abstract indicator(shape: T): any;
isAspectRatioLocked: TLShapeUtilFlag<T>;
isClosed: TLShapeUtilFlag<T>;
onBeforeCreate?: TLOnBeforeCreateHandler<T>;
onBeforeUpdate?: TLOnBeforeUpdateHandler<T>;
expandSelectionOutlinePx(shape: Shape): number;
protected abstract getBounds(shape: Shape): Box2d;
abstract getCenter(shape: Shape): Vec2d;
getEditingBounds: (shape: Shape) => Box2d;
protected getHandles?(shape: Shape): TLHandle[];
protected abstract getOutline(shape: Shape): Vec2d[];
protected getOutlineSegments(shape: Shape): Vec2d[][];
// (undocumented)
getStyleIfExists<T>(style: StyleProp<T>, shape: Shape | TLShapePartial<Shape>): T | undefined;
handles(shape: Shape): TLHandle[];
// (undocumented)
hasStyle(style: StyleProp<unknown>): boolean;
hideResizeHandles: TLShapeUtilFlag<Shape>;
hideRotateHandle: TLShapeUtilFlag<Shape>;
hideSelectionBoundsBg: TLShapeUtilFlag<Shape>;
hideSelectionBoundsFg: TLShapeUtilFlag<Shape>;
hitTestLineSegment(shape: Shape, A: VecLike, B: VecLike): boolean;
hitTestPoint(shape: Shape, point: VecLike): boolean;
abstract indicator(shape: Shape): any;
isAspectRatioLocked: TLShapeUtilFlag<Shape>;
isClosed: TLShapeUtilFlag<Shape>;
// (undocumented)
iterateStyles(shape: Shape | TLShapePartial<Shape>): Generator<[StyleProp<unknown>, unknown], void, unknown>;
onBeforeCreate?: TLOnBeforeCreateHandler<Shape>;
onBeforeUpdate?: TLOnBeforeUpdateHandler<Shape>;
// @internal
onBindingChange?: TLOnBindingChangeHandler<T>;
onChildrenChange?: TLOnChildrenChangeHandler<T>;
onClick?: TLOnClickHandler<T>;
onDoubleClick?: TLOnDoubleClickHandler<T>;
onDoubleClickEdge?: TLOnDoubleClickHandler<T>;
onDoubleClickHandle?: TLOnDoubleClickHandleHandler<T>;
onDragShapesOut?: TLOnDragHandler<T>;
onDragShapesOver?: TLOnDragHandler<T, {
onBindingChange?: TLOnBindingChangeHandler<Shape>;
onChildrenChange?: TLOnChildrenChangeHandler<Shape>;
onClick?: TLOnClickHandler<Shape>;
onDoubleClick?: TLOnDoubleClickHandler<Shape>;
onDoubleClickEdge?: TLOnDoubleClickHandler<Shape>;
onDoubleClickHandle?: TLOnDoubleClickHandleHandler<Shape>;
onDragShapesOut?: TLOnDragHandler<Shape>;
onDragShapesOver?: TLOnDragHandler<Shape, {
shouldHint: boolean;
}>;
onDropShapesOver?: TLOnDragHandler<T>;
onEditEnd?: TLOnEditEndHandler<T>;
onHandleChange?: TLOnHandleChangeHandler<T>;
onResize?: TLOnResizeHandler<T>;
onResizeEnd?: TLOnResizeEndHandler<T>;
onResizeStart?: TLOnResizeStartHandler<T>;
onRotate?: TLOnRotateHandler<T>;
onRotateEnd?: TLOnRotateEndHandler<T>;
onRotateStart?: TLOnRotateStartHandler<T>;
onTranslate?: TLOnTranslateHandler<T>;
onTranslateEnd?: TLOnTranslateEndHandler<T>;
onTranslateStart?: TLOnTranslateStartHandler<T>;
outline(shape: T): Vec2d[];
outlineSegments(shape: T): Vec2d[][];
onDropShapesOver?: TLOnDragHandler<Shape>;
onEditEnd?: TLOnEditEndHandler<Shape>;
onHandleChange?: TLOnHandleChangeHandler<Shape>;
onResize?: TLOnResizeHandler<Shape>;
onResizeEnd?: TLOnResizeEndHandler<Shape>;
onResizeStart?: TLOnResizeStartHandler<Shape>;
onRotate?: TLOnRotateHandler<Shape>;
onRotateEnd?: TLOnRotateEndHandler<Shape>;
onRotateStart?: TLOnRotateStartHandler<Shape>;
onTranslate?: TLOnTranslateHandler<Shape>;
onTranslateEnd?: TLOnTranslateEndHandler<Shape>;
onTranslateStart?: TLOnTranslateStartHandler<Shape>;
outline(shape: Shape): Vec2d[];
outlineSegments(shape: Shape): Vec2d[][];
// @internal
providesBackgroundForChildren(shape: T): boolean;
abstract render(shape: T): any;
providesBackgroundForChildren(shape: Shape): boolean;
abstract render(shape: Shape): any;
// @internal
renderBackground?(shape: T): any;
snapPoints(shape: T): Vec2d[];
toBackgroundSvg?(shape: T, font: string | undefined, colors: TLExportColors): null | Promise<SVGElement> | SVGElement;
toSvg?(shape: T, font: string | undefined, colors: TLExportColors): Promise<SVGElement> | SVGElement;
renderBackground?(shape: Shape): any;
// (undocumented)
readonly type: T['type'];
setStyleInPartial<T>(style: StyleProp<T>, shape: TLShapePartial<Shape>, value: T): TLShapePartial<Shape>;
snapPoints(shape: Shape): Vec2d[];
// (undocumented)
readonly styleProps: ReadonlyMap<StyleProp<unknown>, string>;
toBackgroundSvg?(shape: Shape, font: string | undefined, colors: TLExportColors): null | Promise<SVGElement> | SVGElement;
toSvg?(shape: Shape, font: string | undefined, colors: TLExportColors): Promise<SVGElement> | SVGElement;
// (undocumented)
readonly type: Shape['type'];
static type: string;
}
// @public (undocumented)
export type SharedStyle<T> = {
readonly type: 'mixed';
} | {
readonly type: 'shared';
readonly value: T;
};
// @internal (undocumented)
export class SharedStyleMap extends ReadonlySharedStyleMap {
// (undocumented)
applyValue<T>(prop: StyleProp<T>, value: T): void;
// (undocumented)
set<T>(prop: StyleProp<T>, value: SharedStyle<T>): void;
}
// @public (undocumented)
export function snapToGrid(n: number, gridSize: number): number;
@ -1981,18 +1985,13 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
// (undocumented)
path: Computed<string>;
// (undocumented)
shapeType?: string;
// (undocumented)
readonly styles: TLStyleType[];
shapeType?: TLShapeUtilConstructor<TLBaseShape<any, any>>;
// (undocumented)
transition(id: string, info: any): this;
// (undocumented)
type: TLStateNodeType;
}
// @public (undocumented)
export const STYLES: TLStyleCollections;
// @internal (undocumented)
export const SVG_PADDING = 32;
@ -2005,16 +2004,6 @@ export type SVGContainerProps = React_3.HTMLAttributes<SVGElement>;
// @public
export const TAB_ID: string;
// @public (undocumented)
export const TEXT_PROPS: {
lineHeight: number;
fontWeight: string;
fontVariant: string;
fontStyle: string;
padding: string;
maxWidth: string;
};
// @public (undocumented)
export const TextShape: TLShapeInfo<TLTextShape>;
@ -2049,7 +2038,16 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
parentId: TLParentId;
isLocked: boolean;
opacity: number;
props: TLTextShapeProps;
props: {
color: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow";
size: "l" | "m" | "s" | "xl";
font: "draw" | "mono" | "sans" | "serif";
align: "end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start";
w: number;
text: string;
scale: number;
autoSize: boolean;
};
id: TLShapeId;
typeName: "shape";
} | undefined;
@ -2062,7 +2060,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
color: "black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow";
size: "l" | "m" | "s" | "xl";
font: "draw" | "mono" | "sans" | "serif";
align: "end" | "middle" | "start";
align: "end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start";
text: string;
scale: number;
autoSize: boolean;
@ -2616,7 +2614,7 @@ export type TLShapeInfo<T extends TLUnknownShape = TLUnknownShape> = {
// @public (undocumented)
export interface TLShapeUtilConstructor<T extends TLUnknownShape, U extends ShapeUtil<T> = ShapeUtil<T>> {
// (undocumented)
new (editor: Editor, type: T['type']): U;
new (editor: Editor, type: T['type'], styleProps: ReadonlyMap<StyleProp<unknown>, string>): U;
// (undocumented)
type: T['type'];
}
@ -2634,8 +2632,6 @@ export interface TLStateNodeConstructor {
id: string;
// (undocumented)
initial?: string;
// (undocumented)
styles?: TLStyleType[];
}
// @public (undocumented)
@ -2772,9 +2768,6 @@ export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
static type: "video";
}
// @internal (undocumented)
export const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10;
// @public (undocumented)
export class WeakMapCache<T extends object, K> {
// (undocumented)

View file

@ -51,22 +51,13 @@ export { defineShape, type TLShapeInfo } from './lib/config/defineShape'
export {
ANIMATION_MEDIUM_MS,
ANIMATION_SHORT_MS,
ARROW_LABEL_FONT_SIZES,
BOUND_ARROW_OFFSET,
DEFAULT_ANIMATION_OPTIONS,
DEFAULT_BOOKMARK_HEIGHT,
DEFAULT_BOOKMARK_WIDTH,
DOUBLE_CLICK_DURATION,
DRAG_DISTANCE,
FONT_ALIGNMENT,
FONT_FAMILIES,
FONT_SIZES,
GRID_INCREMENT,
GRID_STEPS,
HAND_TOOL_FRICTION,
HASH_PATERN_ZOOM_NAMES,
ICON_SIZES,
LABEL_FONT_SIZES,
MAJOR_NUDGE_FACTOR,
MAX_ASSET_HEIGHT,
MAX_ASSET_WIDTH,
@ -74,15 +65,11 @@ export {
MAX_SHAPES_PER_PAGE,
MAX_ZOOM,
MINOR_NUDGE_FACTOR,
MIN_ARROW_LENGTH,
MIN_ZOOM,
MULTI_CLICK_DURATION,
REMOVE_SYMBOL,
RICH_TYPES,
STYLES,
SVG_PADDING,
TEXT_PROPS,
WAY_TOO_BIG_ARROW_BEND_FACTOR,
ZOOMS,
} from './lib/constants'
export { Editor, type TLAnimationOptions, type TLEditorOptions } from './lib/editor/Editor'
@ -198,6 +185,11 @@ export { usePresence } from './lib/hooks/usePresence'
export { useQuickReactor } from './lib/hooks/useQuickReactor'
export { useReactor } from './lib/hooks/useReactor'
export { useTLStore } from './lib/hooks/useTLStore'
export {
ReadonlySharedStyleMap,
SharedStyleMap,
type SharedStyle,
} from './lib/utils/SharedStylesMap'
export { WeakMapCache } from './lib/utils/WeakMapCache'
export {
ACCEPTED_ASSET_TYPE,
@ -253,7 +245,6 @@ export {
} from './lib/utils/export'
export { hardResetEditor } from './lib/utils/hard-reset'
export { isAnimated, isGIF } from './lib/utils/is-gif-animated'
export { setPropsForNextShape } from './lib/utils/props-for-next-shape'
export { refreshPage } from './lib/utils/refresh-page'
export { runtime, setRuntimeOverrides } from './lib/utils/runtime'
export {

View file

@ -66,16 +66,13 @@ export const Shape = track(function Shape({
)
useQuickReactor(
'set shape container clip path / color',
'set shape container clip path',
() => {
const shape = editor.getShapeById(id)
if (!shape) return null
const clipPath = editor.getClipPathById(id)
setProperty('clip-path', clipPath ?? 'none')
if ('color' in shape.props) {
setProperty('color', editor.getCssColor(shape.props.color))
}
},
[editor, setProperty]
)

View file

@ -8,7 +8,6 @@ import {
import {
CameraRecordType,
InstancePageStateRecordType,
InstanceRecordType,
TLINSTANCE_ID,
TLPageId,
TLRecord,
@ -233,7 +232,7 @@ export function loadSessionStateSnapshotIntoStore(
removed: {},
updated: {},
added: {
[TLINSTANCE_ID]: InstanceRecordType.create({
[TLINSTANCE_ID]: store.schema.types.instance.create({
id: TLINSTANCE_ID,
currentPageId: res.currentPageId,
isDebugMode: res.isDebugMode,

View file

@ -1,5 +1,4 @@
import { EASINGS } from '@tldraw/primitives'
import { TLAlignType, TLFontType, TLSizeType, TLStyleCollections } from '@tldraw/tlschema'
/** @internal */
export const MAX_SHAPES_PER_PAGE = 2000
@ -87,19 +86,6 @@ export const DEFAULT_ANIMATION_OPTIONS = {
/** @internal */
export const HAND_TOOL_FRICTION = 0.09
/** @internal */
export const MIN_ARROW_LENGTH = 48
/** @internal */
export const BOUND_ARROW_OFFSET = 10
/** @internal */
export const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10
/** @internal */
export const DEFAULT_BOOKMARK_WIDTH = 300
/** @internal */
export const DEFAULT_BOOKMARK_HEIGHT = 320
/** @public */
export const GRID_STEPS = [
{ min: -1, mid: 0.15, step: 100 },
@ -108,172 +94,6 @@ export const GRID_STEPS = [
{ min: 0.7, mid: 2.5, step: 1 },
]
/** @public */
export const TEXT_PROPS = {
lineHeight: 1.35,
fontWeight: 'normal',
fontVariant: 'normal',
fontStyle: 'normal',
padding: '0px',
maxWidth: 'auto',
}
/** @public */
export const FONT_SIZES: Record<TLSizeType, number> = {
s: 18,
m: 24,
l: 36,
xl: 44,
}
/** @public */
export const LABEL_FONT_SIZES: Record<TLSizeType, number> = {
s: 18,
m: 22,
l: 26,
xl: 32,
}
/** @public */
export const ARROW_LABEL_FONT_SIZES: Record<TLSizeType, number> = {
s: 18,
m: 20,
l: 24,
xl: 28,
}
/** @public */
export const ICON_SIZES: Record<TLSizeType, number> = {
s: 16,
m: 32,
l: 48,
xl: 64,
}
/** @public */
export const FONT_FAMILIES: Record<TLFontType, string> = {
draw: 'var(--tl-font-draw)',
sans: 'var(--tl-font-sans)',
serif: 'var(--tl-font-serif)',
mono: 'var(--tl-font-mono)',
}
/** @public */
export const FONT_ALIGNMENT: Record<TLAlignType, string> = {
middle: 'center',
start: 'left',
end: 'right',
}
/** @public */
export const STYLES: TLStyleCollections = {
color: [
{ id: 'black', type: 'color', icon: 'color' },
{ id: 'grey', type: 'color', icon: 'color' },
{ id: 'light-violet', type: 'color', icon: 'color' },
{ id: 'violet', type: 'color', icon: 'color' },
{ id: 'blue', type: 'color', icon: 'color' },
{ id: 'light-blue', type: 'color', icon: 'color' },
{ id: 'yellow', type: 'color', icon: 'color' },
{ id: 'orange', type: 'color', icon: 'color' },
{ id: 'green', type: 'color', icon: 'color' },
{ id: 'light-green', type: 'color', icon: 'color' },
{ id: 'light-red', type: 'color', icon: 'color' },
{ id: 'red', type: 'color', icon: 'color' },
],
fill: [
{ id: 'none', type: 'fill', icon: 'fill-none' },
{ id: 'semi', type: 'fill', icon: 'fill-semi' },
{ id: 'solid', type: 'fill', icon: 'fill-solid' },
{ id: 'pattern', type: 'fill', icon: 'fill-pattern' },
],
dash: [
{ id: 'draw', type: 'dash', icon: 'dash-draw' },
{ id: 'dashed', type: 'dash', icon: 'dash-dashed' },
{ id: 'dotted', type: 'dash', icon: 'dash-dotted' },
{ id: 'solid', type: 'dash', icon: 'dash-solid' },
],
size: [
{ id: 's', type: 'size', icon: 'size-small' },
{ id: 'm', type: 'size', icon: 'size-medium' },
{ id: 'l', type: 'size', icon: 'size-large' },
{ id: 'xl', type: 'size', icon: 'size-extra-large' },
],
font: [
{ id: 'draw', type: 'font', icon: 'font-draw' },
{ id: 'sans', type: 'font', icon: 'font-sans' },
{ id: 'serif', type: 'font', icon: 'font-serif' },
{ id: 'mono', type: 'font', icon: 'font-mono' },
],
align: [
{ id: 'start', type: 'align', icon: 'text-align-left' },
{ id: 'middle', type: 'align', icon: 'text-align-center' },
{ id: 'end', type: 'align', icon: 'text-align-right' },
],
verticalAlign: [
{ id: 'start', type: 'verticalAlign', icon: 'vertical-align-start' },
{ id: 'middle', type: 'verticalAlign', icon: 'vertical-align-center' },
{ id: 'end', type: 'verticalAlign', icon: 'vertical-align-end' },
],
geo: [
{ id: 'rectangle', type: 'geo', icon: 'geo-rectangle' },
{ id: 'ellipse', type: 'geo', icon: 'geo-ellipse' },
{ id: 'triangle', type: 'geo', icon: 'geo-triangle' },
{ id: 'diamond', type: 'geo', icon: 'geo-diamond' },
{ id: 'pentagon', type: 'geo', icon: 'geo-pentagon' },
{ id: 'hexagon', type: 'geo', icon: 'geo-hexagon' },
{ id: 'octagon', type: 'geo', icon: 'geo-octagon' },
{ id: 'star', type: 'geo', icon: 'geo-star' },
{ id: 'rhombus', type: 'geo', icon: 'geo-rhombus' },
{ id: 'rhombus-2', type: 'geo', icon: 'geo-rhombus-2' },
{ id: 'oval', type: 'geo', icon: 'geo-oval' },
{ id: 'trapezoid', type: 'geo', icon: 'geo-trapezoid' },
{ id: 'arrow-right', type: 'geo', icon: 'geo-arrow-right' },
{ id: 'arrow-left', type: 'geo', icon: 'geo-arrow-left' },
{ id: 'arrow-up', type: 'geo', icon: 'geo-arrow-up' },
{ id: 'arrow-down', type: 'geo', icon: 'geo-arrow-down' },
{ id: 'x-box', type: 'geo', icon: 'geo-x-box' },
{ id: 'check-box', type: 'geo', icon: 'geo-check-box' },
],
arrowheadStart: [
{ id: 'none', type: 'arrowheadStart', icon: 'arrowhead-none' },
{ id: 'arrow', type: 'arrowheadStart', icon: 'arrowhead-arrow' },
{ id: 'triangle', type: 'arrowheadStart', icon: 'arrowhead-triangle' },
{ id: 'square', type: 'arrowheadStart', icon: 'arrowhead-square' },
{ id: 'dot', type: 'arrowheadStart', icon: 'arrowhead-dot' },
{ id: 'diamond', type: 'arrowheadStart', icon: 'arrowhead-diamond' },
{ id: 'inverted', type: 'arrowheadStart', icon: 'arrowhead-triangle-inverted' },
{ id: 'bar', type: 'arrowheadStart', icon: 'arrowhead-bar' },
],
arrowheadEnd: [
{ id: 'none', type: 'arrowheadEnd', icon: 'arrowhead-none' },
{ id: 'arrow', type: 'arrowheadEnd', icon: 'arrowhead-arrow' },
{ id: 'triangle', type: 'arrowheadEnd', icon: 'arrowhead-triangle' },
{ id: 'square', type: 'arrowheadEnd', icon: 'arrowhead-square' },
{ id: 'dot', type: 'arrowheadEnd', icon: 'arrowhead-dot' },
{ id: 'diamond', type: 'arrowheadEnd', icon: 'arrowhead-diamond' },
{ id: 'inverted', type: 'arrowheadEnd', icon: 'arrowhead-triangle-inverted' },
{ id: 'bar', type: 'arrowheadEnd', icon: 'arrowhead-bar' },
],
spline: [
{ id: 'line', type: 'spline', icon: 'spline-line' },
{ id: 'cubic', type: 'spline', icon: 'spline-cubic' },
],
}
// These props should not cause Editor.props to update
export const BLACKLISTED_PROPS = new Set([
'bend',
'w',
'h',
'start',
'end',
'text',
'name',
'url',
'growY',
])
/** @internal */
export const COLLABORATOR_TIMEOUT = 3000

View file

@ -25,14 +25,15 @@ import { ComputedCache, RecordType } from '@tldraw/store'
import {
Box2dModel,
CameraRecordType,
DefaultColorStyle,
DefaultFontStyle,
InstancePageStateRecordType,
PageRecordType,
StyleProp,
TLArrowShape,
TLAsset,
TLAssetId,
TLAssetPartial,
TLColorStyle,
TLColorType,
TLCursor,
TLCursorType,
TLDOCUMENT_ID,
@ -43,7 +44,6 @@ import {
TLImageAsset,
TLInstance,
TLInstancePageState,
TLNullableShapeProps,
TLPOINTER_ID,
TLPage,
TLPageId,
@ -53,13 +53,12 @@ import {
TLShape,
TLShapeId,
TLShapePartial,
TLShapeProp,
TLSizeStyle,
TLStore,
TLUnknownShape,
TLVideoAsset,
Vec2dModel,
createShapeId,
getShapePropKeysByStyle,
isPageId,
isShape,
isShapeId,
@ -72,6 +71,7 @@ import {
deepCopy,
getOwnProperty,
hasOwnProperty,
objectMapFromEntries,
partition,
sortById,
structuredClone,
@ -84,7 +84,6 @@ import { checkShapesAndAddCore } from '../config/defaultShapes'
import { AnyTLShapeInfo } from '../config/defineShape'
import {
ANIMATION_MEDIUM_MS,
BLACKLISTED_PROPS,
COARSE_DRAG_DISTANCE,
COLLABORATOR_TIMEOUT,
DEFAULT_ANIMATION_OPTIONS,
@ -103,15 +102,14 @@ import {
MAX_ZOOM,
MINOR_NUDGE_FACTOR,
MIN_ZOOM,
STYLES,
SVG_PADDING,
ZOOMS,
} from '../constants'
import { exportPatternSvgDefs } from '../hooks/usePattern'
import { ReadonlySharedStyleMap, SharedStyle, SharedStyleMap } from '../utils/SharedStylesMap'
import { WeakMapCache } from '../utils/WeakMapCache'
import { dataUrlToFile } from '../utils/assets'
import { getIncrementedName, uniqueId } from '../utils/data'
import { setPropsForNextShape } from '../utils/props-for-next-shape'
import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
import { arrowBindingsIndex } from './derivations/arrowBindingsIndex'
import { parentsToChildrenWithIndexes } from './derivations/parentsToChildrenWithIndexes'
@ -216,9 +214,25 @@ export class Editor extends EventEmitter<TLEventMap> {
}" is present in the store schema but not provided to the editor`
)
}
this.shapeUtils = Object.fromEntries(
allShapes.map(({ util: Util }) => [Util.type, new Util(this, Util.type)])
)
const shapeUtils = {} as Record<string, ShapeUtil>
const allStylesById = new Map<string, StyleProp<unknown>>()
for (const { util: Util, props } of allShapes) {
const propKeysByStyle = getShapePropKeysByStyle(props ?? {})
shapeUtils[Util.type] = new Util(this, Util.type, propKeysByStyle)
for (const style of propKeysByStyle.keys()) {
if (!allStylesById.has(style.id)) {
allStylesById.set(style.id, style)
} else if (allStylesById.get(style.id) !== style) {
throw Error(
`Multiple style props with id "${style.id}" in use. Style prop IDs must be unique.`
)
}
}
}
this.shapeUtils = shapeUtils
// Tools.
// Accept tools from constructor parameters which may not conflict with the root note's default or
@ -248,9 +262,6 @@ export class Editor extends EventEmitter<TLEventMap> {
this.isChromeForIos = false
}
// Set styles
this.colors = new Map(Editor.styles.color.map((c) => [c.id, `var(--palette-${c.id})`]))
this.store.onBeforeDelete = (record) => {
if (record.typeName === 'shape') {
this._shapeWillBeDeleted(record)
@ -1086,126 +1097,94 @@ export class Editor extends EventEmitter<TLEventMap> {
}
/**
* Get all the current props among the users selected shapes
* Get all the current styles among the users selected shapes
*
* @internal
*/
private _extractSharedProps(shape: TLShape, sharedProps: TLNullableShapeProps) {
private _extractSharedStyles(shape: TLShape, sharedStyleMap: SharedStyleMap) {
if (this.isShapeOfType(shape, GroupShapeUtil)) {
// For groups, ignore the props of the group shape and instead include
// the props of the group's children. These are the shapes that would have
// their props changed if the user called `setProp` on the current selection.
// For groups, ignore the styles of the group shape and instead include the styles of the
// group's children. These are the shapes that would have their styles changed if the
// user called `setStyle` on the current selection.
const childIds = this._parentIdsToChildIds.value[shape.id]
if (!childIds) return
for (let i = 0, n = childIds.length; i < n; i++) {
this._extractSharedProps(this.getShapeById(childIds[i][0])!, sharedProps)
this._extractSharedStyles(this.getShapeById(childIds[i][0])!, sharedStyleMap)
}
} else {
const props = Object.entries(shape.props)
let prop: [TLShapeProp, any]
for (let i = 0, n = props.length; i < n; i++) {
prop = props[i] as [TLShapeProp, any]
// We should probably white list rather than black list here
if (BLACKLISTED_PROPS.has(prop[0])) continue
// Check the value of this prop on the shared props object.
switch (sharedProps[prop[0]]) {
case undefined: {
// If this key hasn't been defined yet in the shared props object,
// we can set it to the value from the shape's props object.
sharedProps[prop[0]] = prop[1]
break
}
case null:
case prop[1]: {
// If the value in the shared props object matches the value from
// the shape's props object exactly—or if there is already a mixed
// value (null) in the shared props object—then this is a noop. We
// want to leave the value as it is in the shared props object.
continue
}
default: {
// If there's a value in the shared props object that isn't null AND
// that isn't undefined AND that doesn't match the shape's props object,
// then we've got a conflict, mixed props, so set the value to null.
sharedProps[prop[0]] = null
}
}
const util = this.getShapeUtil(shape)
for (const [style, value] of util.iterateStyles(shape)) {
sharedStyleMap.applyValue(style, value)
}
}
}
/**
* A derived object containing all current props among the user's selected shapes.
* A derived map containing all current styles among the user's selected shapes.
*
* @internal
*/
private _selectionSharedProps = computed<TLNullableShapeProps>('_selectionSharedProps', () => {
const { selectedShapes } = this
private _selectionSharedStyles = computed<ReadonlySharedStyleMap>(
'_selectionSharedStyles',
() => {
const { selectedShapes } = this
const sharedProps = {} as TLNullableShapeProps
const sharedStyles = new SharedStyleMap()
for (const selectedShape of selectedShapes) {
this._extractSharedStyles(selectedShape, sharedStyles)
}
for (let i = 0, n = selectedShapes.length; i < n; i++) {
this._extractSharedProps(selectedShapes[i], sharedProps)
return sharedStyles
}
)
return sharedProps as TLNullableShapeProps
})
@computed private get _stylesForNextShape() {
return this.instanceState.stylesForNextShape
}
/** @internal */
private _prevProps: any = {}
/**
* A derived object containing either all current props among the user's selected shapes, or else
* the user's most recent prop choices that correspond to the current active state (i.e. the
* selected tool).
*
* @internal
*/
@computed get props(): TLNullableShapeProps | null {
let next: TLNullableShapeProps | null
// If we're in selecting and if we have a selection,
// return the shared props from the current selection
if (this.isIn('select') && this.selectedIds.length > 0) {
next = this._selectionSharedProps.value
} else {
// Otherwise, pull the style props from the app state
// (the most recent choices made by the user) that are
// exposed by the current state (i.e. the active tool).
const currentState = this.root.current.value!
if (currentState.styles.length === 0) {
next = null
} else {
const { propsForNextShape } = this.instanceState
next = Object.fromEntries(
currentState.styles.map((k) => {
return [k, propsForNextShape[k]]
})
)
}
}
// todo: any way to improve this? still faster than rendering the style panel every frame
if (JSON.stringify(this._prevProps) === JSON.stringify(next)) {
return this._prevProps
}
this._prevProps = next
return next
getStyleForNextShape<T>(style: StyleProp<T>): T {
const value = this._stylesForNextShape[style.id]
return value === undefined ? style.defaultValue : (value as T)
}
/**
* Get the currently selected opacity.
* If any shapes are selected, this returns the opacity of the selected shapes.
* A derived object containing either all current styles among the user's selected shapes, or
* else the user's most recent style choices that correspond to the current active state (i.e.
* the selected tool).
*
* @public
*/
@computed<ReadonlySharedStyleMap>({ isEqual: (a, b) => a.equals(b) })
get sharedStyles(): ReadonlySharedStyleMap {
// If we're in selecting and if we have a selection, return the shared styles from the
// current selection
if (this.isIn('select') && this.selectedIds.length > 0) {
return this._selectionSharedStyles.value
}
// If the current tool is associated with a shape, return the styles for that shape.
// Otherwise, just return an empty map.
const currentTool = this.root.current.value!
const styles = new SharedStyleMap()
if (currentTool.shapeType) {
for (const style of this.getShapeUtil(currentTool.shapeType).styleProps.keys()) {
styles.applyValue(style, this.getStyleForNextShape(style))
}
}
return styles
}
/**
* Get the currently selected shared opacity.
* If any shapes are selected, this returns the shared opacity of the selected shapes.
* Otherwise, this returns the chosen opacity for the next shape.
*
* @public
*/
@computed get opacity(): number | null {
@computed get sharedOpacity(): SharedStyle<number> {
if (this.isIn('select') && this.selectedIds.length > 0) {
const shapesToCheck: TLShape[] = []
const addShape = (shapeId: TLShapeId) => {
@ -1231,14 +1210,13 @@ export class Editor extends EventEmitter<TLEventMap> {
if (opacity === null) {
opacity = shape.opacity
} else if (opacity !== shape.opacity) {
return null
return { type: 'mixed' }
}
}
return opacity
} else {
return this.instanceState.opacityForNextShape
if (opacity !== null) return { type: 'shared', value: opacity }
}
return { type: 'shared', value: this.instanceState.opacityForNextShape }
}
/**
@ -3260,14 +3238,8 @@ export class Editor extends EventEmitter<TLEventMap> {
/* --------------------- Shapes --------------------- */
/**
* The app's set of styles.
*
* @public
*/
static styles = STYLES
/**
* The current page bounds of all the selected shapes (Not the same thing as the page bounds of the selection bounding box when the selection has been rotated)
* The current page bounds of all the selected shapes (Not the same thing as the page bounds of
* the selection bounding box when the selection has been rotated)
*
* @readonly
*
@ -3576,59 +3548,6 @@ export class Editor extends EventEmitter<TLEventMap> {
return shapeIsInPage
}
/* --------------------- Styles --------------------- */
/**
* A mapping of color ids to CSS color values.
*
* @internal
*/
private colors: Map<TLColorStyle['id'], string>
/**
* A mapping of size ids to size values.
*
* @internal
*/
private sizes = {
s: 2,
m: 3.5,
l: 5,
xl: 10,
}
/**
* Get the CSS color value for a given color id.
*
* @example
* ```ts
* editor.getCssColor('red')
* ```
*
* @param id - The id of the color to get.
*
* @public
*/
getCssColor(id: TLColorStyle['id']): string {
return this.colors.get(id)!
}
/**
* Get the stroke width value for a given size id.
*
* @example
* ```ts
* editor.getStrokeWidth('m')
* ```
*
* @param id - The id of the size to get.
*
* @public
*/
getStrokeWidth(id: TLSizeStyle['id']): number {
return this.sizes[id]
}
/* ------------------- Statechart ------------------- */
/**
@ -5022,15 +4941,10 @@ export class Editor extends EventEmitter<TLEventMap> {
// The initial props starts as the shape utility's default props
const initialProps = util.defaultProps()
// We then look up each key in the tab state's props; and if it's there,
// we use the value from the tab state's props instead of the default.
// Note that props will never include opacity.
const { propsForNextShape, opacityForNextShape } = this.instanceState
for (const key in initialProps) {
if (key in propsForNextShape) {
if (key === 'url') continue
;(initialProps as any)[key] = (propsForNextShape as any)[key]
}
// We then look up each key in the tab state's styles; and if it's there,
// we use the value from the tab state's styles instead of the default.
for (const [style, propKey] of util.styleProps) {
;(initialProps as any)[propKey] = this.getStyleForNextShape(style)
}
// When we create the shape, take in the partial (the props coming into the
@ -5043,7 +4957,7 @@ export class Editor extends EventEmitter<TLEventMap> {
).create({
...partial,
index,
opacity: partial.opacity ?? opacityForNextShape,
opacity: partial.opacity ?? this.instanceState.opacityForNextShape,
parentId: partial.parentId ?? focusLayerId,
props: 'props' in partial ? { ...initialProps, ...partial.props } : initialProps,
})
@ -5974,30 +5888,30 @@ export class Editor extends EventEmitter<TLEventMap> {
const fontsUsedInExport = new Map<string, string>()
const colors: TLExportColors = {
fill: Object.fromEntries(
STYLES.color.map((color) => [
color.id,
containerStyle.getPropertyValue(`--palette-${color.id}`),
fill: objectMapFromEntries(
DefaultColorStyle.values.map((color) => [
color,
containerStyle.getPropertyValue(`--palette-${color}`),
])
) as Record<TLColorType, string>,
pattern: Object.fromEntries(
STYLES.color.map((color) => [
color.id,
containerStyle.getPropertyValue(`--palette-${color.id}-pattern`),
),
pattern: objectMapFromEntries(
DefaultColorStyle.values.map((color) => [
color,
containerStyle.getPropertyValue(`--palette-${color}-pattern`),
])
) as Record<TLColorType, string>,
semi: Object.fromEntries(
STYLES.color.map((color) => [
color.id,
containerStyle.getPropertyValue(`--palette-${color.id}-semi`),
),
semi: objectMapFromEntries(
DefaultColorStyle.values.map((color) => [
color,
containerStyle.getPropertyValue(`--palette-${color}-semi`),
])
) as Record<TLColorType, string>,
highlight: Object.fromEntries(
STYLES.color.map((color) => [
color.id,
containerStyle.getPropertyValue(`--palette-${color.id}-highlight`),
),
highlight: objectMapFromEntries(
DefaultColorStyle.values.map((color) => [
color,
containerStyle.getPropertyValue(`--palette-${color}-highlight`),
])
) as Record<TLColorType, string>,
),
text: containerStyle.getPropertyValue(`--color-text`),
background: containerStyle.getPropertyValue(`--color-background`),
solid: containerStyle.getPropertyValue(`--palette-solid`),
@ -6094,16 +6008,17 @@ export class Editor extends EventEmitter<TLEventMap> {
const util = this.getShapeUtil(shape)
let font: string | undefined
if ('font' in shape.props) {
if (shape.props.font) {
if (fontsUsedInExport.has(shape.props.font)) {
font = fontsUsedInExport.get(shape.props.font)!
} else {
// For some reason these styles aren't present in the fake element
// so we need to get them from the real element
font = realContainerStyle.getPropertyValue(`--tl-font-${shape.props.font}`)
fontsUsedInExport.set(shape.props.font, font)
}
// TODO: `Editor` shouldn't know about `DefaultFontStyle`. We need another way
// for shapes to register fonts for export.
const fontFromShape = util.getStyleIfExists(DefaultFontStyle, shape)
if (fontFromShape) {
if (fontsUsedInExport.has(fontFromShape)) {
font = fontsUsedInExport.get(fontFromShape)!
} else {
// For some reason these styles aren't present in the fake element
// so we need to get them from the real element
font = realContainerStyle.getPropertyValue(`--tl-font-${fontFromShape}`)
fontsUsedInExport.set(fontFromShape, font)
}
}
@ -8255,22 +8170,22 @@ export class Editor extends EventEmitter<TLEventMap> {
}
/**
* Set the current props (generally styles).
* Set the current styles
*
* @example
* ```ts
* editor.setProp('color', 'red')
* editor.setProp('color', 'red', true)
* editor.setProp(DefaultColorStyle, 'red')
* editor.setProp(DefaultColorStyle, 'red', true)
* ```
*
* @param key - The key to set.
* @param style - The style to set.
* @param value - The value to set.
* @param ephemeral - Whether the style change is ephemeral. Ephemeral changes don't get added to the undo/redo stack. Defaults to false.
* @param squashing - Whether the style change will be squashed into the existing history entry rather than creating a new one. Defaults to false.
*
* @public
*/
setProp(key: TLShapeProp, value: any, ephemeral = false, squashing = false): this {
setStyle<T>(style: StyleProp<T>, value: T, ephemeral = false, squashing = false): this {
this.history.batch(() => {
if (this.isIn('select')) {
const {
@ -8278,7 +8193,7 @@ export class Editor extends EventEmitter<TLEventMap> {
} = this
if (selectedIds.length > 0) {
const shapesToUpdate: TLShape[] = []
const updates: { originalShape: TLShape; updatePartial: TLShapePartial }[] = []
// We can have many deep levels of grouped shape
// Making a recursive function to look through all the levels
@ -8290,8 +8205,19 @@ export class Editor extends EventEmitter<TLEventMap> {
for (const childId of childIds) {
addShapeById(childId)
}
} else if (shape!.props[key as keyof TLShape['props']] !== undefined) {
shapesToUpdate.push(shape)
} else {
const util = this.getShapeUtil(shape)
if (util.hasStyle(style)) {
const shapePartial: TLShapePartial = {
id: shape.id,
type: shape.type,
props: {},
}
updates.push({
originalShape: shape,
updatePartial: util.setStyleInPartial(style, shapePartial, value),
})
}
}
}
@ -8300,77 +8226,62 @@ export class Editor extends EventEmitter<TLEventMap> {
}
this.updateShapes(
shapesToUpdate.map((shape) => {
const props = { ...shape.props, [key]: value }
if (key === 'color' && 'labelColor' in props) {
props.labelColor = 'black'
}
return {
id: shape.id,
type: shape.type,
props,
}
}),
updates.map(({ updatePartial }) => updatePartial),
ephemeral
)
if (key !== 'color') {
const changes: TLShapePartial[] = []
// TODO: find a way to sink this stuff into shape utils directly?
const changes: TLShapePartial[] = []
for (const { originalShape: originalShape } of updates) {
const currentShape = this.getShapeById(originalShape.id)
if (!currentShape) continue
const util = this.getShapeUtil(currentShape)
for (const shape of shapesToUpdate) {
const currentShape = this.getShapeById(shape.id)
if (!currentShape) continue
const util = this.getShapeUtil(currentShape)
const boundsA = util.bounds(originalShape)
const boundsB = util.bounds(currentShape)
const boundsA = util.bounds(shape)
const boundsB = util.bounds(currentShape)
const change: TLShapePartial = { id: originalShape.id, type: originalShape.type }
const change: TLShapePartial = { id: shape.id, type: shape.type }
let didChange = false
let didChange = false
if (boundsA.width !== boundsB.width) {
didChange = true
if (boundsA.width !== boundsB.width) {
didChange = true
if (this.isShapeOfType(shape, TextShapeUtil)) {
switch (shape.props.align) {
case 'middle': {
change.x = currentShape.x + (boundsA.width - boundsB.width) / 2
break
}
case 'end': {
change.x = currentShape.x + boundsA.width - boundsB.width
break
}
if (this.isShapeOfType(originalShape, TextShapeUtil)) {
switch (originalShape.props.align) {
case 'middle': {
change.x = currentShape.x + (boundsA.width - boundsB.width) / 2
break
}
case 'end': {
change.x = currentShape.x + boundsA.width - boundsB.width
break
}
} else {
change.x = currentShape.x + (boundsA.width - boundsB.width) / 2
}
}
if (boundsA.height !== boundsB.height) {
didChange = true
change.y = currentShape.y + (boundsA.height - boundsB.height) / 2
}
if (didChange) {
changes.push(change)
} else {
change.x = currentShape.x + (boundsA.width - boundsB.width) / 2
}
}
if (changes.length) {
this.updateShapes(changes, ephemeral)
if (boundsA.height !== boundsB.height) {
didChange = true
change.y = currentShape.y + (boundsA.height - boundsB.height) / 2
}
if (didChange) {
changes.push(change)
}
}
if (changes.length) {
this.updateShapes(changes, ephemeral)
}
}
}
this.updateInstanceState(
{
propsForNextShape: setPropsForNextShape(this.instanceState.propsForNextShape, {
[key]: value,
}),
stylesForNextShape: { ...this._stylesForNextShape, [style.id]: value },
},
ephemeral,
squashing

View file

@ -11,13 +11,7 @@ import {
createShapeId,
} from '@tldraw/tlschema'
import { compact, getHashForString } from '@tldraw/utils'
import {
FONT_FAMILIES,
FONT_SIZES,
MAX_ASSET_HEIGHT,
MAX_ASSET_WIDTH,
TEXT_PROPS,
} from '../../constants'
import { MAX_ASSET_HEIGHT, MAX_ASSET_WIDTH } from '../../constants'
import {
ACCEPTED_IMG_TYPE,
ACCEPTED_VID_TYPE,
@ -31,6 +25,7 @@ import {
import { truncateStringWithEllipsis } from '../../utils/dom'
import { getEmbedInfo } from '../../utils/embeds'
import { Editor } from '../Editor'
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shapes/shared/default-shape-constants'
import { INDENT } from '../shapes/text/TextHelpers'
import { TextShapeUtil } from '../shapes/text/TextShapeUtil'

View file

@ -1,4 +1,4 @@
import { Box2dModel, TLAlignType } from '@tldraw/tlschema'
import { Box2dModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
import { uniqueId } from '../../utils/data'
import { Editor } from '../Editor'
import { TextHelpers } from '../shapes/text/TextHelpers'
@ -23,7 +23,7 @@ type TLMeasureTextSpanOpts = {
fontFamily: string
fontStyle: string
lineHeight: number
textAlign: TLAlignType
textAlign: TLDefaultHorizontalAlignStyle
}
const spaceCharacterRegex = /\s/

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Box2d, linesIntersect, Vec2d, VecLike } from '@tldraw/primitives'
import { ComputedCache } from '@tldraw/store'
import { TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema'
import { StyleProp, TLHandle, TLShape, TLShapePartial, TLUnknownShape } from '@tldraw/tlschema'
import { computed, EMPTY_ARRAY } from 'signia'
import type { Editor } from '../Editor'
import { TLResizeHandle } from '../types/selection-types'
@ -12,7 +12,7 @@ export interface TLShapeUtilConstructor<
T extends TLUnknownShape,
U extends ShapeUtil<T> = ShapeUtil<T>
> {
new (editor: Editor, type: T['type']): U
new (editor: Editor, type: T['type'], styleProps: ReadonlyMap<StyleProp<unknown>, string>): U
type: T['type']
}
@ -20,8 +20,45 @@ export interface TLShapeUtilConstructor<
export type TLShapeUtilFlag<T> = (shape: T) => boolean
/** @public */
export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
constructor(public editor: Editor, public readonly type: T['type']) {}
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
constructor(
public editor: Editor,
public readonly type: Shape['type'],
public readonly styleProps: ReadonlyMap<StyleProp<unknown>, string>
) {}
hasStyle(style: StyleProp<unknown>) {
return this.styleProps.has(style)
}
getStyleIfExists<T>(style: StyleProp<T>, shape: Shape | TLShapePartial<Shape>): T | undefined {
const styleKey = this.styleProps.get(style)
if (!styleKey) return undefined
return (shape.props as any)[styleKey]
}
*iterateStyles(shape: Shape | TLShapePartial<Shape>) {
for (const [style, styleKey] of this.styleProps) {
const value = (shape.props as any)[styleKey]
yield [style, value] as [StyleProp<unknown>, unknown]
}
}
setStyleInPartial<T>(
style: StyleProp<T>,
shape: TLShapePartial<Shape>,
value: T
): TLShapePartial<Shape> {
const styleKey = this.styleProps.get(style)
if (!styleKey) return shape
return {
...shape,
props: {
...shape.props,
[styleKey]: value,
},
}
}
/**
* The type of the shape util, which should match the shape's type.
@ -35,21 +72,21 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
*
* @public
*/
canSnap: TLShapeUtilFlag<T> = () => true
canSnap: TLShapeUtilFlag<Shape> = () => true
/**
* Whether the shape can be scrolled while editing.
*
* @public
*/
canScroll: TLShapeUtilFlag<T> = () => false
canScroll: TLShapeUtilFlag<Shape> = () => false
/**
* Whether the shape should unmount when not visible in the editor. Consider keeping this to false if the shape's `component` has local state.
*
* @public
*/
canUnmount: TLShapeUtilFlag<T> = () => true
canUnmount: TLShapeUtilFlag<Shape> = () => true
/**
* Whether the shape can be bound to by an arrow.
@ -57,28 +94,28 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param _otherShape - The other shape attempting to bind to this shape.
* @public
*/
canBind = <K>(_shape: T, _otherShape?: K) => true
canBind = <K>(_shape: Shape, _otherShape?: K) => true
/**
* Whether the shape can be double clicked to edit.
*
* @public
*/
canEdit: TLShapeUtilFlag<T> = () => false
canEdit: TLShapeUtilFlag<Shape> = () => false
/**
* Whether the shape can be resized.
*
* @public
*/
canResize: TLShapeUtilFlag<T> = () => true
canResize: TLShapeUtilFlag<Shape> = () => true
/**
* Whether the shape can be cropped.
*
* @public
*/
canCrop: TLShapeUtilFlag<T> = () => false
canCrop: TLShapeUtilFlag<Shape> = () => false
/**
* Bounds of the shape to edit.
@ -87,7 +124,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
*
* @public
*/
getEditingBounds = (shape: T) => {
getEditingBounds = (shape: Shape) => {
return this.bounds(shape)
}
@ -96,49 +133,49 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
*
* @public
*/
isClosed: TLShapeUtilFlag<T> = () => true
isClosed: TLShapeUtilFlag<Shape> = () => true
/**
* Whether the shape should hide its resize handles when selected.
*
* @public
*/
hideResizeHandles: TLShapeUtilFlag<T> = () => false
hideResizeHandles: TLShapeUtilFlag<Shape> = () => false
/**
* Whether the shape should hide its resize handles when selected.
*
* @public
*/
hideRotateHandle: TLShapeUtilFlag<T> = () => false
hideRotateHandle: TLShapeUtilFlag<Shape> = () => false
/**
* Whether the shape should hide its selection bounds background when selected.
*
* @public
*/
hideSelectionBoundsBg: TLShapeUtilFlag<T> = () => false
hideSelectionBoundsBg: TLShapeUtilFlag<Shape> = () => false
/**
* Whether the shape should hide its selection bounds foreground when selected.
*
* @public
*/
hideSelectionBoundsFg: TLShapeUtilFlag<T> = () => false
hideSelectionBoundsFg: TLShapeUtilFlag<Shape> = () => false
/**
* Whether the shape's aspect ratio is locked.
*
* @public
*/
isAspectRatioLocked: TLShapeUtilFlag<T> = () => false
isAspectRatioLocked: TLShapeUtilFlag<Shape> = () => false
/**
* Get the default props for a shape.
*
* @public
*/
abstract defaultProps(): T['props']
abstract defaultProps(): Shape['props']
/**
* Get a JSX element for the shape (as an HTML element).
@ -146,7 +183,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
abstract render(shape: T): any
abstract render(shape: Shape): any
/**
* Get JSX describing the shape's indicator (as an SVG element).
@ -154,7 +191,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
abstract indicator(shape: T): any
abstract indicator(shape: Shape): any
/**
* Get a JSX element for the shape (as an HTML element) to be rendered as part of the canvas background - behind any other shape content.
@ -162,7 +199,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @internal
*/
renderBackground?(shape: T): any
renderBackground?(shape: Shape): any
/**
* Get an array of handle models for the shape. This is an optional method.
@ -176,7 +213,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
protected getHandles?(shape: T): TLHandle[]
protected getHandles?(shape: Shape): TLHandle[]
@computed
private get handlesCache(): ComputedCache<TLHandle[], TLShape> {
@ -191,7 +228,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
handles(shape: T): TLHandle[] {
handles(shape: Shape): TLHandle[] {
if (!this.getHandles) return EMPTY_ARRAY
return this.handlesCache.get(shape.id) ?? EMPTY_ARRAY
}
@ -211,7 +248,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
protected getOutlineSegments(shape: T): Vec2d[][] {
protected getOutlineSegments(shape: Shape): Vec2d[][] {
return [this.outline(shape)]
}
@ -228,7 +265,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
outlineSegments(shape: T): Vec2d[][] {
outlineSegments(shape: Shape): Vec2d[][] {
if (!this.getOutlineSegments) return EMPTY_ARRAY
return this.outlineSegmentsCache.get(shape.id) ?? EMPTY_ARRAY
}
@ -239,7 +276,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
protected abstract getBounds(shape: T): Box2d
protected abstract getBounds(shape: Shape): Box2d
@computed
private get boundsCache(): ComputedCache<Box2d, TLShape> {
@ -254,7 +291,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
bounds(shape: T): Box2d {
bounds(shape: Shape): Box2d {
const result = this.boundsCache.get(shape.id) ?? new Box2d()
if (result.width === 0 || result.height === 0) {
return new Box2d(result.x, result.y, Math.max(result.width, 1), Math.max(result.height, 1))
@ -268,7 +305,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
protected abstract getOutline(shape: T): Vec2d[]
protected abstract getOutline(shape: Shape): Vec2d[]
@computed
private get outlineCache(): ComputedCache<Vec2d[], TLShape> {
@ -283,7 +320,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
outline(shape: T): Vec2d[] {
outline(shape: Shape): Vec2d[] {
return this.outlineCache.get(shape.id) ?? EMPTY_ARRAY
}
@ -293,7 +330,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
snapPoints(shape: T) {
snapPoints(shape: Shape) {
return this.bounds(shape).snapPoints
}
@ -303,7 +340,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
center(shape: T): Vec2d {
center(shape: Shape): Vec2d {
return this.getCenter(shape)
}
@ -313,7 +350,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
abstract getCenter(shape: T): Vec2d
abstract getCenter(shape: Shape): Vec2d
/**
* Get whether the shape can receive children of a given type.
@ -321,7 +358,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param type - The shape type.
* @public
*/
canReceiveNewChildrenOfType(shape: T, type: TLShape['type']) {
canReceiveNewChildrenOfType(shape: Shape, type: TLShape['type']) {
return false
}
@ -332,7 +369,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shapes - The shapes that are being dropped.
* @public
*/
canDropShapes(shape: T, shapes: TLShape[]) {
canDropShapes(shape: Shape, shapes: TLShape[]) {
return false
}
@ -346,7 +383,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @public
*/
toSvg?(
shape: T,
shape: Shape,
font: string | undefined,
colors: TLExportColors
): SVGElement | Promise<SVGElement>
@ -361,7 +398,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @public
*/
toBackgroundSvg?(
shape: T,
shape: Shape,
font: string | undefined,
colors: TLExportColors
): SVGElement | Promise<SVGElement> | null
@ -374,7 +411,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns Whether the point intersects the shape.
* @public
*/
hitTestPoint(shape: T, point: VecLike): boolean {
hitTestPoint(shape: Shape, point: VecLike): boolean {
return this.bounds(shape).containsPoint(point)
}
@ -387,7 +424,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns Whether the line segment intersects the shape.
* @public
*/
hitTestLineSegment(shape: T, A: VecLike, B: VecLike): boolean {
hitTestLineSegment(shape: Shape, A: VecLike, B: VecLike): boolean {
const outline = this.outline(shape)
for (let i = 0; i < outline.length; i++) {
@ -400,7 +437,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
}
/** @internal */
expandSelectionOutlinePx(shape: T): number {
expandSelectionOutlinePx(shape: Shape): number {
return 0
}
@ -413,7 +450,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
*
* @internal
*/
providesBackgroundForChildren(shape: T): boolean {
providesBackgroundForChildren(shape: Shape): boolean {
return false
}
@ -435,7 +472,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns The next shape or void.
* @public
*/
onBeforeCreate?: TLOnBeforeCreateHandler<T>
onBeforeCreate?: TLOnBeforeCreateHandler<Shape>
/**
* A callback called just before a shape is updated. This method provides a last chance to modify
@ -456,7 +493,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns The next shape or void.
* @public
*/
onBeforeUpdate?: TLOnBeforeUpdateHandler<T>
onBeforeUpdate?: TLOnBeforeUpdateHandler<Shape>
/**
* A callback called when some other shapes are dragged over this one.
@ -474,7 +511,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns An object specifying whether the shape should hint that it can receive the dragged shapes.
* @public
*/
onDragShapesOver?: TLOnDragHandler<T, { shouldHint: boolean }>
onDragShapesOver?: TLOnDragHandler<Shape, { shouldHint: boolean }>
/**
* A callback called when some other shapes are dragged out of this one.
@ -483,7 +520,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shapes - The shapes that are being dragged out.
* @public
*/
onDragShapesOut?: TLOnDragHandler<T>
onDragShapesOut?: TLOnDragHandler<Shape>
/**
* A callback called when some other shapes are dropped over this one.
@ -492,7 +529,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shapes - The shapes that are being dropped over this one.
* @public
*/
onDropShapesOver?: TLOnDragHandler<T>
onDropShapesOver?: TLOnDragHandler<Shape>
/**
* A callback called when a shape starts being resized.
@ -501,7 +538,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns A change to apply to the shape, or void.
* @public
*/
onResizeStart?: TLOnResizeStartHandler<T>
onResizeStart?: TLOnResizeStartHandler<Shape>
/**
* A callback called when a shape changes from a resize.
@ -511,7 +548,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns A change to apply to the shape, or void.
* @public
*/
onResize?: TLOnResizeHandler<T>
onResize?: TLOnResizeHandler<Shape>
/**
* A callback called when a shape finishes resizing.
@ -521,7 +558,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns A change to apply to the shape, or void.
* @public
*/
onResizeEnd?: TLOnResizeEndHandler<T>
onResizeEnd?: TLOnResizeEndHandler<Shape>
/**
* A callback called when a shape starts being translated.
@ -530,7 +567,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns A change to apply to the shape, or void.
* @public
*/
onTranslateStart?: TLOnTranslateStartHandler<T>
onTranslateStart?: TLOnTranslateStartHandler<Shape>
/**
* A callback called when a shape changes from a translation.
@ -540,7 +577,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns A change to apply to the shape, or void.
* @public
*/
onTranslate?: TLOnTranslateHandler<T>
onTranslate?: TLOnTranslateHandler<Shape>
/**
* A callback called when a shape finishes translating.
@ -550,7 +587,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns A change to apply to the shape, or void.
* @public
*/
onTranslateEnd?: TLOnTranslateEndHandler<T>
onTranslateEnd?: TLOnTranslateEndHandler<Shape>
/**
* A callback called when a shape starts being rotated.
@ -559,7 +596,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns A change to apply to the shape, or void.
* @public
*/
onRotateStart?: TLOnRotateStartHandler<T>
onRotateStart?: TLOnRotateStartHandler<Shape>
/**
* A callback called when a shape changes from a rotation.
@ -569,7 +606,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns A change to apply to the shape, or void.
* @public
*/
onRotate?: TLOnRotateHandler<T>
onRotate?: TLOnRotateHandler<Shape>
/**
* A callback called when a shape finishes rotating.
@ -579,7 +616,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns A change to apply to the shape, or void.
* @public
*/
onRotateEnd?: TLOnRotateEndHandler<T>
onRotateEnd?: TLOnRotateEndHandler<Shape>
/**
* A callback called when a shape's handle changes.
@ -589,14 +626,14 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns A change to apply to the shape, or void.
* @public
*/
onHandleChange?: TLOnHandleChangeHandler<T>
onHandleChange?: TLOnHandleChangeHandler<Shape>
/**
* Not currently used.
*
* @internal
*/
onBindingChange?: TLOnBindingChangeHandler<T>
onBindingChange?: TLOnBindingChangeHandler<Shape>
/**
* A callback called when a shape's children change.
@ -605,7 +642,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns An array of shape updates, or void.
* @public
*/
onChildrenChange?: TLOnChildrenChangeHandler<T>
onChildrenChange?: TLOnChildrenChangeHandler<Shape>
/**
* A callback called when a shape's handle is double clicked.
@ -615,7 +652,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns A change to apply to the shape, or void.
* @public
*/
onDoubleClickHandle?: TLOnDoubleClickHandleHandler<T>
onDoubleClickHandle?: TLOnDoubleClickHandleHandler<Shape>
/**
* A callback called when a shape's edge is double clicked.
@ -624,7 +661,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns A change to apply to the shape, or void.
* @public
*/
onDoubleClickEdge?: TLOnDoubleClickHandler<T>
onDoubleClickEdge?: TLOnDoubleClickHandler<Shape>
/**
* A callback called when a shape is double clicked.
@ -633,7 +670,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns A change to apply to the shape, or void.
* @public
*/
onDoubleClick?: TLOnDoubleClickHandler<T>
onDoubleClick?: TLOnDoubleClickHandler<Shape>
/**
* A callback called when a shape is clicked.
@ -642,7 +679,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @returns A change to apply to the shape, or void.
* @public
*/
onClick?: TLOnClickHandler<T>
onClick?: TLOnClickHandler<Shape>
/**
* A callback called when a shape finishes being editing.
@ -650,7 +687,7 @@ export abstract class ShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
* @param shape - The shape.
* @public
*/
onEditEnd?: TLOnEditEndHandler<T>
onEditEnd?: TLOnEditEndHandler<Shape>
}
/** @public */

View file

@ -1,5 +1,5 @@
import { TLStyleType } from '@tldraw/tlschema'
import { StateNode } from '../../tools/StateNode'
import { ArrowShapeUtil } from './ArrowShapeUtil'
import { Idle } from './toolStates/Idle'
import { Pointing } from './toolStates/Pointing'
@ -8,15 +8,5 @@ export class ArrowShapeTool extends StateNode {
static initial = 'idle'
static children = () => [Idle, Pointing]
shapeType = 'arrow'
styles = [
'color',
'dash',
'size',
'arrowheadStart',
'arrowheadEnd',
'font',
'fill',
] as TLStyleType[]
shapeType = ArrowShapeUtil
}

View file

@ -1,5 +1,5 @@
import { TAU } from '@tldraw/primitives'
import { TLArrowShape, TLArrowTerminal, TLShapeId, createShapeId } from '@tldraw/tlschema'
import { TLArrowShape, TLArrowShapeTerminal, TLShapeId, createShapeId } from '@tldraw/tlschema'
import { assert } from '@tldraw/utils'
import { TestEditor } from '../../../test/TestEditor'
import { ArrowShapeUtil } from './ArrowShapeUtil'
@ -343,7 +343,7 @@ describe('When a shape it rotated', () => {
})
const anchor = (
editor.getShapeById<TLArrowShape>(arrow.id)!.props.end as TLArrowTerminal & {
editor.getShapeById<TLArrowShape>(arrow.id)!.props.end as TLArrowShapeTerminal & {
type: 'binding'
}
).normalizedAnchor

View file

@ -12,10 +12,10 @@ import {
} from '@tldraw/primitives'
import { ComputedCache } from '@tldraw/store'
import {
TLArrowheadType,
TLArrowShape,
TLColorType,
TLFillType,
TLArrowShapeArrowheadStyle,
TLDefaultColorStyle,
TLDefaultFillStyle,
TLHandle,
TLShapeId,
TLShapePartial,
@ -25,7 +25,6 @@ import { deepCopy, last, minBy } from '@tldraw/utils'
import * as React from 'react'
import { computed, EMPTY_ARRAY } from 'signia'
import { SVGContainer } from '../../../components/SVGContainer'
import { ARROW_LABEL_FONT_SIZES, FONT_FAMILIES, TEXT_PROPS } from '../../../constants'
import {
ShapeUtil,
TLOnEditEndHandler,
@ -35,6 +34,12 @@ import {
TLShapeUtilFlag,
} from '../ShapeUtil'
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
import {
ARROW_LABEL_FONT_SIZES,
FONT_FAMILIES,
STROKE_SIZES,
TEXT_PROPS,
} from '../shared/default-shape-constants'
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
import { getShapeFillSvg, ShapeFill } from '../shared/ShapeFill'
import { TLExportColors } from '../shared/TLExportColors'
@ -530,7 +535,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
hitTestPoint(shape: TLArrowShape, point: VecLike): boolean {
const outline = this.outline(shape)
const zoomLevel = this.editor.zoomLevel
const offsetDist = this.editor.getStrokeWidth(shape.props.size) / zoomLevel
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
for (let i = 0; i < outline.length - 1; i++) {
const C = outline[i]
@ -577,7 +582,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
if (!info?.isValid) return null
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
const strokeWidth = STROKE_SIZES[shape.props.size]
const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth)
const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth)
@ -692,7 +697,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
)}
<g
fill="none"
stroke="currentColor"
stroke={`var(--palette-${shape.props.color})`}
strokeWidth={strokeWidth}
strokeLinejoin="round"
strokeLinecap="round"
@ -735,7 +740,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
size={shape.props.size}
position={info.middle}
width={labelSize?.w ?? 0}
labelColor={this.editor.getCssColor(shape.props.labelColor)}
labelColor={shape.props.labelColor}
/>
</>
)
@ -751,7 +756,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
if (!info) return null
if (Vec2d.Equals(start, end)) return null
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
const strokeWidth = STROKE_SIZES[shape.props.size]
const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth)
const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth)
@ -925,7 +930,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
const info = this.getArrowInfo(shape)
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
const strokeWidth = STROKE_SIZES[shape.props.size]
// Group for arrow
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
@ -1088,7 +1093,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
}
}
function getArrowheadSvgMask(d: string, arrowhead: TLArrowheadType) {
function getArrowheadSvgMask(d: string, arrowhead: TLArrowShapeArrowheadStyle) {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
path.setAttribute('d', d)
path.setAttribute('fill', arrowhead === 'arrow' ? 'none' : 'black')
@ -1107,9 +1112,9 @@ function getArrowSvgPath(d: string, color: string, strokeWidth: number) {
function getArrowheadSvgPath(
d: string,
color: TLColorType,
color: TLDefaultColorStyle,
strokeWidth: number,
fill: TLFillType,
fill: TLDefaultFillStyle,
colors: TLExportColors
) {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')

View file

@ -1,10 +1,10 @@
import { VecLike } from '@tldraw/primitives'
import { TLArrowheadType } from '@tldraw/tlschema'
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema'
export type ArrowPoint = {
handle: VecLike
point: VecLike
arrowhead: TLArrowheadType
arrowhead: TLArrowShapeArrowheadStyle
}
export interface ArcInfo {

View file

@ -14,14 +14,16 @@ import {
VecLike,
} from '@tldraw/primitives'
import { TLArrowShape } from '@tldraw/tlschema'
import type { Editor } from '../../../Editor'
import { STROKE_SIZES } from '../../shared/default-shape-constants'
import { ArcInfo, ArrowInfo } from './arrow-types'
import {
BOUND_ARROW_OFFSET,
getArrowTerminalsInArrowSpace,
getBoundShapeInfoForTerminal,
MIN_ARROW_LENGTH,
WAY_TOO_BIG_ARROW_BEND_FACTOR,
} from '../../../../constants'
import type { Editor } from '../../../Editor'
import { ArcInfo, ArrowInfo } from './arrow-types'
import { getArrowTerminalsInArrowSpace, getBoundShapeInfoForTerminal } from './shared'
} from './shared'
import { getStraightArrowInfo } from './straight-arrow'
export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBend = 0): ArrowInfo {
@ -115,9 +117,9 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
if (arrowheadStart !== 'none') {
const offset =
BOUND_ARROW_OFFSET +
editor.getStrokeWidth(shape.props.size) / 2 +
STROKE_SIZES[shape.props.size] / 2 +
('size' in startShapeInfo.shape.props
? editor.getStrokeWidth(startShapeInfo.shape.props.size) / 2
? STROKE_SIZES[startShapeInfo.shape.props.size] / 2
: 0)
a.setTo(
@ -192,10 +194,8 @@ export function getCurvedArrowInfo(editor: Editor, shape: TLArrowShape, extraBen
if (arrowheadEnd !== 'none') {
let offset =
BOUND_ARROW_OFFSET +
editor.getStrokeWidth(shape.props.size) / 2 +
('size' in endShapeInfo.shape.props
? editor.getStrokeWidth(endShapeInfo.shape.props.size) / 2
: 0)
STROKE_SIZES[shape.props.size] / 2 +
('size' in endShapeInfo.shape.props ? STROKE_SIZES[endShapeInfo.shape.props.size] / 2 : 0)
if (Vec2d.Dist(a, b) < MIN_ARROW_LENGTH) {
offset *= -2

View file

@ -1,5 +1,5 @@
import { Matrix2d, Vec2d } from '@tldraw/primitives'
import { TLArrowShape, TLArrowTerminal, TLShape } from '@tldraw/tlschema'
import { TLArrowShape, TLArrowShapeTerminal, TLShape } from '@tldraw/tlschema'
import { Editor } from '../../../Editor'
import { ShapeUtil } from '../../ShapeUtil'
@ -13,13 +13,11 @@ export type BoundShapeInfo<T extends TLShape = TLShape> = {
didIntersect: boolean
isExact: boolean
transform: Matrix2d
// toLocalPoint: (v: VecLike) => Vec2d
// toPagePoint: (v: VecLike) => Vec2d
}
export function getBoundShapeInfoForTerminal(
editor: Editor,
terminal: TLArrowTerminal
terminal: TLArrowShapeTerminal
): BoundShapeInfo | undefined {
if (terminal.type === 'point') {
return
@ -41,7 +39,7 @@ export function getBoundShapeInfoForTerminal(
export function getArrowTerminalInArrowSpace(
editor: Editor,
arrowPageTransform: Matrix2d,
terminal: TLArrowTerminal
terminal: TLArrowShapeTerminal
) {
if (terminal.type === 'point') {
return Vec2d.From(terminal)
@ -72,3 +70,10 @@ export function getArrowTerminalsInArrowSpace(editor: Editor, shape: TLArrowShap
return { start, end }
}
/** @internal */
export const MIN_ARROW_LENGTH = 48
/** @internal */
export const BOUND_ARROW_OFFSET = 10
/** @internal */
export const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10

View file

@ -8,13 +8,15 @@ import {
VecLike,
} from '@tldraw/primitives'
import { TLArrowShape } from '@tldraw/tlschema'
import { BOUND_ARROW_OFFSET, MIN_ARROW_LENGTH } from '../../../../constants'
import { Editor } from '../../../Editor'
import { STROKE_SIZES } from '../../shared/default-shape-constants'
import { ArrowInfo } from './arrow-types'
import {
BOUND_ARROW_OFFSET,
BoundShapeInfo,
getArrowTerminalsInArrowSpace,
getBoundShapeInfoForTerminal,
MIN_ARROW_LENGTH,
} from './shared'
export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): ArrowInfo {
@ -87,9 +89,9 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): Arrow
if (startShapeInfo && arrowheadStart !== 'none' && !startShapeInfo.isExact) {
const offset =
BOUND_ARROW_OFFSET +
editor.getStrokeWidth(shape.props.size) / 2 +
STROKE_SIZES[shape.props.size] / 2 +
('size' in startShapeInfo.shape.props
? editor.getStrokeWidth(startShapeInfo.shape.props.size) / 2
? STROKE_SIZES[startShapeInfo.shape.props.size] / 2
: 0)
minDist -= offset
@ -101,10 +103,8 @@ export function getStraightArrowInfo(editor: Editor, shape: TLArrowShape): Arrow
if (endShapeInfo && arrowheadEnd !== 'none' && !endShapeInfo.isExact) {
const offset =
BOUND_ARROW_OFFSET +
editor.getStrokeWidth(shape.props.size) / 2 +
('size' in endShapeInfo.shape.props
? editor.getStrokeWidth(endShapeInfo.shape.props.size) / 2
: 0)
STROKE_SIZES[shape.props.size] / 2 +
('size' in endShapeInfo.shape.props ? STROKE_SIZES[endShapeInfo.shape.props.size] / 2 : 0)
minDist -= offset
b.nudge(a, offset * (didFlip ? -1 : 1))

View file

@ -1,8 +1,8 @@
import { VecLike } from '@tldraw/primitives'
import { TLArrowShape, TLShapeId } from '@tldraw/tlschema'
import * as React from 'react'
import { ARROW_LABEL_FONT_SIZES, TEXT_PROPS } from '../../../../constants'
import { stopEventPropagation } from '../../../../utils/dom'
import { ARROW_LABEL_FONT_SIZES, TEXT_PROPS } from '../../shared/default-shape-constants'
import { useEditableText } from '../../shared/useEditableText'
import { TextHelpers } from '../../text/TextHelpers'

View file

@ -1,108 +0,0 @@
import { CubicSpline2d, Polyline2d } from '@tldraw/primitives'
import { TLArrowheadType, TLDashType } from '@tldraw/tlschema'
import { Segment, SegmentSvg } from './Segment'
/**
* A base interface for a shape's arrowheads.
*
* @public
*/
export interface TLArrowHeadModel {
id: string
type: TLArrowheadType
}
export function DashedArrowComponent({
strokeWidth,
dash,
spline,
start,
end,
}: {
dash: TLDashType
strokeWidth: number
spline: CubicSpline2d | Polyline2d
start: TLArrowHeadModel
end: TLArrowHeadModel
}) {
const { segments } = spline
return (
<g stroke="currentColor" strokeWidth={strokeWidth}>
{segments.map((segment, i) => (
<Segment
key={i}
strokeWidth={strokeWidth}
segment={segment}
dash={dash}
location={
segments.length === 1
? start && end
? 'middle'
: start
? 'middle'
: 'start'
: i === 0
? start
? 'end'
: 'start'
: i === segments.length - 1
? end
? 'middle'
: 'end'
: 'middle'
}
/>
))}
</g>
)
}
export function DashedArrowComponentSvg({
strokeWidth,
dash,
spline,
start,
end,
color,
}: {
dash: TLDashType
strokeWidth: number
spline: CubicSpline2d | Polyline2d
start: TLArrowHeadModel
end: TLArrowHeadModel
color: string
}) {
const { segments } = spline
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
g.setAttribute('stroke', color)
g.setAttribute('stroke-width', strokeWidth.toString())
segments.forEach((segment, i) => {
const segmentG = SegmentSvg({
strokeWidth,
segment,
dash,
location:
segments.length === 1
? start && end
? 'middle'
: start
? 'middle'
: 'start'
: i === 0
? start
? 'end'
: 'start'
: i === segments.length - 1
? end
? 'middle'
: 'end'
: 'middle',
})
g.appendChild(segmentG)
})
return g
}

View file

@ -1,41 +0,0 @@
import { CubicSpline2d, getStroke, Polyline2d } from '@tldraw/primitives'
import { getSvgPathFromStroke } from '../../../../utils/svg'
import { getDrawStrokeInfo } from '../../shared/getDrawStrokeInfo'
export function DrawArrowComponent({
strokeWidth,
spline,
}: {
strokeWidth: number
spline: CubicSpline2d | Polyline2d
}) {
const { segments } = spline
const allPoints = segments.flatMap((segment) => segment.lut)
const pf = getStroke(allPoints, getDrawStrokeInfo(strokeWidth))
const pfPath = getSvgPathFromStroke(pf)
return <path strokeWidth="0" stroke="none" fill="currentColor" d={pfPath} />
}
export function DrawArrowComponentSvg({
strokeWidth,
spline,
color,
}: {
strokeWidth: number
spline: CubicSpline2d | Polyline2d
color: string
}) {
const { segments } = spline
const allPoints = segments.flatMap((segment) => segment.lut)
const pf = getStroke(allPoints, getDrawStrokeInfo(strokeWidth))
const pfPath = getSvgPathFromStroke(pf)
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
path.setAttribute('stroke-width', '0')
path.setAttribute('stroke', 'none')
path.setAttribute('fill', color)
path.setAttribute('d', pfPath)
return path
}

View file

@ -1,37 +0,0 @@
import { CubicSegment2d, LineSegment2d } from '@tldraw/primitives'
import { TLDashType } from '@tldraw/tlschema'
import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
export interface SegmentProps {
strokeWidth: number
dash: TLDashType
segment: LineSegment2d | CubicSegment2d
location: 'start' | 'middle' | 'end' | 'solo'
}
export function Segment({ segment, dash, strokeWidth, location }: SegmentProps) {
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(segment.length, strokeWidth, {
style: dash,
start: location === 'end' || location === 'middle' ? 'outset' : 'none',
end: location === 'start' || location === 'middle' ? 'outset' : 'none',
})
return (
<path strokeDasharray={strokeDasharray} strokeDashoffset={strokeDashoffset} d={segment.path} />
)
}
export function SegmentSvg({ segment, dash, strokeWidth, location }: SegmentProps) {
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(segment.length, strokeWidth, {
style: dash,
start: location === 'end' || location === 'middle' ? 'outset' : 'none',
end: location === 'start' || location === 'middle' ? 'outset' : 'none',
})
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
path.setAttribute('stroke-dasharray', strokeDasharray.toString())
path.setAttribute('stroke-dashoffset', strokeDashoffset.toString())
path.setAttribute('d', segment.path)
return path
}

View file

@ -3,9 +3,6 @@ import { AssetRecordType, TLAssetId, TLBookmarkAsset, TLBookmarkShape } from '@t
import { debounce, getHashForString } from '@tldraw/utils'
import { HTMLContainer } from '../../../components/HTMLContainer'
const DEFAULT_BOOKMARK_WIDTH = 300
const DEFAULT_BOOKMARK_HEIGHT = 320
import { isValidUrl } from '../../../utils/data'
import {
getRotatedBoxShadow,
@ -29,8 +26,8 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
override defaultProps(): TLBookmarkShape['props'] {
return {
url: '',
w: DEFAULT_BOOKMARK_WIDTH,
h: DEFAULT_BOOKMARK_HEIGHT,
w: 300,
h: 320,
assetId: null,
}
}

View file

@ -1,6 +1,5 @@
import { TLStyleType } from '@tldraw/tlschema'
import { StateNode } from '../../tools/StateNode'
import { DrawShapeUtil } from './DrawShapeUtil'
import { Drawing } from './toolStates/Drawing'
import { Idle } from './toolStates/Idle'
@ -9,8 +8,7 @@ export class DrawShapeTool extends StateNode {
static initial = 'idle'
static children = () => [Idle, Drawing]
styles = ['color', 'dash', 'fill', 'size'] as TLStyleType[]
shapeType = 'draw'
shapeType = DrawShapeUtil
onExit = () => {
const drawingState = this.children!['drawing'] as Drawing

View file

@ -15,6 +15,7 @@ import { last, rng } from '@tldraw/utils'
import { SVGContainer } from '../../../components/SVGContainer'
import { getSvgPathFromStroke, getSvgPathFromStrokePoints } from '../../../utils/svg'
import { ShapeUtil, TLOnResizeHandler } from '../ShapeUtil'
import { STROKE_SIZES } from '../shared/default-shape-constants'
import { getShapeFillSvg, ShapeFill } from '../shared/ShapeFill'
import { TLExportColors } from '../shared/TLExportColors'
import { useForceSolid } from '../shared/useForceSolid'
@ -59,7 +60,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
hitTestPoint(shape: TLDrawShape, point: VecLike): boolean {
const outline = this.outline(shape)
const zoomLevel = this.editor.zoomLevel
const offsetDist = this.editor.getStrokeWidth(shape.props.size) / zoomLevel
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
if (shape.props.segments[0].points.some((pt) => Vec2d.Dist(point, pt) < offsetDist * 1.5)) {
@ -88,7 +89,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
if (shape.props.segments.length === 1 && shape.props.segments[0].points.length < 4) {
const zoomLevel = this.editor.zoomLevel
const offsetDist = this.editor.getStrokeWidth(shape.props.size) / zoomLevel
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
if (
shape.props.segments[0].points.some(
@ -118,7 +119,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
render(shape: TLDrawShape) {
const forceSolid = useForceSolid()
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
const strokeWidth = STROKE_SIZES[shape.props.size]
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
@ -155,7 +156,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
<path
d={getSvgPathFromStroke(strokeOutlinePoints, true)}
strokeLinecap="round"
fill="currentColor"
fill={`var(--palette-${shape.props.color})`}
/>
</SVGContainer>
)
@ -172,7 +173,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
d={solidStrokePath}
strokeLinecap="round"
fill="none"
stroke="currentColor"
stroke={`var(--palette-${shape.props.color})`}
strokeWidth={strokeWidth}
strokeDasharray={getDrawShapeStrokeDashArray(shape, strokeWidth)}
strokeDashoffset="0"
@ -183,7 +184,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
indicator(shape: TLDrawShape) {
const forceSolid = useForceSolid()
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
const strokeWidth = STROKE_SIZES[shape.props.size]
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
let sw = strokeWidth
@ -210,7 +211,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
toSvg(shape: TLDrawShape, _font: string | undefined, colors: TLExportColors) {
const { color } = shape.props
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
const strokeWidth = STROKE_SIZES[shape.props.size]
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
@ -296,7 +297,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
expandSelectionOutlinePx(shape: TLDrawShape): number {
const multiplier = shape.props.dash === 'draw' ? 1.6 : 1
return (this.editor.getStrokeWidth(shape.props.size) * multiplier) / 2
return (STROKE_SIZES[shape.props.size] * multiplier) / 2
}
}

View file

@ -1,5 +1,5 @@
import { EASINGS, PI, SIN, StrokeOptions, Vec2d } from '@tldraw/primitives'
import { TLDashType, TLDrawShape, TLDrawShapeSegment } from '@tldraw/tlschema'
import { TLDefaultDashStyle, TLDrawShape, TLDrawShapeSegment } from '@tldraw/tlschema'
const PEN_EASING = (t: number) => t * 0.65 + SIN((t * PI) / 2) * 0.35
@ -57,7 +57,7 @@ export function getHighlightFreehandSettings({
}
export function getFreehandOptions(
shapeProps: { dash: TLDashType; isPen: boolean; isComplete: boolean },
shapeProps: { dash: TLDefaultDashStyle; isPen: boolean; isComplete: boolean },
strokeWidth: number,
forceComplete: boolean,
forceSolid: boolean

View file

@ -1,19 +1,21 @@
import { Matrix2d, snapAngle, toFixed, Vec2d } from '@tldraw/primitives'
import {
createShapeId,
TLDefaultSizeStyle,
TLDrawShape,
TLDrawShapeSegment,
TLHighlightShape,
TLShapePartial,
TLSizeType,
Vec2dModel,
} from '@tldraw/tlschema'
import { last, structuredClone } from '@tldraw/utils'
import { DRAG_DISTANCE } from '../../../../constants'
import { uniqueId } from '../../../../utils/data'
import { TLEventHandlers, TLPointerEventInfo } from '../../../types/event-types'
import { StateNode } from '../../../tools/StateNode'
import { TLEventHandlers, TLPointerEventInfo } from '../../../types/event-types'
import { HighlightShapeUtil } from '../../highlight/HighlightShapeUtil'
import { STROKE_SIZES } from '../../shared/default-shape-constants'
import { DrawShapeUtil } from '../DrawShapeUtil'
type DrawableShape = TLDrawShape | TLHighlightShape
@ -24,7 +26,9 @@ export class Drawing extends StateNode {
initialShape?: DrawableShape
shapeType: DrawableShape['type'] = this.parent.id === 'highlight' ? 'highlight' : 'draw'
shapeType = this.parent.id === 'highlight' ? HighlightShapeUtil : DrawShapeUtil
util = this.editor.getShapeUtil(this.shapeType)
isPen = false
@ -134,13 +138,13 @@ export class Drawing extends StateNode {
}
canClose() {
return this.shapeType !== 'highlight'
return this.shapeType.type !== 'highlight'
}
getIsClosed(segments: TLDrawShapeSegment[], size: TLSizeType) {
getIsClosed(segments: TLDrawShapeSegment[], size: TLDefaultSizeStyle) {
if (!this.canClose()) return false
const strokeWidth = this.editor.getStrokeWidth(size)
const strokeWidth = STROKE_SIZES[size]
const firstPoint = segments[0].points[0]
const lastSegment = segments[segments.length - 1]
const lastPoint = lastSegment.points[lastSegment.points.length - 1]
@ -215,7 +219,7 @@ export class Drawing extends StateNode {
const shapePartial: TLShapePartial<DrawableShape> = {
id: shape.id,
type: this.shapeType,
type: this.shapeType.type,
props: {
segments,
},
@ -242,7 +246,7 @@ export class Drawing extends StateNode {
this.editor.createShapes<DrawableShape>([
{
id,
type: this.shapeType,
type: this.shapeType.type,
x: originPagePoint.x,
y: originPagePoint.y,
props: {
@ -345,7 +349,7 @@ export class Drawing extends StateNode {
const shapePartial: TLShapePartial<DrawableShape> = {
id,
type: this.shapeType,
type: this.shapeType.type,
props: {
segments: [...segments, newSegment],
},
@ -405,7 +409,7 @@ export class Drawing extends StateNode {
const shapePartial: TLShapePartial<DrawableShape> = {
id,
type: this.shapeType,
type: this.shapeType.type,
props: {
segments: finalSegments,
},
@ -547,7 +551,7 @@ export class Drawing extends StateNode {
const shapePartial: TLShapePartial<DrawableShape> = {
id,
type: this.shapeType,
type: this.shapeType.type,
props: {
segments: newSegments,
},
@ -592,7 +596,7 @@ export class Drawing extends StateNode {
const shapePartial: TLShapePartial<DrawableShape> = {
id,
type: this.shapeType,
type: this.shapeType.type,
props: {
segments: newSegments,
},
@ -609,7 +613,7 @@ export class Drawing extends StateNode {
// Set a maximum length for the lines array; after 200 points, complete the line.
if (newPoints.length > 500) {
this.editor.updateShapes([{ id, type: this.shapeType, props: { isComplete: true } }])
this.editor.updateShapes([{ id, type: this.shapeType.type, props: { isComplete: true } }])
const { currentPagePoint } = this.editor.inputs
@ -618,7 +622,7 @@ export class Drawing extends StateNode {
this.editor.createShapes<DrawableShape>([
{
id: newShapeId,
type: this.shapeType,
type: this.shapeType.type,
x: toFixed(currentPagePoint.x),
y: toFixed(currentPagePoint.y),
props: {

View file

@ -1,8 +1,9 @@
import { BaseBoxShapeTool } from '../../tools/BaseBoxShapeTool/BaseBoxShapeTool'
import { FrameShapeUtil } from './FrameShapeUtil'
export class FrameShapeTool extends BaseBoxShapeTool {
static override id = 'frame'
static initial = 'idle'
shapeType = 'frame'
shapeType = FrameShapeUtil
}

View file

@ -1,5 +1,5 @@
import { TLStyleType } from '@tldraw/tlschema'
import { StateNode } from '../../tools/StateNode'
import { GeoShapeUtil } from './GeoShapeUtil'
import { Idle } from './toolStates/Idle'
import { Pointing } from './toolStates/Pointing'
@ -8,15 +8,5 @@ export class GeoShapeTool extends StateNode {
static initial = 'idle'
static children = () => [Idle, Pointing]
styles = [
'color',
'dash',
'fill',
'size',
'geo',
'font',
'align',
'verticalAlign',
] as TLStyleType[]
shapeType = 'geo'
shapeType = GeoShapeUtil
}

View file

@ -12,12 +12,17 @@ import {
Vec2d,
VecLike,
} from '@tldraw/primitives'
import { TLDashType, TLGeoShape } from '@tldraw/tlschema'
import { TLDefaultDashStyle, TLGeoShape } from '@tldraw/tlschema'
import { SVGContainer } from '../../../components/SVGContainer'
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
import { Editor } from '../../Editor'
import { BaseBoxShapeUtil } from '../BaseBoxShapeUtil'
import { TLOnEditEndHandler, TLOnResizeHandler } from '../ShapeUtil'
import {
FONT_FAMILIES,
LABEL_FONT_SIZES,
STROKE_SIZES,
TEXT_PROPS,
} from '../shared/default-shape-constants'
import { getTextLabelSvgElement } from '../shared/getTextLabelSvgElement'
import { HyperlinkButton } from '../shared/HyperlinkButton'
import { TextLabel } from '../shared/TextLabel'
@ -90,7 +95,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
if (shape.props.fill === 'none') {
const zoomLevel = this.editor.zoomLevel
const offsetDist = this.editor.getStrokeWidth(shape.props.size) / zoomLevel
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
// Check the outline
for (let i = 0; i < outline.length; i++) {
const C = outline[i]
@ -334,7 +339,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
const { id, type, props } = shape
const forceSolid = useForceSolid()
const strokeWidth = this.editor.getStrokeWidth(props.size)
const strokeWidth = STROKE_SIZES[props.size]
const { w, color, labelColor, fill, dash, growY, font, align, verticalAlign, size, text } =
props
@ -444,7 +449,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
align={align}
verticalAlign={verticalAlign}
text={text}
labelColor={this.editor.getCssColor(labelColor)}
labelColor={labelColor}
wrap
/>
{shape.props.url && (
@ -459,7 +464,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
const { w, h, growY, size } = props
const forceSolid = useForceSolid()
const strokeWidth = this.editor.getStrokeWidth(size)
const strokeWidth = STROKE_SIZES[size]
switch (props.geo) {
case 'ellipse': {
@ -499,7 +504,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
toSvg(shape: TLGeoShape, font: string, colors: TLExportColors) {
const { id, props } = shape
const strokeWidth = this.editor.getStrokeWidth(props.size)
const strokeWidth = STROKE_SIZES[props.size]
let svgElm: SVGElement
@ -954,7 +959,7 @@ function getLines(props: TLGeoShape['props'], sw: number) {
}
}
function getXBoxLines(w: number, h: number, sw: number, dash: TLDashType) {
function getXBoxLines(w: number, h: number, sw: number, dash: TLDefaultDashStyle) {
const inset = dash === 'draw' ? 0.62 : 0
if (dash === 'dashed') {

View file

@ -44,7 +44,7 @@ export const DashStyleEllipse = React.memo(function DashStyleEllipse({
width={toDomPrecision(w)}
height={toDomPrecision(h)}
fill="none"
stroke="currentColor"
stroke={`var(--palette-${color})`}
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
pointerEvents="all"

View file

@ -41,7 +41,7 @@ export const DashStyleOval = React.memo(function DashStyleOval({
width={toDomPrecision(w)}
height={toDomPrecision(h)}
fill="none"
stroke="currentColor"
stroke={`var(--palette-${color})`}
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
pointerEvents="all"

View file

@ -31,7 +31,12 @@ export const DashStylePolygon = React.memo(function DashStylePolygon({
d={`M${l[0].x},${l[0].y}L${l[1].x},${l[1].y}`}
/>
))}
<g strokeWidth={strokeWidth} stroke="currentColor" fill="none" pointerEvents="all">
<g
strokeWidth={strokeWidth}
stroke={`var(--palette-${color})`}
fill="none"
pointerEvents="all"
>
{Array.from(Array(outline.length)).map((_, i) => {
const A = outline[i]
const B = outline[(i + 1) % outline.length]
@ -71,7 +76,7 @@ export const DashStylePolygon = React.memo(function DashStylePolygon({
<path
key={`line_fg_${i}`}
d={`M${A.x},${A.y}L${B.x},${B.y}`}
stroke="currentColor"
stroke={`var(--palette-${color})`}
strokeWidth={strokeWidth}
fill="none"
strokeDasharray={strokeDasharray}

View file

@ -32,7 +32,7 @@ export const DrawStyleEllipse = React.memo(function DrawStyleEllipse({
return (
<>
<ShapeFill d={innerPath} color={color} fill={fill} />
<path d={outerPath} fill="currentColor" strokeWidth={0} pointerEvents="all" />
<path d={outerPath} fill={`var(--palette-${color})`} strokeWidth={0} pointerEvents="all" />
</>
)
})

View file

@ -32,7 +32,12 @@ export const DrawStylePolygon = React.memo(function DrawStylePolygon({
return (
<>
<ShapeFill d={innerPathData} fill={fill} color={color} />
<path d={strokePathData} stroke="currentColor" strokeWidth={strokeWidth} fill="none" />
<path
d={strokePathData}
stroke={`var(--palette-${color})`}
strokeWidth={strokeWidth}
fill="none"
/>
</>
)
})

View file

@ -1,5 +1,5 @@
import { Box2d, getStarBounds } from '@tldraw/primitives'
import { TLGeoShape, createShapeId } from '@tldraw/tlschema'
import { GeoShapeGeoStyle, TLGeoShape, createShapeId } from '@tldraw/tlschema'
import { StateNode } from '../../../tools/StateNode'
import { TLEventHandlers } from '../../../types/event-types'
@ -23,7 +23,7 @@ export class Pointing extends StateNode {
props: {
w: 1,
h: 1,
geo: this.editor.instanceState.propsForNextShape.geo,
geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle),
},
},
])
@ -70,7 +70,7 @@ export class Pointing extends StateNode {
x: originPagePoint.x,
y: originPagePoint.y,
props: {
geo: this.editor.instanceState.propsForNextShape.geo,
geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle),
w: 1,
h: 1,
},
@ -92,7 +92,7 @@ export class Pointing extends StateNode {
x: shape.x - delta.x,
y: shape.y - delta.y,
props: {
geo: this.editor.instanceState.propsForNextShape.geo,
geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle),
w: bounds.width,
h: bounds.height,
},

View file

@ -1,17 +1,15 @@
import { TLStyleType } from '@tldraw/tlschema'
// shared custody
import { StateNode } from '../../tools/StateNode'
// shared custody
import { Drawing } from '../draw/toolStates/Drawing'
import { Idle } from '../draw/toolStates/Idle'
import { HighlightShapeUtil } from './HighlightShapeUtil'
export class HighlightShapeTool extends StateNode {
static override id = 'highlight'
static initial = 'idle'
static children = () => [Idle, Drawing]
styles = ['color', 'size'] as TLStyleType[]
shapeType = 'highlight'
shapeType = HighlightShapeUtil
onExit = () => {
const drawingState = this.children!['drawing'] as Drawing

View file

@ -3,10 +3,10 @@ import { Box2d, getStrokePoints, linesIntersect, Vec2d, VecLike } from '@tldraw/
import { TLDrawShapeSegment, TLHighlightShape } from '@tldraw/tlschema'
import { last, rng } from '@tldraw/utils'
import { SVGContainer } from '../../../components/SVGContainer'
import { FONT_SIZES } from '../../../constants'
import { getSvgPathFromStrokePoints } from '../../../utils/svg'
import { getHighlightFreehandSettings, getPointsFromSegments } from '../draw/getPath'
import { ShapeUtil, TLOnResizeHandler } from '../ShapeUtil'
import { FONT_SIZES } from '../shared/default-shape-constants'
import { TLExportColors } from '../shared/TLExportColors'
import { useForceSolid } from '../shared/useForceSolid'

View file

@ -1,5 +1,5 @@
import { TLStyleType } from '@tldraw/tlschema'
import { StateNode } from '../../tools/StateNode'
import { LineShapeUtil } from './LineShapeUtil'
import { Idle } from './toolStates/Idle'
import { Pointing } from './toolStates/Pointing'
@ -8,7 +8,5 @@ export class LineShapeTool extends StateNode {
static initial = 'idle'
static children = () => [Idle, Pointing]
shapeType = 'line'
styles = ['color', 'dash', 'size', 'spline'] as TLStyleType[]
shapeType = LineShapeUtil
}

View file

@ -16,6 +16,7 @@ import { WeakMapCache } from '../../../utils/WeakMapCache'
import { ShapeUtil, TLOnHandleChangeHandler, TLOnResizeHandler } from '../ShapeUtil'
import { ShapeFill } from '../shared/ShapeFill'
import { TLExportColors } from '../shared/TLExportColors'
import { STROKE_SIZES } from '../shared/default-shape-constants'
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
import { useForceSolid } from '../shared/useForceSolid'
import { getLineDrawPath, getLineIndicatorPath, getLinePoints } from './components/getLinePath'
@ -172,7 +173,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
hitTestPoint(shape: TLLineShape, point: Vec2d): boolean {
const zoomLevel = this.editor.zoomLevel
const offsetDist = this.editor.getStrokeWidth(shape.props.size) / zoomLevel
const offsetDist = STROKE_SIZES[shape.props.size] / zoomLevel
return pointNearToPolyline(point, this.outline(shape), offsetDist)
}
@ -183,7 +184,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
render(shape: TLLineShape) {
const forceSolid = useForceSolid()
const spline = getSplineForLineShape(shape)
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
const strokeWidth = STROKE_SIZES[shape.props.size]
const { dash, color } = shape.props
@ -196,7 +197,12 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
return (
<SVGContainer id={shape.id}>
<ShapeFill d={pathData} fill={'none'} color={color} />
<path d={pathData} stroke="currentColor" strokeWidth={strokeWidth} fill="none" />
<path
d={pathData}
stroke={`var(--palette-${color})`}
strokeWidth={strokeWidth}
fill="none"
/>
</SVGContainer>
)
}
@ -208,7 +214,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
return (
<SVGContainer id={shape.id}>
<ShapeFill d={pathData} fill={'none'} color={color} />
<g stroke="currentColor" strokeWidth={strokeWidth}>
<g stroke={`var(--palette-${color})`} strokeWidth={strokeWidth}>
{spline.segments.map((segment, i) => {
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
segment.length,
@ -242,7 +248,12 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
return (
<SVGContainer id={shape.id}>
<ShapeFill d={innerPathData} fill={'none'} color={color} />
<path d={outerPathData} stroke="currentColor" strokeWidth={strokeWidth} fill="none" />
<path
d={outerPathData}
stroke={`var(--palette-${color})`}
strokeWidth={strokeWidth}
fill="none"
/>
</SVGContainer>
)
}
@ -256,7 +267,12 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
return (
<SVGContainer id={shape.id}>
<ShapeFill d={splinePath} fill={'none'} color={color} />
<path strokeWidth={strokeWidth} stroke="currentColor" fill="none" d={splinePath} />
<path
strokeWidth={strokeWidth}
stroke={`var(--palette-${color})`}
fill="none"
d={splinePath}
/>
</SVGContainer>
)
}
@ -265,7 +281,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
return (
<SVGContainer id={shape.id}>
<ShapeFill d={splinePath} fill={'none'} color={color} />
<g stroke="currentColor" strokeWidth={strokeWidth}>
<g stroke={`var(--palette-${color})`} strokeWidth={strokeWidth}>
{spline.segments.map((segment, i) => {
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
segment.length,
@ -299,8 +315,8 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
<path
d={getLineDrawPath(shape, spline, strokeWidth)}
strokeWidth={1}
stroke="currentColor"
fill="currentColor"
stroke={`var(--palette-${color})`}
fill={`var(--palette-${color})`}
/>
</SVGContainer>
)
@ -309,7 +325,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
}
indicator(shape: TLLineShape) {
const strokeWidth = this.editor.getStrokeWidth(shape.props.size)
const strokeWidth = STROKE_SIZES[shape.props.size]
const spline = getSplineForLineShape(shape)
const { dash } = shape.props
@ -334,7 +350,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
const { color: _color, size } = shape.props
const color = colors.fill[_color]
const spline = getSplineForLineShape(shape)
return getLineSvg(shape, spline, color, this.editor.getStrokeWidth(size))
return getLineSvg(shape, spline, color, STROKE_SIZES[size])
}
}

View file

@ -1,5 +1,5 @@
import { CubicSpline2d, Polyline2d } from '@tldraw/primitives'
import { TLDashType, TLLineShape } from '@tldraw/tlschema'
import { TLDefaultDashStyle, TLLineShape } from '@tldraw/tlschema'
import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
import { getLineDrawPath } from './getLinePath'
@ -31,7 +31,7 @@ export function getDashedLineShapeSvg({
spline,
color,
}: {
dash: TLDashType
dash: TLDefaultDashStyle
strokeWidth: number
spline: CubicSpline2d | Polyline2d
color: string

View file

@ -1,5 +1,5 @@
import { TLStyleType } from '@tldraw/tlschema'
import { StateNode } from '../../tools/StateNode'
import { NoteShapeUtil } from './NoteShapeUtil'
import { Idle } from './toolStates/Idle'
import { Pointing } from './toolStates/Pointing'
@ -8,6 +8,5 @@ export class NoteShapeTool extends StateNode {
static initial = 'idle'
static children = () => [Idle, Pointing]
styles = ['color', 'size', 'align', 'verticalAlign', 'font'] as TLStyleType[]
shapeType = 'note'
shapeType = NoteShapeUtil
}

View file

@ -1,8 +1,8 @@
import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives'
import { TLNoteShape } from '@tldraw/tlschema'
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
import { Editor } from '../../Editor'
import { ShapeUtil, TLOnEditEndHandler } from '../ShapeUtil'
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
import { getTextLabelSvgElement } from '../shared/getTextLabelSvgElement'
import { HyperlinkButton } from '../shared/HyperlinkButton'
import { TextLabel } from '../shared/TextLabel'
@ -83,7 +83,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
align={align}
verticalAlign={verticalAlign}
text={text}
labelColor="inherit"
labelColor={adjustedColor}
wrap
/>
</div>

View file

@ -1,4 +1,4 @@
import { TLColorType, TLFillType } from '@tldraw/tlschema'
import { TLDefaultColorStyle, TLDefaultFillStyle } from '@tldraw/tlschema'
import * as React from 'react'
import { useValue } from 'signia-react'
import { HASH_PATERN_ZOOM_NAMES } from '../../../constants'
@ -7,8 +7,8 @@ import { TLExportColors } from './TLExportColors'
export interface ShapeFillProps {
d: string
fill: TLFillType
color: TLColorType
fill: TLDefaultFillStyle
color: TLDefaultColorStyle
}
export const ShapeFill = React.memo(function ShapeFill({ d, color, fill }: ShapeFillProps) {

View file

@ -1,10 +1,10 @@
import { TLColorType } from '@tldraw/tlschema'
import { TLDefaultColorStyle } from '@tldraw/tlschema'
export type TLExportColors = {
fill: Record<TLColorType, string>
pattern: Record<TLColorType, string>
semi: Record<TLColorType, string>
highlight: Record<TLColorType, string>
fill: Record<TLDefaultColorStyle, string>
pattern: Record<TLDefaultColorStyle, string>
semi: Record<TLDefaultColorStyle, string>
highlight: Record<TLDefaultColorStyle, string>
solid: string
text: string
background: string

View file

@ -1,16 +1,17 @@
import {
TLAlignType,
TLFillType,
TLFontType,
TLDefaultColorStyle,
TLDefaultFillStyle,
TLDefaultFontStyle,
TLDefaultHorizontalAlignStyle,
TLDefaultSizeStyle,
TLDefaultVerticalAlignStyle,
TLShape,
TLSizeType,
TLVerticalAlignType,
} from '@tldraw/tlschema'
import React from 'react'
import { LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
import { stopEventPropagation } from '../../../utils/dom'
import { isLegacyAlign } from '../../../utils/legacy'
import { TextHelpers } from '../text/TextHelpers'
import { LABEL_FONT_SIZES, TEXT_PROPS } from './default-shape-constants'
import { useEditableText } from './useEditableText'
export const TextLabel = React.memo(function TextLabel<
@ -28,14 +29,14 @@ export const TextLabel = React.memo(function TextLabel<
}: {
id: T['id']
type: T['type']
size: TLSizeType
font: TLFontType
fill?: TLFillType
align: TLAlignType
verticalAlign: TLVerticalAlignType
size: TLDefaultSizeStyle
font: TLDefaultFontStyle
fill?: TLDefaultFillStyle
align: TLDefaultHorizontalAlignStyle
verticalAlign: TLDefaultVerticalAlignStyle
wrap?: boolean
text: string
labelColor: string
labelColor: TLDefaultColorStyle
}) {
const {
rInput,
@ -77,7 +78,7 @@ export const TextLabel = React.memo(function TextLabel<
lineHeight: LABEL_FONT_SIZES[size] * TEXT_PROPS.lineHeight + 'px',
minHeight: isEmpty ? LABEL_FONT_SIZES[size] * TEXT_PROPS.lineHeight + 32 : 0,
minWidth: isEmpty ? 33 : 0,
color: labelColor,
color: `var(--palette-${labelColor})`,
}}
>
<div className="tl-text tl-text-content" dir="ltr">

View file

@ -1,5 +1,9 @@
import { Box2d } from '@tldraw/primitives'
import { Box2dModel, TLAlignType, TLVerticalAlignType } from '@tldraw/tlschema'
import {
Box2dModel,
TLDefaultHorizontalAlignStyle,
TLDefaultVerticalAlignStyle,
} from '@tldraw/tlschema'
import { correctSpacesToNbsp } from '../../../utils/string'
import { Editor } from '../../Editor'
@ -10,8 +14,8 @@ export function createTextSvgElementFromSpans(
opts: {
fontSize: number
fontFamily: string
textAlign: TLAlignType
verticalTextAlign: TLVerticalAlignType
textAlign: TLDefaultHorizontalAlignStyle
verticalTextAlign: TLDefaultVerticalAlignStyle
fontWeight: string
fontStyle: string
lineHeight: number

View file

@ -0,0 +1,58 @@
import { TLDefaultFontStyle, TLDefaultSizeStyle } from '@tldraw/tlschema'
/** @public */
export const TEXT_PROPS = {
lineHeight: 1.35,
fontWeight: 'normal',
fontVariant: 'normal',
fontStyle: 'normal',
padding: '0px',
maxWidth: 'auto',
}
/** @public */
export const STROKE_SIZES: Record<TLDefaultSizeStyle, number> = {
s: 2,
m: 3.5,
l: 5,
xl: 10,
}
/** @public */
export const FONT_SIZES: Record<TLDefaultSizeStyle, number> = {
s: 18,
m: 24,
l: 36,
xl: 44,
}
/** @public */
export const LABEL_FONT_SIZES: Record<TLDefaultSizeStyle, number> = {
s: 18,
m: 22,
l: 26,
xl: 32,
}
/** @public */
export const ARROW_LABEL_FONT_SIZES: Record<TLDefaultSizeStyle, number> = {
s: 18,
m: 20,
l: 24,
xl: 28,
}
/** @public */
export const FONT_FAMILIES: Record<TLDefaultFontStyle, string> = {
draw: 'var(--tl-font-draw)',
sans: 'var(--tl-font-sans)',
serif: 'var(--tl-font-serif)',
mono: 'var(--tl-font-mono)',
}
/** @internal */
export const MIN_ARROW_LENGTH = 48
/** @internal */
export const BOUND_ARROW_OFFSET = 10
/** @internal */
export const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10

View file

@ -1,13 +0,0 @@
import { StrokeOptions } from '@tldraw/primitives'
export function getDrawStrokeInfo(strokeWidth: number) {
const options: StrokeOptions = {
size: strokeWidth * 1.3,
thinning: 0.36,
streamline: 0,
smoothing: 0.25,
simulatePressure: true,
last: true,
}
return options
}

View file

@ -1,10 +1,10 @@
import { TLDashType } from '@tldraw/tlschema'
import { TLDefaultDashStyle } from '@tldraw/tlschema'
export function getPerfectDashProps(
totalLength: number,
strokeWidth: number,
opts = {} as Partial<{
style: TLDashType
style: TLDefaultDashStyle
snap: number
end: 'skip' | 'outset' | 'none'
start: 'skip' | 'outset' | 'none'

View file

@ -1,12 +0,0 @@
import { TLDashType } from '@tldraw/tlschema'
export function getStrokeDashArray(dash: TLDashType, strokeWidth: number) {
switch (dash) {
case 'dashed':
return `${strokeWidth * 2} ${strokeWidth * 2}`
case 'dotted':
return `0 ${strokeWidth * 2}`
case 'solid':
return ``
}
}

View file

@ -1,9 +1,9 @@
import { Box2d } from '@tldraw/primitives'
import { TLGeoShape, TLNoteShape } from '@tldraw/tlschema'
import { LABEL_FONT_SIZES, TEXT_PROPS } from '../../../constants'
import { getLegacyOffsetX } from '../../../utils/legacy'
import { Editor } from '../../Editor'
import { createTextSvgElementFromSpans } from './createTextSvgElementFromSpans'
import { LABEL_FONT_SIZES, TEXT_PROPS } from './default-shape-constants'
export function getTextLabelSvgElement({
bounds,

View file

@ -1,4 +0,0 @@
// Is the color black? Used in a few places to swap for white instead.
export function isBlackColor(color: string) {
return color === 'black' || color === ' #1d1d1d'
}

View file

@ -1,5 +1,5 @@
import { TLStyleType } from '@tldraw/tlschema'
import { StateNode } from '../../tools/StateNode'
import { TextShapeUtil } from './TextShapeUtil'
import { Idle } from './toolStates/Idle'
import { Pointing } from './toolStates/Pointing'
@ -9,6 +9,5 @@ export class TextShapeTool extends StateNode {
static children = () => [Idle, Pointing]
styles = ['color', 'font', 'align', 'size'] as TLStyleType[]
shapeType = 'text'
shapeType = TextShapeUtil
}

View file

@ -2,12 +2,12 @@
import { Box2d, toDomPrecision, Vec2d } from '@tldraw/primitives'
import { TLTextShape } from '@tldraw/tlschema'
import { HTMLContainer } from '../../../components/HTMLContainer'
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../../../constants'
import { stopEventPropagation } from '../../../utils/dom'
import { WeakMapCache } from '../../../utils/WeakMapCache'
import { Editor } from '../../Editor'
import { ShapeUtil, TLOnEditEndHandler, TLOnResizeHandler, TLShapeUtilFlag } from '../ShapeUtil'
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
import { resizeScaled } from '../shared/resizeScaled'
import { TLExportColors } from '../shared/TLExportColors'
import { useEditableText } from '../shared/useEditableText'

View file

@ -1,3 +1,4 @@
import { TLShapeUtilConstructor } from '../../shapes/ShapeUtil'
import { StateNode } from '../StateNode'
import { Idle } from './children/Idle'
import { Pointing } from './children/Pointing'
@ -8,5 +9,5 @@ export abstract class BaseBoxShapeTool extends StateNode {
static initial = 'idle'
static children = () => [Idle, Pointing]
abstract shapeType: string
abstract shapeType: TLShapeUtilConstructor<any>
}

View file

@ -21,7 +21,7 @@ export class Pointing extends StateNode {
if (this.editor.inputs.isDragging) {
const { originPagePoint } = this.editor.inputs
const shapeType = (this.parent as BaseBoxShapeTool)!.shapeType as TLBaseBoxShape['type']
const shapeType = (this.parent as BaseBoxShapeTool)!.shapeType.type as TLBaseBoxShape['type']
const id = createShapeId()
@ -78,7 +78,7 @@ export class Pointing extends StateNode {
this.editor.mark(this.markId)
const shapeType = (this.parent as BaseBoxShapeTool)!.shapeType as TLBaseBoxShape['type']
const shapeType = (this.parent as BaseBoxShapeTool)!.shapeType.type as TLBaseBoxShape['type']
const id = createShapeId()

View file

@ -11,8 +11,6 @@ export class HandTool extends StateNode {
static initial = 'idle'
static children = () => [Idle, Pointing, Dragging]
styles = []
onDoubleClick: TLClickEvent = (info) => {
if (info.phase === 'settle') {
const { currentScreenPoint } = this.editor.inputs

View file

@ -1,6 +1,4 @@
import { StateNode } from '../StateNode'
import { TLStyleType } from '@tldraw/tlschema'
import { Brushing } from './children/Brushing'
import { Crop } from './children/Crop/Crop'
import { Cropping } from './children/Cropping'
@ -42,8 +40,6 @@ export class SelectTool extends StateNode {
DraggingHandle,
]
styles = ['color', 'dash', 'fill', 'size'] as TLStyleType[]
onExit = () => {
if (this.editor.pageState.editingId) {
this.editor.setEditingId(null)

View file

@ -1,13 +1,13 @@
import { Vec2d } from '@tldraw/primitives'
import { TLBaseShape, TLImageCrop, TLShapePartial } from '@tldraw/tlschema'
import { TLBaseShape, TLImageShapeCrop, TLShapePartial } from '@tldraw/tlschema'
import { deepCopy } from '@tldraw/utils'
import { Editor } from '../../../../../Editor'
export type ShapeWithCrop = TLBaseShape<string, { w: number; h: number; crop: TLImageCrop }>
export type ShapeWithCrop = TLBaseShape<string, { w: number; h: number; crop: TLImageShapeCrop }>
export function getTranslateCroppedImageChange(
editor: Editor,
shape: TLBaseShape<string, { w: number; h: number; crop: TLImageCrop }>,
shape: TLBaseShape<string, { w: number; h: number; crop: TLImageShapeCrop }>,
delta: Vec2d
) {
if (!shape) {

View file

@ -1,7 +1,7 @@
import { SelectionHandle, Vec2d } from '@tldraw/primitives'
import {
TLBaseShape,
TLImageCrop,
TLImageShapeCrop,
TLImageShapeProps,
TLShape,
TLShapePartial,
@ -72,7 +72,7 @@ export class Cropping extends StateNode {
})
}
private getDefaultCrop = (): TLImageCrop => ({
private getDefaultCrop = (): TLImageShapeCrop => ({
topLeft: { x: 0, y: 0 },
bottomRight: { x: 1, y: 1 },
})
@ -187,7 +187,7 @@ export class Cropping extends StateNode {
newPoint.add(pointDelta.rot(shape.rotation))
const partial: TLShapePartial<
TLBaseShape<string, { w: number; h: number; crop: TLImageCrop }>
TLBaseShape<string, { w: number; h: number; crop: TLImageShapeCrop }>
> = {
id: shape.id,
type: shape.type,

View file

@ -2,7 +2,7 @@ import { sortByIndex } from '@tldraw/indices'
import { Matrix2d, snapAngle, Vec2d } from '@tldraw/primitives'
import {
TLArrowShape,
TLArrowTerminal,
TLArrowShapeTerminal,
TLHandle,
TLShapeId,
TLShapePartial,
@ -264,7 +264,7 @@ export class DraggingHandle extends StateNode {
// Arrows
if (initialHandle.canBind) {
const bindingAfter = (next.props as any)[initialHandle.id] as TLArrowTerminal | undefined
const bindingAfter = (next.props as any)[initialHandle.id] as TLArrowShapeTerminal | undefined
if (bindingAfter?.type === 'binding') {
if (hintingIds[0] !== bindingAfter.boundShapeId) {

View file

@ -1,6 +1,7 @@
import { TLStyleType } from '@tldraw/tlschema'
import { atom, Atom, computed, Computed } from 'signia'
import { TLBaseShape } from '@tldraw/tlschema'
import { Atom, Computed, atom, computed } from 'signia'
import type { Editor } from '../Editor'
import { TLShapeUtilConstructor } from '../shapes/ShapeUtil'
import {
EVENT_NAME_MAP,
TLEnterEventHandler,
@ -18,7 +19,6 @@ export interface TLStateNodeConstructor {
id: string
initial?: string
children?: () => TLStateNodeConstructor[]
styles?: TLStyleType[]
}
/** @public */
@ -69,8 +69,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
id: string
current: Atom<StateNode | undefined>
type: TLStateNodeType
readonly styles: TLStyleType[] = []
shapeType?: string
shapeType?: TLShapeUtilConstructor<TLBaseShape<any, any>>
initial?: string
children?: Record<string, StateNode>
parent: StateNode

View file

@ -1,5 +1,4 @@
import { PageRecordType, TLShape, createShapeId } from '@tldraw/tlschema'
import { structuredClone } from '@tldraw/utils'
import { BaseBoxShapeUtil } from '../editor/shapes/BaseBoxShapeUtil'
import { GeoShapeUtil } from '../editor/shapes/geo/GeoShapeUtil'
import { TestEditor } from './TestEditor'
@ -150,26 +149,17 @@ it('Does not create an undo stack item when first clicking on an empty canvas',
expect(editor.canUndo).toBe(false)
})
describe('Editor.setProp', () => {
it('Does not set non-style props on propsForNextShape', () => {
const initialPropsForNextShape = structuredClone(editor.instanceState.propsForNextShape)
editor.setProp('w', 100)
editor.setProp('url', 'https://example.com')
expect(editor.instanceState.propsForNextShape).toStrictEqual(initialPropsForNextShape)
})
})
describe('Editor.opacity', () => {
describe('Editor.sharedOpacity', () => {
it('should return the current opacity', () => {
expect(editor.opacity).toBe(1)
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 1 })
editor.setOpacity(0.5)
expect(editor.opacity).toBe(0.5)
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.5 })
})
it('should return opacity for a single selected shape', () => {
const { A } = editor.createShapesFromJsx(<TL.geo ref="A" opacity={0.3} x={0} y={0} />)
editor.setSelectedIds([A])
expect(editor.opacity).toBe(0.3)
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
})
it('should return opacity for multiple selected shapes', () => {
@ -178,16 +168,16 @@ describe('Editor.opacity', () => {
<TL.geo ref="B" opacity={0.3} x={0} y={0} />,
])
editor.setSelectedIds([A, B])
expect(editor.opacity).toBe(0.3)
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
})
it('should return null when multiple selected shapes have different opacity', () => {
it('should return mixed when multiple selected shapes have different opacity', () => {
const { A, B } = editor.createShapesFromJsx([
<TL.geo ref="A" opacity={0.3} x={0} y={0} />,
<TL.geo ref="B" opacity={0.5} x={0} y={0} />,
])
editor.setSelectedIds([A, B])
expect(editor.opacity).toBe(null)
expect(editor.sharedOpacity).toStrictEqual({ type: 'mixed' })
})
it('ignores the opacity of groups and returns the opacity of their children', () => {
@ -197,7 +187,7 @@ describe('Editor.opacity', () => {
</TL.group>,
])
editor.setSelectedIds([ids.group])
expect(editor.opacity).toBe(0.3)
expect(editor.sharedOpacity).toStrictEqual({ type: 'shared', value: 0.3 })
})
})

View file

@ -9,7 +9,6 @@ import {
} from '@tldraw/primitives'
import {
Box2dModel,
InstanceRecordType,
PageRecordType,
TLShapeId,
TLShapePartial,
@ -52,7 +51,6 @@ declare global {
}
}
}
export const TEST_INSTANCE_ID = InstanceRecordType.createId('testInstance1')
export class TestEditor extends Editor {
constructor(options: Partial<Omit<TLEditorOptions, 'store'>> = {}) {

View file

@ -307,10 +307,12 @@ describe('Custom shapes', () => {
}
}
const CardShape = defineShape('card', { util: CardUtil })
class CardTool extends BaseBoxShapeTool {
static override id = 'card'
static override initial = 'idle'
override shapeType = 'card'
override shapeType = CardUtil
}
const tools = [CardTool]

View file

@ -1,4 +1,4 @@
import { TLArrowShape, TLGeoShape, createShapeId } from '@tldraw/tlschema'
import { DefaultColorStyle, TLArrowShape, TLGeoShape, createShapeId } from '@tldraw/tlschema'
import { TestEditor } from '../TestEditor'
let editor: TestEditor
@ -88,12 +88,12 @@ it('Parents shapes to the current page if the parent is not found', () => {
})
it('Creates shapes with the current style', () => {
expect(editor.instanceState.propsForNextShape!.color).toBe('black')
expect(editor.instanceState.stylesForNextShape[DefaultColorStyle.id]).toBe(undefined)
editor.createShapes([{ id: ids.box1, type: 'geo' }])
expect(editor.getShapeById<TLGeoShape>(ids.box1)!.props.color).toEqual('black')
editor.setProp('color', 'red')
expect(editor.instanceState.propsForNextShape!.color).toBe('red')
editor.setStyle(DefaultColorStyle, 'red')
expect(editor.instanceState.stylesForNextShape[DefaultColorStyle.id]).toBe('red')
editor.createShapes([{ id: ids.box2, type: 'geo' }])
expect(editor.getShapeById<TLGeoShape>(ids.box2)!.props.color).toEqual('red')
})

View file

@ -1,4 +1,4 @@
import { createShapeId } from '@tldraw/tlschema'
import { DefaultDashStyle, createShapeId } from '@tldraw/tlschema'
import { SVG_PADDING } from '../../constants'
import { TestEditor } from '../TestEditor'
@ -12,7 +12,7 @@ const ids = {
beforeEach(() => {
editor = new TestEditor()
editor.setProp('dash', 'solid')
editor.setStyle(DefaultDashStyle, 'solid')
editor.createShapes([
{
id: ids.boxA,

View file

@ -1,4 +1,4 @@
import { PageRecordType, TLShape, createShapeId } from '@tldraw/tlschema'
import { DefaultFillStyle, PageRecordType, TLShape, createShapeId } from '@tldraw/tlschema'
import { TestEditor } from '../TestEditor'
let editor: TestEditor
@ -154,7 +154,7 @@ describe('arrows', () => {
.pointerDown(200, 200)
.pointerMove(300, 300)
.pointerUp(300, 300)
.setProp('fill', 'solid')
.setStyle(DefaultFillStyle, 'solid')
firstBox = editor.onlySelectedShape!
// draw a second box
@ -163,7 +163,7 @@ describe('arrows', () => {
.pointerDown(400, 400)
.pointerMove(500, 500)
.pointerUp(500, 500)
.setProp('fill', 'solid')
.setStyle(DefaultFillStyle, 'solid')
secondBox = editor.onlySelectedShape!
// draw an arrow from the first box to the second box

View file

@ -1,114 +0,0 @@
import { createDefaultShapes, defaultShapesIds, TestEditor } from './TestEditor'
let editor: TestEditor
beforeEach(() => {
editor = new TestEditor()
editor.createShapes(createDefaultShapes())
editor.reparentShapesById([defaultShapesIds.ellipse1], editor.currentPageId)
})
describe('Editor.props', () => {
it('should return props', () => {
editor.selectNone()
expect(editor.props).toEqual({
color: 'black',
dash: 'draw',
fill: 'none',
size: 'm',
})
})
it('should return props for a single shape', () => {
editor.select(defaultShapesIds.box1)
expect(editor.props).toEqual({
align: 'middle',
labelColor: 'black',
color: 'black',
dash: 'draw',
fill: 'none',
size: 'm',
font: 'draw',
geo: 'rectangle',
verticalAlign: 'middle',
})
})
it('should return props for two matching shapes', () => {
editor.select(defaultShapesIds.box1, defaultShapesIds.box2)
expect(editor.props).toEqual({
align: 'middle',
color: 'black',
labelColor: 'black',
dash: 'draw',
fill: 'none',
size: 'm',
font: 'draw',
geo: 'rectangle',
verticalAlign: 'middle',
})
})
it('should return mixed props for shapes that have mixed values', () => {
editor.updateShapes([
{
id: defaultShapesIds.box1,
type: 'geo',
props: { h: 200, w: 200, color: 'red', dash: 'solid' },
},
])
editor.select(defaultShapesIds.box1, defaultShapesIds.box2)
expect(editor.props).toEqual({
align: 'middle',
labelColor: 'black',
color: null, // mixed!
dash: null, // mixed!
fill: 'none',
size: 'm',
font: 'draw',
geo: 'rectangle',
verticalAlign: 'middle',
})
})
it('should return null for all mixed props', () => {
editor.updateShapes([
{
id: defaultShapesIds.box1,
type: 'geo',
props: { h: 200, w: 200, color: 'red', dash: 'solid' },
},
{
id: defaultShapesIds.box2,
type: 'geo',
props: { size: 'l', fill: 'pattern', font: 'mono' },
},
{
id: defaultShapesIds.ellipse1,
type: 'geo',
props: {
align: 'start',
text: 'hello world this is a long sentence that should wrap',
w: 100,
url: 'https://aol.com',
verticalAlign: 'start',
},
},
])
editor.selectAll()
expect(editor.props).toEqual({
align: null,
labelColor: 'black',
color: null,
dash: null,
fill: null,
geo: null,
size: null,
font: null,
verticalAlign: null,
})
})
})

View file

@ -0,0 +1,189 @@
import { DefaultColorStyle, TLGeoShape, TLGroupShape } from '@tldraw/tlschema'
import { ReadonlySharedStyleMap, SharedStyle } from '../utils/SharedStylesMap'
import { TestEditor, createDefaultShapes, defaultShapesIds } from './TestEditor'
import { TL } from './jsx'
let editor: TestEditor
function asPlainObject(styles: ReadonlySharedStyleMap | null) {
if (!styles) return null
const object: Record<string, SharedStyle<unknown>> = {}
for (const [key, value] of styles) {
object[key.id] = value
}
return object
}
beforeEach(() => {
editor = new TestEditor()
editor.createShapes(createDefaultShapes())
editor.reparentShapesById([defaultShapesIds.ellipse1], editor.currentPageId)
})
describe('Editor.styles', () => {
it('should return empty if nothing is selected', () => {
editor.selectNone()
expect(asPlainObject(editor.sharedStyles)).toStrictEqual({})
})
it('should return styles for a single shape', () => {
editor.select(defaultShapesIds.box1)
expect(asPlainObject(editor.sharedStyles)).toStrictEqual({
'tldraw:horizontalAlign': { type: 'shared', value: 'middle' },
'tldraw:labelColor': { type: 'shared', value: 'black' },
'tldraw:color': { type: 'shared', value: 'black' },
'tldraw:dash': { type: 'shared', value: 'draw' },
'tldraw:fill': { type: 'shared', value: 'none' },
'tldraw:size': { type: 'shared', value: 'm' },
'tldraw:font': { type: 'shared', value: 'draw' },
'tldraw:geo': { type: 'shared', value: 'rectangle' },
'tldraw:verticalAlign': { type: 'shared', value: 'middle' },
})
})
it('should return styles for two matching shapes', () => {
editor.select(defaultShapesIds.box1, defaultShapesIds.box2)
expect(asPlainObject(editor.sharedStyles)).toStrictEqual({
'tldraw:horizontalAlign': { type: 'shared', value: 'middle' },
'tldraw:labelColor': { type: 'shared', value: 'black' },
'tldraw:color': { type: 'shared', value: 'black' },
'tldraw:dash': { type: 'shared', value: 'draw' },
'tldraw:fill': { type: 'shared', value: 'none' },
'tldraw:size': { type: 'shared', value: 'm' },
'tldraw:font': { type: 'shared', value: 'draw' },
'tldraw:geo': { type: 'shared', value: 'rectangle' },
'tldraw:verticalAlign': { type: 'shared', value: 'middle' },
})
})
it('should return mixed styles for shapes that have mixed values', () => {
editor.updateShapes([
{
id: defaultShapesIds.box1,
type: 'geo',
props: { h: 200, w: 200, color: 'red', dash: 'solid' },
},
])
editor.select(defaultShapesIds.box1, defaultShapesIds.box2)
expect(asPlainObject(editor.sharedStyles)).toStrictEqual({
'tldraw:horizontalAlign': { type: 'shared', value: 'middle' },
'tldraw:labelColor': { type: 'shared', value: 'black' },
'tldraw:color': { type: 'mixed' },
'tldraw:dash': { type: 'mixed' },
'tldraw:fill': { type: 'shared', value: 'none' },
'tldraw:size': { type: 'shared', value: 'm' },
'tldraw:font': { type: 'shared', value: 'draw' },
'tldraw:geo': { type: 'shared', value: 'rectangle' },
'tldraw:verticalAlign': { type: 'shared', value: 'middle' },
})
})
it('should return mixed for all mixed styles', () => {
editor.updateShapes([
{
id: defaultShapesIds.box1,
type: 'geo',
props: { h: 200, w: 200, color: 'red', dash: 'solid' },
},
{
id: defaultShapesIds.box2,
type: 'geo',
props: { size: 'l', fill: 'pattern', font: 'mono' },
},
{
id: defaultShapesIds.ellipse1,
type: 'geo',
props: {
align: 'start',
text: 'hello world this is a long sentence that should wrap',
w: 100,
url: 'https://aol.com',
verticalAlign: 'start',
},
},
])
editor.selectAll()
expect(asPlainObject(editor.sharedStyles)).toStrictEqual({
'tldraw:color': { type: 'mixed' },
'tldraw:dash': { type: 'mixed' },
'tldraw:fill': { type: 'mixed' },
'tldraw:font': { type: 'mixed' },
'tldraw:geo': { type: 'mixed' },
'tldraw:horizontalAlign': { type: 'mixed' },
'tldraw:labelColor': { type: 'shared', value: 'black' },
'tldraw:size': { type: 'mixed' },
'tldraw:verticalAlign': { type: 'mixed' },
})
})
it('should return the same styles object if nothing relevant changes', () => {
editor.select(defaultShapesIds.box1, defaultShapesIds.box2)
const initialStyles = editor.sharedStyles
// update position of one of the shapes - not a style prop, so maps to same styles
editor.updateShapes([
{
id: defaultShapesIds.box1,
type: 'geo',
x: 1000,
y: 1000,
},
])
expect(editor.sharedStyles).toBe(initialStyles)
})
})
describe('Editor.setStyle', () => {
it('should set style for selected shapes', () => {
const ids = editor.createShapesFromJsx([
<TL.geo ref="A" x={0} y={0} color="blue" />,
<TL.geo ref="B" x={0} y={0} color="green" />,
])
editor.setSelectedIds([ids.A, ids.B])
editor.setStyle(DefaultColorStyle, 'red')
expect(editor.getShapeById<TLGeoShape>(ids.A)!.props.color).toBe('red')
expect(editor.getShapeById<TLGeoShape>(ids.B)!.props.color).toBe('red')
})
it('should traverse into groups and set styles in their children', () => {
const ids = editor.createShapesFromJsx([
<TL.geo ref="boxA" x={0} y={0} />,
<TL.group ref="groupA" x={0} y={0}>
<TL.geo ref="boxB" x={0} y={0} />
<TL.group ref="groupB" x={0} y={0}>
<TL.geo ref="boxC" x={0} y={0} />
<TL.geo ref="boxD" x={0} y={0} />
</TL.group>
</TL.group>,
])
editor.setSelectedIds([ids.groupA])
editor.setStyle(DefaultColorStyle, 'red')
// a wasn't selected...
expect(editor.getShapeById<TLGeoShape>(ids.boxA)!.props.color).toBe('black')
// b, c, & d were within a selected group...
expect(editor.getShapeById<TLGeoShape>(ids.boxB)!.props.color).toBe('red')
expect(editor.getShapeById<TLGeoShape>(ids.boxC)!.props.color).toBe('red')
expect(editor.getShapeById<TLGeoShape>(ids.boxD)!.props.color).toBe('red')
// groups get skipped
expect(editor.getShapeById<TLGroupShape>(ids.groupA)!.props).not.toHaveProperty('color')
expect(editor.getShapeById<TLGroupShape>(ids.groupB)!.props).not.toHaveProperty('color')
})
it('stores styles on stylesForNextShape', () => {
editor.setStyle(DefaultColorStyle, 'red')
expect(editor.instanceState.stylesForNextShape[DefaultColorStyle.id]).toBe('red')
editor.setStyle(DefaultColorStyle, 'green')
expect(editor.instanceState.stylesForNextShape[DefaultColorStyle.id]).toBe('green')
})
})

View file

@ -1,4 +1,4 @@
import { TLArrowShape, createShapeId } from '@tldraw/tlschema'
import { DefaultFillStyle, TLArrowShape, createShapeId } from '@tldraw/tlschema'
import { FrameShapeUtil } from '../../editor/shapes/frame/FrameShapeUtil'
import { TestEditor } from '../TestEditor'
@ -495,7 +495,11 @@ describe('frame shapes', () => {
const frameId = editor.onlySelectedShape!.id
editor.setSelectedTool('geo')
editor.pointerDown(125, 125).pointerMove(175, 175).pointerUp(175, 175).setProp('fill', 'solid')
editor
.pointerDown(125, 125)
.pointerMove(175, 175)
.pointerUp(175, 175)
.setStyle(DefaultFillStyle, 'solid')
const boxId = editor.onlySelectedShape!.id
editor.setSelectedTool('arrow')
@ -602,7 +606,11 @@ describe('frame shapes', () => {
// make a shape inside the frame that extends out of the frame
editor.setSelectedTool('geo')
editor.pointerDown(150, 150).pointerMove(400, 400).pointerUp(400, 400).setProp('fill', 'solid')
editor
.pointerDown(150, 150)
.pointerMove(400, 400)
.pointerUp(400, 400)
.setStyle(DefaultFillStyle, 'solid')
const innerBoxId = editor.onlySelectedShape!.id
// Make an arrow that binds to the inner box's bottom right corner
@ -651,15 +659,27 @@ test('arrows bound to a shape within a group within a frame are reparented if th
const frameId = editor.onlySelectedShape!.id
editor.setSelectedTool('geo')
editor.pointerDown(110, 110).pointerMove(120, 120).pointerUp(120, 120).setProp('fill', 'solid')
editor
.pointerDown(110, 110)
.pointerMove(120, 120)
.pointerUp(120, 120)
.setStyle(DefaultFillStyle, 'solid')
const boxAId = editor.onlySelectedShape!.id
editor.setSelectedTool('geo')
editor.pointerDown(180, 110).pointerMove(190, 120).pointerUp(190, 120).setProp('fill', 'solid')
editor
.pointerDown(180, 110)
.pointerMove(190, 120)
.pointerUp(190, 120)
.setStyle(DefaultFillStyle, 'solid')
const boxBId = editor.onlySelectedShape!.id
editor.setSelectedTool('geo')
editor.pointerDown(160, 160).pointerMove(170, 170).pointerUp(170, 170).setProp('fill', 'solid')
editor
.pointerDown(160, 160)
.pointerMove(170, 170)
.pointerUp(170, 170)
.setStyle(DefaultFillStyle, 'solid')
const boxCId = editor.onlySelectedShape!.id
editor.setSelectedTool('select')

View file

@ -0,0 +1,105 @@
import { StyleProp } from '@tldraw/tlschema'
import { exhaustiveSwitchError } from '@tldraw/utils'
/** @public */
export type SharedStyle<T> =
| { readonly type: 'mixed' }
| { readonly type: 'shared'; readonly value: T }
function sharedStyleEquals<T>(a: SharedStyle<T>, b: SharedStyle<T> | undefined) {
if (!b) return false
switch (a.type) {
case 'mixed':
return b.type === 'mixed'
case 'shared':
return b.type === 'shared' && a.value === b.value
default:
throw exhaustiveSwitchError(a)
}
}
/** @public */
export class ReadonlySharedStyleMap {
protected map: Map<StyleProp<unknown>, SharedStyle<unknown>>
constructor(entries?: Iterable<[StyleProp<unknown>, SharedStyle<unknown>]>) {
this.map = new Map(entries)
}
get<T>(prop: StyleProp<T>): SharedStyle<T> | undefined {
return this.map.get(prop) as SharedStyle<T> | undefined
}
getAsKnownValue<T>(prop: StyleProp<T>): T | undefined {
const value = this.get(prop)
if (!value) return undefined
if (value.type === 'mixed') return undefined
return value.value
}
get size() {
return this.map.size
}
equals(other: ReadonlySharedStyleMap) {
if (this.size !== other.size) return false
const checkedKeys = new Set()
for (const [styleProp, value] of this) {
if (!sharedStyleEquals(value, other.get(styleProp))) return false
checkedKeys.add(styleProp)
}
for (const [styleProp, value] of other) {
if (checkedKeys.has(styleProp)) continue
if (!sharedStyleEquals(value, this.get(styleProp))) return false
}
return true
}
keys() {
return this.map.keys()
}
values() {
return this.map.values()
}
entries() {
return this.map.entries()
}
[Symbol.iterator]() {
return this.map[Symbol.iterator]()
}
}
/** @internal */
export class SharedStyleMap extends ReadonlySharedStyleMap {
set<T>(prop: StyleProp<T>, value: SharedStyle<T>) {
this.map.set(prop, value)
}
applyValue<T>(prop: StyleProp<T>, value: T) {
const existingValue = this.get(prop)
// if we don't have a value yet, set it
if (!existingValue) {
this.set(prop, { type: 'shared', value })
return
}
switch (existingValue.type) {
case 'mixed':
// we're already mixed, adding new values won't help
return
case 'shared':
if (existingValue.value !== value) {
// if the value is different, we're now mixed:
this.set(prop, { type: 'mixed' })
}
return
default:
exhaustiveSwitchError(existingValue, 'type')
}
}
}

View file

@ -1,8 +1,8 @@
import { Box2d } from '@tldraw/primitives'
import { Box2dModel, TLAlignType } from '@tldraw/tlschema'
import { Box2dModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
export function getLegacyOffsetX(
align: TLAlignType | string,
align: TLDefaultHorizontalAlignStyle | string,
padding: number,
spans: { text: string; box: Box2dModel }[],
totalWidth: number
@ -20,7 +20,7 @@ export function getLegacyOffsetX(
}
}
// sneaky TLAlignType for legacies
export function isLegacyAlign(align: TLAlignType | string): boolean {
// sneaky TLDefaultHorizontalAlignStyle for legacies
export function isLegacyAlign(align: TLDefaultHorizontalAlignStyle | string): boolean {
return align === 'start-legacy' || align === 'middle-legacy' || align === 'end-legacy'
}

View file

@ -1,21 +0,0 @@
import {
TLInstancePropsForNextShape,
TLShapeProps,
TLStyleType,
TL_STYLE_TYPES,
} from '@tldraw/tlschema'
/** @public */
export function setPropsForNextShape(
previousProps: TLInstancePropsForNextShape,
newProps: Partial<TLShapeProps>
): TLInstancePropsForNextShape {
let nextProps: null | TLInstancePropsForNextShape = null
for (const [prop, value] of Object.entries(newProps)) {
if (!TL_STYLE_TYPES.has(prop as TLStyleType)) continue
if (!nextProps) nextProps = { ...previousProps }
// @ts-expect-error typescript can't track `value` correctly
nextProps[prop] = value
}
return nextProps ?? previousProps
}

View file

@ -4,22 +4,22 @@ import {
Editor,
MAX_SHAPES_PER_PAGE,
PageRecordType,
TLAlignType,
TLArrowShape,
TLArrowTerminal,
TLArrowheadType,
TLArrowShapeArrowheadStyle,
TLArrowShapeTerminal,
TLAsset,
TLAssetId,
TLColorType,
TLDashType,
TLDefaultColorStyle,
TLDefaultDashStyle,
TLDefaultFontStyle,
TLDefaultHorizontalAlignStyle,
TLDefaultSizeStyle,
TLDrawShape,
TLFontType,
TLGeoShape,
TLImageShape,
TLNoteShape,
TLPageId,
TLShapeId,
TLSizeType,
TLTextShape,
TLVideoShape,
Vec2dModel,
@ -563,7 +563,7 @@ export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocume
if (change) {
if (change.props?.[handleId]) {
const terminal = change.props?.[handleId] as TLArrowTerminal
const terminal = change.props?.[handleId] as TLArrowShapeTerminal
if (terminal.type === 'binding') {
terminal.isExact = binding.distance === 0
@ -1076,7 +1076,7 @@ export interface LegacyTldrawDocument {
/* ------------------ Translations ------------------ */
const v1ColorsToV2Colors: Record<ColorStyle, TLColorType> = {
const v1ColorsToV2Colors: Record<ColorStyle, TLDefaultColorStyle> = {
[ColorStyle.White]: 'black',
[ColorStyle.Black]: 'black',
[ColorStyle.LightGray]: 'grey',
@ -1091,60 +1091,60 @@ const v1ColorsToV2Colors: Record<ColorStyle, TLColorType> = {
[ColorStyle.Violet]: 'light-violet',
}
const v1FontsToV2Fonts: Record<FontStyle, TLFontType> = {
const v1FontsToV2Fonts: Record<FontStyle, TLDefaultFontStyle> = {
[FontStyle.Mono]: 'mono',
[FontStyle.Sans]: 'sans',
[FontStyle.Script]: 'draw',
[FontStyle.Serif]: 'serif',
}
const v1AlignsToV2Aligns: Record<AlignStyle, TLAlignType> = {
const v1AlignsToV2Aligns: Record<AlignStyle, TLDefaultHorizontalAlignStyle> = {
[AlignStyle.Start]: 'start',
[AlignStyle.Middle]: 'middle',
[AlignStyle.End]: 'end',
[AlignStyle.Justify]: 'start',
}
const v1TextSizesToV2TextSizes: Record<SizeStyle, TLSizeType> = {
const v1TextSizesToV2TextSizes: Record<SizeStyle, TLDefaultSizeStyle> = {
[SizeStyle.Small]: 's',
[SizeStyle.Medium]: 'l',
[SizeStyle.Large]: 'xl',
}
const v1SizesToV2Sizes: Record<SizeStyle, TLSizeType> = {
const v1SizesToV2Sizes: Record<SizeStyle, TLDefaultSizeStyle> = {
[SizeStyle.Small]: 'm',
[SizeStyle.Medium]: 'l',
[SizeStyle.Large]: 'xl',
}
const v1DashesToV2Dashes: Record<DashStyle, TLDashType> = {
const v1DashesToV2Dashes: Record<DashStyle, TLDefaultDashStyle> = {
[DashStyle.Solid]: 'solid',
[DashStyle.Dashed]: 'dashed',
[DashStyle.Dotted]: 'dotted',
[DashStyle.Draw]: 'draw',
}
function getV2Color(color: ColorStyle | undefined): TLColorType {
function getV2Color(color: ColorStyle | undefined): TLDefaultColorStyle {
return color ? v1ColorsToV2Colors[color] ?? 'black' : 'black'
}
function getV2Font(font: FontStyle | undefined): TLFontType {
function getV2Font(font: FontStyle | undefined): TLDefaultFontStyle {
return font ? v1FontsToV2Fonts[font] ?? 'draw' : 'draw'
}
function getV2Align(align: AlignStyle | undefined): TLAlignType {
function getV2Align(align: AlignStyle | undefined): TLDefaultHorizontalAlignStyle {
return align ? v1AlignsToV2Aligns[align] ?? 'middle' : 'middle'
}
function getV2TextSize(size: SizeStyle | undefined): TLSizeType {
function getV2TextSize(size: SizeStyle | undefined): TLDefaultSizeStyle {
return size ? v1TextSizesToV2TextSizes[size] ?? 'm' : 'm'
}
function getV2Size(size: SizeStyle | undefined): TLSizeType {
function getV2Size(size: SizeStyle | undefined): TLDefaultSizeStyle {
return size ? v1SizesToV2Sizes[size] ?? 'l' : 'l'
}
function getV2Dash(dash: DashStyle | undefined): TLDashType {
function getV2Dash(dash: DashStyle | undefined): TLDefaultDashStyle {
return dash ? v1DashesToV2Dashes[dash] ?? 'draw' : 'draw'
}
@ -1156,7 +1156,7 @@ function getV2Point(point: number[]): Vec2dModel {
}
}
function getV2Arrowhead(decoration: Decoration | undefined): TLArrowheadType {
function getV2Arrowhead(decoration: Decoration | undefined): TLArrowShapeArrowheadStyle {
return decoration === Decoration.Arrow ? 'arrow' : 'none'
}

View file

@ -5,6 +5,7 @@
```ts
import { BaseRecord } from '@tldraw/store';
import { Expand } from '@tldraw/utils';
import { Migrations } from '@tldraw/store';
import { RecordId } from '@tldraw/store';
import { RecordType } from '@tldraw/store';
@ -15,14 +16,54 @@ import { StoreSnapshot } from '@tldraw/store';
import { T } from '@tldraw/validate';
import { UnknownRecord } from '@tldraw/store';
// @internal (undocumented)
export const alignValidator: T.Validator<"end" | "middle" | "start">;
// @public (undocumented)
export const ArrowShapeArrowheadEndStyle: EnumStyleProp<"arrow" | "bar" | "diamond" | "dot" | "inverted" | "none" | "pipe" | "square" | "triangle">;
// @public (undocumented)
export const ArrowShapeArrowheadStartStyle: EnumStyleProp<"arrow" | "bar" | "diamond" | "dot" | "inverted" | "none" | "pipe" | "square" | "triangle">;
// @internal (undocumented)
export const arrowShapeMigrations: Migrations;
// @internal (undocumented)
export const arrowShapeProps: ShapeProps<TLArrowShape>;
// @public (undocumented)
export const arrowShapeProps: {
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
arrowheadStart: EnumStyleProp<"arrow" | "bar" | "diamond" | "dot" | "inverted" | "none" | "pipe" | "square" | "triangle">;
arrowheadEnd: EnumStyleProp<"arrow" | "bar" | "diamond" | "dot" | "inverted" | "none" | "pipe" | "square" | "triangle">;
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
start: T.UnionValidator<"type", {
binding: T.ObjectValidator<{
type: "binding";
boundShapeId: TLShapeId;
normalizedAnchor: Vec2dModel;
isExact: boolean;
}>;
point: T.ObjectValidator<{
type: "point";
x: number;
y: number;
}>;
}, never>;
end: T.UnionValidator<"type", {
binding: T.ObjectValidator<{
type: "binding";
boundShapeId: TLShapeId;
normalizedAnchor: Vec2dModel;
isExact: boolean;
}>;
point: T.ObjectValidator<{
type: "point";
x: number;
y: number;
}>;
}, never>;
bend: T.Validator<number>;
text: T.Validator<string>;
};
// @public
export const assetIdValidator: T.Validator<TLAssetId>;
@ -39,8 +80,13 @@ export const assetValidator: T.Validator<TLAsset>;
// @internal (undocumented)
export const bookmarkShapeMigrations: Migrations;
// @internal (undocumented)
export const bookmarkShapeProps: ShapeProps<TLBookmarkShape>;
// @public (undocumented)
export const bookmarkShapeProps: {
w: T.Validator<number>;
h: T.Validator<number>;
assetId: T.Validator<TLAssetId | null>;
url: T.Validator<string>;
};
// @public
export interface Box2dModel {
@ -54,6 +100,9 @@ export interface Box2dModel {
y: number;
}
// @public (undocumented)
export const box2dModelValidator: T.Validator<Box2dModel>;
// @public (undocumented)
export const CameraRecordType: RecordType<TLCamera, never>;
@ -63,9 +112,6 @@ export const canvasUiColorTypeValidator: T.Validator<"accent" | "black" | "laser
// @internal (undocumented)
export function CLIENT_FIXUP_SCRIPT(persistedStore: StoreSnapshot<TLRecord>): StoreSnapshot<TLRecord>;
// @internal (undocumented)
export const colorValidator: T.Validator<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
// @public
export function createAssetValidator<Type extends string, Props extends object>(type: Type, props: T.Validator<Props>): T.ObjectValidator<{
id: TLAssetId;
@ -106,8 +152,26 @@ export function createTLSchema({ shapes }: {
shapes: Record<string, SchemaShapeInfo>;
}): TLSchema;
// @internal (undocumented)
export const dashValidator: T.Validator<"dashed" | "dotted" | "draw" | "solid">;
// @public (undocumented)
export const DefaultColorStyle: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
// @public (undocumented)
export const DefaultDashStyle: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
// @public (undocumented)
export const DefaultFillStyle: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
// @public (undocumented)
export const DefaultFontStyle: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
// @public (undocumented)
export const DefaultHorizontalAlignStyle: EnumStyleProp<"end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start">;
// @public (undocumented)
export const DefaultSizeStyle: EnumStyleProp<"l" | "m" | "s" | "xl">;
// @public (undocumented)
export const DefaultVerticalAlignStyle: EnumStyleProp<"end" | "middle" | "start">;
// @public (undocumented)
export const DocumentRecordType: RecordType<TLDocument, never>;
@ -115,8 +179,20 @@ export const DocumentRecordType: RecordType<TLDocument, never>;
// @internal (undocumented)
export const drawShapeMigrations: Migrations;
// @internal (undocumented)
export const drawShapeProps: ShapeProps<TLDrawShape>;
// @public (undocumented)
export const drawShapeProps: {
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
segments: T.ArrayOfValidator<{
type: "free" | "straight";
points: Vec2dModel[];
}>;
isComplete: T.Validator<boolean>;
isClosed: T.Validator<boolean>;
isPen: T.Validator<boolean>;
};
// @public (undocumented)
export const EMBED_DEFINITIONS: readonly [{
@ -343,11 +419,20 @@ export const embedShapePermissionDefaults: {
readonly 'allow-forms': true;
};
// @internal (undocumented)
export const embedShapeProps: ShapeProps<TLEmbedShape>;
// @public (undocumented)
export const embedShapeProps: {
w: T.Validator<number>;
h: T.Validator<number>;
url: T.Validator<string>;
};
// @internal (undocumented)
export const fillValidator: T.Validator<"none" | "pattern" | "semi" | "solid">;
// @public (undocumented)
export class EnumStyleProp<T> extends StyleProp<T> {
// @internal
constructor(id: string, defaultValue: T, values: readonly T[]);
// (undocumented)
readonly values: readonly T[];
}
// @internal (undocumented)
export function fixupRecord(oldRecord: TLRecord): {
@ -355,27 +440,46 @@ export function fixupRecord(oldRecord: TLRecord): {
issues: string[];
};
// @internal (undocumented)
export const fontValidator: T.Validator<"draw" | "mono" | "sans" | "serif">;
// @internal (undocumented)
export const frameShapeMigrations: Migrations;
// @internal (undocumented)
export const frameShapeProps: ShapeProps<TLFrameShape>;
// @public (undocumented)
export const frameShapeProps: {
w: T.Validator<number>;
h: T.Validator<number>;
name: T.Validator<string>;
};
// @public (undocumented)
export const GeoShapeGeoStyle: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
// @internal (undocumented)
export const geoShapeMigrations: Migrations;
// @internal (undocumented)
export const geoShapeProps: ShapeProps<TLGeoShape>;
// @internal (undocumented)
export const geoValidator: T.Validator<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
// @public (undocumented)
export const geoShapeProps: {
geo: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
align: EnumStyleProp<"end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start">;
verticalAlign: EnumStyleProp<"end" | "middle" | "start">;
url: T.Validator<string>;
w: T.Validator<number>;
h: T.Validator<number>;
growY: T.Validator<number>;
text: T.Validator<string>;
};
// @public (undocumented)
export function getDefaultTranslationLocale(): TLLanguage['locale'];
// @internal (undocumented)
export function getShapePropKeysByStyle(props: Record<string, T.Validatable<any>>): Map<StyleProp<unknown>, string>;
// @internal (undocumented)
export const groupShapeMigrations: Migrations;
@ -385,17 +489,17 @@ export const groupShapeProps: ShapeProps<TLGroupShape>;
// @internal (undocumented)
export const highlightShapeMigrations: Migrations;
// @internal (undocumented)
export const highlightShapeProps: ShapeProps<TLHighlightShape>;
// @internal (undocumented)
export const iconShapeMigrations: Migrations;
// @internal (undocumented)
export const iconShapeProps: ShapeProps<TLIconShape>;
// @internal (undocumented)
export const iconValidator: T.Validator<"activity" | "airplay" | "alert-circle" | "alert-octagon" | "alert-triangle" | "align-center" | "align-justify" | "align-left" | "align-right" | "anchor" | "aperture" | "archive" | "arrow-down-circle" | "arrow-down-left" | "arrow-down-right" | "arrow-down" | "arrow-left-circle" | "arrow-left" | "arrow-right-circle" | "arrow-right" | "arrow-up-circle" | "arrow-up-left" | "arrow-up-right" | "arrow-up" | "at-sign" | "award" | "bar-chart-2" | "bar-chart" | "battery-charging" | "battery" | "bell-off" | "bell" | "bluetooth" | "bold" | "book-open" | "book" | "bookmark" | "briefcase" | "calendar" | "camera-off" | "camera" | "cast" | "check-circle" | "check-square" | "check" | "chevron-down" | "chevron-left" | "chevron-right" | "chevron-up" | "chevrons-down" | "chevrons-left" | "chevrons-right" | "chevrons-up" | "chrome" | "circle" | "clipboard" | "clock" | "cloud-drizzle" | "cloud-lightning" | "cloud-off" | "cloud-rain" | "cloud-snow" | "cloud" | "codepen" | "codesandbox" | "coffee" | "columns" | "command" | "compass" | "copy" | "corner-down-left" | "corner-down-right" | "corner-left-down" | "corner-left-up" | "corner-right-down" | "corner-right-up" | "corner-up-left" | "corner-up-right" | "cpu" | "credit-card" | "crop" | "crosshair" | "database" | "delete" | "disc" | "divide-circle" | "divide-square" | "divide" | "dollar-sign" | "download-cloud" | "download" | "dribbble" | "droplet" | "edit-2" | "edit-3" | "edit" | "external-link" | "eye-off" | "eye" | "facebook" | "fast-forward" | "feather" | "figma" | "file-minus" | "file-plus" | "file-text" | "file" | "film" | "filter" | "flag" | "folder-minus" | "folder-plus" | "folder" | "framer" | "frown" | "geo" | "gift" | "git-branch" | "git-commit" | "git-merge" | "git-pull-request" | "github" | "gitlab" | "globe" | "grid" | "hard-drive" | "hash" | "headphones" | "heart" | "help-circle" | "hexagon" | "home" | "image" | "inbox" | "info" | "instagram" | "italic" | "key" | "layers" | "layout" | "life-buoy" | "link-2" | "link" | "linkedin" | "list" | "loader" | "lock" | "log-in" | "log-out" | "mail" | "map-pin" | "map" | "maximize-2" | "maximize" | "meh" | "menu" | "message-circle" | "message-square" | "mic-off" | "mic" | "minimize-2" | "minimize" | "minus-circle" | "minus-square" | "minus" | "monitor" | "moon" | "more-horizontal" | "more-vertical" | "mouse-pointer" | "move" | "music" | "navigation-2" | "navigation" | "octagon" | "package" | "paperclip" | "pause-circle" | "pause" | "pen-tool" | "percent" | "phone-call" | "phone-forwarded" | "phone-incoming" | "phone-missed" | "phone-off" | "phone-outgoing" | "phone" | "pie-chart" | "play-circle" | "play" | "plus-circle" | "plus-square" | "plus" | "pocket" | "power" | "printer" | "radio" | "refresh-ccw" | "refresh-cw" | "repeat" | "rewind" | "rotate-ccw" | "rotate-cw" | "rss" | "save" | "scissors" | "search" | "send" | "server" | "settings" | "share-2" | "share" | "shield-off" | "shield" | "shopping-bag" | "shopping-cart" | "shuffle" | "sidebar" | "skip-back" | "skip-forward" | "slack" | "slash" | "sliders" | "smartphone" | "smile" | "speaker" | "square" | "star" | "stop-circle" | "sun" | "sunrise" | "sunset" | "table" | "tablet" | "tag" | "target" | "terminal" | "thermometer" | "thumbs-down" | "thumbs-up" | "toggle-left" | "toggle-right" | "tool" | "trash-2" | "trash" | "trello" | "trending-down" | "trending-up" | "triangle" | "truck" | "tv" | "twitch" | "twitter" | "type" | "umbrella" | "underline" | "unlock" | "upload-cloud" | "upload" | "user-check" | "user-minus" | "user-plus" | "user-x" | "user" | "users" | "video-off" | "video" | "voicemail" | "volume-1" | "volume-2" | "volume-x" | "volume" | "watch" | "wifi-off" | "wifi" | "wind" | "x-circle" | "x-octagon" | "x-square" | "x" | "youtube" | "zap-off" | "zap" | "zoom-in" | "zoom-out">;
// @public (undocumented)
export const highlightShapeProps: {
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
segments: T.ArrayOfValidator<{
type: "free" | "straight";
points: Vec2dModel[];
}>;
isComplete: T.Validator<boolean>;
isPen: T.Validator<boolean>;
};
// @internal (undocumented)
export function idValidator<Id extends RecordId<UnknownRecord>>(prefix: Id['__type__']['typeName']): T.Validator<Id>;
@ -403,8 +507,18 @@ export function idValidator<Id extends RecordId<UnknownRecord>>(prefix: Id['__ty
// @internal (undocumented)
export const imageShapeMigrations: Migrations;
// @internal (undocumented)
export const imageShapeProps: ShapeProps<TLImageShape>;
// @public (undocumented)
export const imageShapeProps: {
w: T.Validator<number>;
h: T.Validator<number>;
playing: T.Validator<boolean>;
url: T.Validator<string>;
assetId: T.Validator<TLAssetId | null>;
crop: T.Validator<{
topLeft: Vec2dModel;
bottomRight: Vec2dModel;
} | null>;
};
// @public (undocumented)
export const InstancePageStateRecordType: RecordType<TLInstancePageState, "pageId">;
@ -412,12 +526,6 @@ export const InstancePageStateRecordType: RecordType<TLInstancePageState, "pageI
// @public (undocumented)
export const InstancePresenceRecordType: RecordType<TLInstancePresence, "currentPageId" | "userId" | "userName">;
// @public (undocumented)
export const InstanceRecordType: RecordType<TLInstance, "currentPageId">;
// @internal (undocumented)
export const instanceTypeValidator: T.Validator<TLInstance>;
// @public (undocumented)
export function isPageId(id: string): id is TLPageId;
@ -532,14 +640,32 @@ export const LANGUAGES: readonly [{
// @internal (undocumented)
export const lineShapeMigrations: Migrations;
// @internal (undocumented)
export const lineShapeProps: ShapeProps<TLLineShape>;
// @public (undocumented)
export const lineShapeProps: {
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
spline: EnumStyleProp<"cubic" | "line">;
handles: T.DictValidator<string, TLHandle>;
};
// @public (undocumented)
export const LineShapeSplineStyle: EnumStyleProp<"cubic" | "line">;
// @internal (undocumented)
export const noteShapeMigrations: Migrations;
// @internal (undocumented)
export const noteShapeProps: ShapeProps<TLNoteShape>;
// @public (undocumented)
export const noteShapeProps: {
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
align: EnumStyleProp<"end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start">;
verticalAlign: EnumStyleProp<"end" | "middle" | "start">;
growY: T.Validator<number>;
url: T.Validator<string>;
text: T.Validator<string>;
};
// @internal (undocumented)
export const opacityValidator: T.Validator<number>;
@ -575,117 +701,61 @@ export const shapeIdValidator: T.Validator<TLShapeId>;
// @public (undocumented)
export type ShapeProps<Shape extends TLBaseShape<any, any>> = {
[K in keyof Shape['props']]: T.Validator<Shape['props'][K]>;
[K in keyof Shape['props']]: T.Validatable<Shape['props'][K]>;
};
// @internal (undocumented)
export const sizeValidator: T.Validator<"l" | "m" | "s" | "xl">;
// @internal (undocumented)
export const splineValidator: T.Validator<"cubic" | "line">;
// @public (undocumented)
export class StyleProp<Type> implements T.Validatable<Type> {
protected constructor(id: string, defaultValue: Type, type: T.Validatable<Type>);
// (undocumented)
readonly defaultValue: Type;
// (undocumented)
static define<Type>(uniqueId: string, { defaultValue, type }: {
defaultValue: Type;
type?: T.Validatable<Type>;
}): StyleProp<Type>;
// (undocumented)
static defineEnum<const Values extends readonly unknown[]>(uniqueId: string, { defaultValue, values }: {
defaultValue: Values[number];
values: Values;
}): EnumStyleProp<Values[number]>;
// (undocumented)
readonly id: string;
// (undocumented)
readonly type: T.Validatable<Type>;
// (undocumented)
validate(value: unknown): Type;
}
// @internal (undocumented)
export const textShapeMigrations: Migrations;
// @internal (undocumented)
export const textShapeProps: ShapeProps<TLTextShape>;
// @public (undocumented)
export const TL_ALIGN_TYPES: Set<"end" | "middle" | "start">;
// @public (undocumented)
export const TL_ARROWHEAD_TYPES: Set<"arrow" | "bar" | "diamond" | "dot" | "inverted" | "none" | "pipe" | "square" | "triangle">;
export const textShapeProps: {
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
align: EnumStyleProp<"end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start">;
w: T.Validator<number>;
text: T.Validator<string>;
scale: T.Validator<number>;
autoSize: T.Validator<boolean>;
};
// @public
export const TL_CANVAS_UI_COLOR_TYPES: Set<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
// @public (undocumented)
export const TL_COLOR_TYPES: Set<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow">;
// @public (undocumented)
export const TL_DASH_TYPES: Set<"dashed" | "dotted" | "draw" | "solid">;
// @public (undocumented)
export const TL_FILL_TYPES: Set<"none" | "pattern" | "semi" | "solid">;
// @public (undocumented)
export const TL_FONT_TYPES: Set<"draw" | "mono" | "sans" | "serif">;
// @public (undocumented)
export const TL_GEO_TYPES: Set<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "diamond" | "ellipse" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
// @public (undocumented)
export const TL_SIZE_TYPES: Set<"l" | "m" | "s" | "xl">;
// @public (undocumented)
export const TL_SPLINE_TYPES: Set<"cubic" | "line">;
// @public (undocumented)
export const TL_STYLE_TYPES: Set<"align" | "arrowheadEnd" | "arrowheadStart" | "color" | "dash" | "fill" | "font" | "geo" | "icon" | "labelColor" | "size" | "spline" | "verticalAlign">;
// @public (undocumented)
export interface TLAlignStyle extends TLBaseStyle {
// (undocumented)
id: TLAlignType;
// (undocumented)
type: 'align';
}
// @public (undocumented)
export type TLAlignType = SetValue<typeof TL_ALIGN_TYPES>;
// @public (undocumented)
export interface TLArrowheadEndStyle extends TLBaseStyle {
// (undocumented)
id: TLArrowheadType;
// (undocumented)
type: 'arrowheadEnd';
}
// @public (undocumented)
export interface TLArrowheadStartStyle extends TLBaseStyle {
// (undocumented)
id: TLArrowheadType;
// (undocumented)
type: 'arrowheadStart';
}
// @public (undocumented)
export type TLArrowheadType = SetValue<typeof TL_ARROWHEAD_TYPES>;
// @public (undocumented)
export type TLArrowShape = TLBaseShape<'arrow', TLArrowShapeProps>;
// @public (undocumented)
export type TLArrowShapeProps = {
labelColor: TLColorType;
color: TLColorType;
fill: TLFillType;
dash: TLDashType;
size: TLSizeType;
arrowheadStart: TLArrowheadType;
arrowheadEnd: TLArrowheadType;
font: TLFontType;
start: TLArrowTerminal;
end: TLArrowTerminal;
bend: number;
text: string;
};
export type TLArrowShapeArrowheadStyle = T.TypeOf<typeof ArrowShapeArrowheadStartStyle>;
// @public (undocumented)
export type TLArrowTerminal = {
type: 'binding';
boundShapeId: TLShapeId;
normalizedAnchor: Vec2dModel;
isExact: boolean;
} | {
type: 'point';
x: number;
y: number;
};
export type TLArrowShapeProps = ShapePropsType<typeof arrowShapeProps>;
// @public (undocumented)
export type TLArrowTerminalType = SetValue<typeof TL_ARROW_TERMINAL_TYPE>;
export type TLArrowShapeTerminal = T.TypeOf<typeof ArrowShapeTerminal>;
// @public (undocumented)
export type TLAsset = TLBookmarkAsset | TLImageAsset | TLVideoAsset;
@ -764,17 +834,6 @@ export type TLCameraId = RecordId<TLCamera>;
// @public
export type TLCanvasUiColor = SetValue<typeof TL_CANVAS_UI_COLOR_TYPES>;
// @public (undocumented)
export interface TLColorStyle extends TLBaseStyle {
// (undocumented)
id: TLColorType;
// (undocumented)
type: 'color';
}
// @public (undocumented)
export type TLColorType = SetValue<typeof TL_COLOR_TYPES>;
// @public
export interface TLCursor {
// (undocumented)
@ -789,18 +848,28 @@ export interface TLCursor {
export type TLCursorType = SetValue<typeof TL_CURSOR_TYPES>;
// @public (undocumented)
export interface TLDashStyle extends TLBaseStyle {
// (undocumented)
id: TLDashType;
// (undocumented)
type: 'dash';
}
export type TLDefaultColorStyle = T.TypeOf<typeof DefaultColorStyle>;
// @public (undocumented)
export type TLDashType = SetValue<typeof TL_DASH_TYPES>;
export type TLDefaultDashStyle = T.TypeOf<typeof DefaultDashStyle>;
// @public (undocumented)
export type TLDefaultFillStyle = T.TypeOf<typeof DefaultFillStyle>;
// @public (undocumented)
export type TLDefaultFontStyle = T.TypeOf<typeof DefaultFontStyle>;
// @public (undocumented)
export type TLDefaultHorizontalAlignStyle = T.TypeOf<typeof DefaultHorizontalAlignStyle>;
// @public
export type TLDefaultShape = TLArrowShape | TLBookmarkShape | TLDrawShape | TLEmbedShape | TLFrameShape | TLGeoShape | TLGroupShape | TLHighlightShape | TLIconShape | TLImageShape | TLLineShape | TLNoteShape | TLTextShape | TLVideoShape;
export type TLDefaultShape = TLArrowShape | TLBookmarkShape | TLDrawShape | TLEmbedShape | TLFrameShape | TLGeoShape | TLGroupShape | TLHighlightShape | TLImageShape | TLLineShape | TLNoteShape | TLTextShape | TLVideoShape;
// @public (undocumented)
export type TLDefaultSizeStyle = T.TypeOf<typeof DefaultSizeStyle>;
// @public (undocumented)
export type TLDefaultVerticalAlignStyle = T.TypeOf<typeof DefaultVerticalAlignStyle>;
// @public
export interface TLDocument extends BaseRecord<'document', RecordId<TLDocument>> {
@ -817,10 +886,7 @@ export const TLDOCUMENT_ID: RecordId<TLDocument>;
export type TLDrawShape = TLBaseShape<'draw', TLDrawShapeProps>;
// @public (undocumented)
export type TLDrawShapeSegment = {
type: SetValue<typeof TL_DRAW_SHAPE_SEGMENT_TYPE>;
points: Vec2dModel[];
};
export type TLDrawShapeSegment = T.TypeOf<typeof DrawShapeSegment>;
// @public (undocumented)
export type TLEmbedShape = TLBaseShape<'embed', TLEmbedShapeProps>;
@ -830,45 +896,12 @@ export type TLEmbedShapePermissions = {
[K in keyof typeof embedShapePermissionDefaults]?: boolean;
};
// @public (undocumented)
export interface TLFillStyle extends TLBaseStyle {
// (undocumented)
id: TLFillType;
// (undocumented)
type: 'fill';
}
// @public (undocumented)
export type TLFillType = SetValue<typeof TL_FILL_TYPES>;
// @public (undocumented)
export interface TLFontStyle extends TLBaseStyle {
// (undocumented)
id: TLFontType;
// (undocumented)
type: 'font';
}
// @public (undocumented)
export type TLFontType = SetValue<typeof TL_FONT_TYPES>;
// @public (undocumented)
export type TLFrameShape = TLBaseShape<'frame', TLFrameShapeProps>;
// @public (undocumented)
export type TLGeoShape = TLBaseShape<'geo', TLGeoShapeProps>;
// @public (undocumented)
export interface TLGeoStyle extends TLBaseStyle {
// (undocumented)
id: TLGeoType;
// (undocumented)
type: 'geo';
}
// @public (undocumented)
export type TLGeoType = SetValue<typeof TL_GEO_TYPES>;
// @public (undocumented)
export type TLGroupShape = TLBaseShape<'group', TLGroupShapeProps>;
@ -893,20 +926,6 @@ export type TLHandleType = SetValue<typeof TL_HANDLE_TYPES>;
// @public (undocumented)
export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>;
// @public (undocumented)
export type TLIconShape = TLBaseShape<'icon', TLIconShapeProps>;
// @public (undocumented)
export interface TLIconStyle extends TLBaseStyle {
// (undocumented)
id: TLIconType;
// (undocumented)
type: 'icon';
}
// @public (undocumented)
export type TLIconType = SetValue<typeof TL_ICON_TYPES>;
// @public
export type TLImageAsset = TLBaseAsset<'image', {
w: number;
@ -917,24 +936,14 @@ export type TLImageAsset = TLBaseAsset<'image', {
src: null | string;
}>;
// @public (undocumented)
export type TLImageCrop = {
topLeft: Vec2dModel;
bottomRight: Vec2dModel;
};
// @public (undocumented)
export type TLImageShape = TLBaseShape<'image', TLImageShapeProps>;
// @public (undocumented)
export type TLImageShapeProps = {
url: string;
playing: boolean;
w: number;
h: number;
assetId: null | TLAssetId;
crop: null | TLImageCrop;
};
export type TLImageShapeCrop = T.TypeOf<typeof ImageShapeCrop>;
// @public (undocumented)
export type TLImageShapeProps = ShapePropsType<typeof imageShapeProps>;
// @public
export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
@ -967,12 +976,12 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
// (undocumented)
opacityForNextShape: TLOpacityType;
// (undocumented)
propsForNextShape: TLInstancePropsForNextShape;
// (undocumented)
screenBounds: Box2dModel;
// (undocumented)
scribble: null | TLScribble;
// (undocumented)
stylesForNextShape: Record<string, unknown>;
// (undocumented)
zoomBrush: Box2dModel | null;
}
@ -1041,9 +1050,6 @@ export interface TLInstancePresence extends BaseRecord<'instance_presence', TLIn
userName: string;
}
// @public (undocumented)
export type TLInstancePropsForNextShape = Pick<TLShapeProps, TLStyleType>;
// @public (undocumented)
export type TLLanguage = (typeof LANGUAGES)[number];
@ -1053,11 +1059,6 @@ export type TLLineShape = TLBaseShape<'line', TLLineShapeProps>;
// @public (undocumented)
export type TLNoteShape = TLBaseShape<'note', TLNoteShapeProps>;
// @public (undocumented)
export type TLNullableShapeProps = {
[K in TLShapeProp]?: null | TLShapeProps[K];
};
// @public (undocumented)
export type TLOpacityType = number;
@ -1113,28 +1114,6 @@ export type TLShapeProp = keyof TLShapeProps;
// @public (undocumented)
export type TLShapeProps = Identity<UnionToIntersection<TLDefaultShape['props']>>;
// @public (undocumented)
export interface TLSizeStyle extends TLBaseStyle {
// (undocumented)
id: TLSizeType;
// (undocumented)
type: 'size';
}
// @public (undocumented)
export type TLSizeType = SetValue<typeof TL_SIZE_TYPES>;
// @public (undocumented)
export interface TLSplineStyle extends TLBaseStyle {
// (undocumented)
id: TLSplineType;
// (undocumented)
type: 'spline';
}
// @public (undocumented)
export type TLSplineType = SetValue<typeof TL_SPLINE_TYPES>;
// @public (undocumented)
export type TLStore = Store<TLRecord, TLStoreProps>;
@ -1149,62 +1128,15 @@ export type TLStoreSchema = StoreSchema<TLRecord, TLStoreProps>;
// @public (undocumented)
export type TLStoreSnapshot = StoreSnapshot<TLRecord>;
// @public (undocumented)
export interface TLStyleCollections {
// (undocumented)
align: TLAlignStyle[];
// (undocumented)
arrowheadEnd: TLArrowheadEndStyle[];
// (undocumented)
arrowheadStart: TLArrowheadStartStyle[];
// (undocumented)
color: TLColorStyle[];
// (undocumented)
dash: TLDashStyle[];
// (undocumented)
fill: TLFillStyle[];
// (undocumented)
font: TLFontStyle[];
// (undocumented)
geo: TLGeoStyle[];
// (undocumented)
size: TLSizeStyle[];
// (undocumented)
spline: TLSplineStyle[];
// (undocumented)
verticalAlign: TLVerticalAlignStyle[];
}
// @public (undocumented)
export type TLStyleItem = TLAlignStyle | TLArrowheadEndStyle | TLArrowheadStartStyle | TLColorStyle | TLDashStyle | TLFillStyle | TLFontStyle | TLGeoStyle | TLSizeStyle | TLSplineStyle | TLVerticalAlignStyle;
// @public (undocumented)
export type TLStyleProps = Pick<TLShapeProps, TLStyleType>;
// @public (undocumented)
export type TLStyleType = SetValue<typeof TL_STYLE_TYPES>;
// @public (undocumented)
export type TLTextShape = TLBaseShape<'text', TLTextShapeProps>;
// @public (undocumented)
export type TLTextShapeProps = {
color: TLColorType;
size: TLSizeType;
font: TLFontType;
align: TLAlignType;
w: number;
text: string;
scale: number;
autoSize: boolean;
};
export type TLTextShapeProps = ShapePropsType<typeof textShapeProps>;
// @public
export type TLUnknownShape = TLBaseShape<string, object>;
// @public (undocumented)
export type TLVerticalAlignType = SetValue<typeof TL_VERTICAL_ALIGN_TYPES>;
// @public
export type TLVideoAsset = TLBaseAsset<'video', {
w: number;
@ -1228,14 +1160,21 @@ export interface Vec2dModel {
z?: number;
}
// @internal (undocumented)
export const verticalAlignValidator: T.Validator<"end" | "middle" | "start">;
// @public (undocumented)
export const vec2dModelValidator: T.Validator<Vec2dModel>;
// @internal (undocumented)
export const videoShapeMigrations: Migrations;
// @internal (undocumented)
export const videoShapeProps: ShapeProps<TLVideoShape>;
// @public (undocumented)
export const videoShapeProps: {
w: T.Validator<number>;
h: T.Validator<number>;
time: T.Validator<number>;
playing: T.Validator<boolean>;
url: T.Validator<string>;
assetId: T.Validator<TLAssetId | null>;
};
// (No @packageDocumentation comment for this package)

View file

@ -2,7 +2,7 @@ import { Store, StoreSchema, StoreSchemaOptions, StoreSnapshot } from '@tldraw/s
import { annotateError, structuredClone } from '@tldraw/utils'
import { CameraRecordType, TLCameraId } from './records/TLCamera'
import { DocumentRecordType, TLDOCUMENT_ID } from './records/TLDocument'
import { InstanceRecordType, TLINSTANCE_ID } from './records/TLInstance'
import { TLINSTANCE_ID } from './records/TLInstance'
import { PageRecordType, TLPageId } from './records/TLPage'
import { InstancePageStateRecordType, TLInstancePageStateId } from './records/TLPageState'
import { PointerRecordType, TLPOINTER_ID } from './records/TLPointer'
@ -103,7 +103,7 @@ export function createIntegrityChecker(store: TLStore): () => void {
const instanceState = store.get(TLINSTANCE_ID)
if (!instanceState) {
store.put([
InstanceRecordType.create({
store.schema.types.instance.create({
id: TLINSTANCE_ID,
currentPageId: getFirstPageId(),
exportBackground: true,

View file

@ -1,19 +1,18 @@
import { Migrations, StoreSchema, createRecordType, defineMigrations } from '@tldraw/store'
import { mapObjectMapValues } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { Migrations, StoreSchema } from '@tldraw/store'
import { objectMapValues } from '@tldraw/utils'
import { TLStoreProps, createIntegrityChecker, onValidationFailure } from './TLStore'
import { AssetRecordType } from './records/TLAsset'
import { CameraRecordType } from './records/TLCamera'
import { DocumentRecordType } from './records/TLDocument'
import { InstanceRecordType } from './records/TLInstance'
import { createInstanceRecordType } from './records/TLInstance'
import { PageRecordType } from './records/TLPage'
import { InstancePageStateRecordType } from './records/TLPageState'
import { PointerRecordType } from './records/TLPointer'
import { InstancePresenceRecordType } from './records/TLPresence'
import { TLRecord } from './records/TLRecord'
import { TLShape, rootShapeMigrations } from './records/TLShape'
import { createShapeValidator } from './shapes/TLBaseShape'
import { createShapeRecordType, getShapePropKeysByStyle } from './records/TLShape'
import { storeMigrations } from './store-migrations'
import { StyleProp } from './styles/StyleProp'
/** @public */
export type SchemaShapeInfo = {
@ -31,23 +30,18 @@ export type TLSchema = StoreSchema<TLRecord, TLStoreProps>
*
* @public */
export function createTLSchema({ shapes }: { shapes: Record<string, SchemaShapeInfo> }): TLSchema {
const ShapeRecordType = createRecordType<TLShape>('shape', {
migrations: defineMigrations({
currentVersion: rootShapeMigrations.currentVersion,
firstVersion: rootShapeMigrations.firstVersion,
migrators: rootShapeMigrations.migrators,
subTypeKey: 'type',
subTypeMigrations: mapObjectMapValues(shapes, (k, v) => v.migrations ?? defineMigrations({})),
}),
scope: 'document',
validator: T.model(
'shape',
T.union(
'type',
mapObjectMapValues(shapes, (type, { props }) => createShapeValidator(type, props))
)
),
}).withDefaultProperties(() => ({ x: 0, y: 0, rotation: 0, isLocked: false, opacity: 1 }))
const stylesById = new Map<string, StyleProp<unknown>>()
for (const shape of objectMapValues(shapes)) {
for (const style of getShapePropKeysByStyle(shape.props ?? {}).keys()) {
if (stylesById.has(style.id) && stylesById.get(style.id) !== style) {
throw new Error(`Multiple StyleProp instances with the same id: ${style.id}`)
}
stylesById.set(style.id, style)
}
}
const ShapeRecordType = createShapeRecordType(shapes)
const InstanceRecordType = createInstanceRecordType(stylesById)
return StoreSchema.create(
{

View file

@ -18,8 +18,14 @@ export {
} from './misc/TLColor'
export { type TLCursor, type TLCursorType } from './misc/TLCursor'
export { type TLHandle, type TLHandleType } from './misc/TLHandle'
export { opacityValidator, type TLOpacityType } from './misc/TLOpacity'
export { scribbleValidator, type TLScribble } from './misc/TLScribble'
export { type Box2dModel, type Vec2dModel } from './misc/geometry-types'
export {
box2dModelValidator,
vec2dModelValidator,
type Box2dModel,
type Vec2dModel,
} from './misc/geometry-types'
export { idValidator } from './misc/id-validator'
export {
AssetRecordType,
@ -32,14 +38,7 @@ export {
} from './records/TLAsset'
export { CameraRecordType, type TLCamera, type TLCameraId } from './records/TLCamera'
export { DocumentRecordType, TLDOCUMENT_ID, type TLDocument } from './records/TLDocument'
export {
InstanceRecordType,
TLINSTANCE_ID,
instanceTypeValidator,
type TLInstance,
type TLInstanceId,
type TLInstancePropsForNextShape,
} from './records/TLInstance'
export { TLINSTANCE_ID, type TLInstance, type TLInstanceId } from './records/TLInstance'
export {
PageRecordType,
isPageId,
@ -53,11 +52,11 @@ export { InstancePresenceRecordType, type TLInstancePresence } from './records/T
export { type TLRecord } from './records/TLRecord'
export {
createShapeId,
getShapePropKeysByStyle,
isShape,
isShapeId,
rootShapeMigrations,
type TLDefaultShape,
type TLNullableShapeProps,
type TLParentId,
type TLShape,
type TLShapeId,
@ -67,12 +66,14 @@ export {
type TLUnknownShape,
} from './records/TLShape'
export {
ArrowShapeArrowheadEndStyle,
ArrowShapeArrowheadStartStyle,
arrowShapeMigrations,
arrowShapeProps,
type TLArrowShape,
type TLArrowShapeArrowheadStyle,
type TLArrowShapeProps,
type TLArrowTerminal,
type TLArrowTerminalType,
type TLArrowShapeTerminal,
} from './shapes/TLArrowShape'
export {
createShapeValidator,
@ -102,22 +103,31 @@ export {
type TLEmbedShapePermissions,
} from './shapes/TLEmbedShape'
export { frameShapeMigrations, frameShapeProps, type TLFrameShape } from './shapes/TLFrameShape'
export { geoShapeMigrations, geoShapeProps, type TLGeoShape } from './shapes/TLGeoShape'
export {
GeoShapeGeoStyle,
geoShapeMigrations,
geoShapeProps,
type TLGeoShape,
} from './shapes/TLGeoShape'
export { groupShapeMigrations, groupShapeProps, type TLGroupShape } from './shapes/TLGroupShape'
export {
highlightShapeMigrations,
highlightShapeProps,
type TLHighlightShape,
} from './shapes/TLHighlightShape'
export { iconShapeMigrations, iconShapeProps, type TLIconShape } from './shapes/TLIconShape'
export {
imageShapeMigrations,
imageShapeProps,
type TLImageCrop,
type TLImageShape,
type TLImageShapeCrop,
type TLImageShapeProps,
} from './shapes/TLImageShape'
export { lineShapeMigrations, lineShapeProps, type TLLineShape } from './shapes/TLLineShape'
export {
LineShapeSplineStyle,
lineShapeMigrations,
lineShapeProps,
type TLLineShape,
} from './shapes/TLLineShape'
export { noteShapeMigrations, noteShapeProps, type TLNoteShape } from './shapes/TLNoteShape'
export {
textShapeMigrations,
@ -126,60 +136,20 @@ export {
type TLTextShapeProps,
} from './shapes/TLTextShape'
export { videoShapeMigrations, videoShapeProps, type TLVideoShape } from './shapes/TLVideoShape'
export { EnumStyleProp, StyleProp } from './styles/StyleProp'
export { DefaultColorStyle, type TLDefaultColorStyle } from './styles/TLColorStyle'
export { DefaultDashStyle, type TLDefaultDashStyle } from './styles/TLDashStyle'
export { DefaultFillStyle, type TLDefaultFillStyle } from './styles/TLFillStyle'
export { DefaultFontStyle, type TLDefaultFontStyle } from './styles/TLFontStyle'
export {
TL_ALIGN_TYPES,
alignValidator,
type TLAlignStyle,
type TLAlignType,
} from './styles/TLAlignStyle'
DefaultHorizontalAlignStyle,
type TLDefaultHorizontalAlignStyle,
} from './styles/TLHorizontalAlignStyle'
export { DefaultSizeStyle, type TLDefaultSizeStyle } from './styles/TLSizeStyle'
export {
TL_ARROWHEAD_TYPES,
type TLArrowheadEndStyle,
type TLArrowheadStartStyle,
type TLArrowheadType,
} from './styles/TLArrowheadStyle'
export { TL_STYLE_TYPES, type TLStyleType } from './styles/TLBaseStyle'
export {
TL_COLOR_TYPES,
colorValidator,
type TLColorStyle,
type TLColorType,
} from './styles/TLColorStyle'
export {
TL_DASH_TYPES,
dashValidator,
type TLDashStyle,
type TLDashType,
} from './styles/TLDashStyle'
export {
TL_FILL_TYPES,
fillValidator,
type TLFillStyle,
type TLFillType,
} from './styles/TLFillStyle'
export {
TL_FONT_TYPES,
fontValidator,
type TLFontStyle,
type TLFontType,
} from './styles/TLFontStyle'
export { TL_GEO_TYPES, geoValidator, type TLGeoStyle, type TLGeoType } from './styles/TLGeoStyle'
export { iconValidator, type TLIconStyle, type TLIconType } from './styles/TLIconStyle'
export { opacityValidator, type TLOpacityType } from './styles/TLOpacityStyle'
export {
TL_SIZE_TYPES,
sizeValidator,
type TLSizeStyle,
type TLSizeType,
} from './styles/TLSizeStyle'
export {
TL_SPLINE_TYPES,
splineValidator,
type TLSplineStyle,
type TLSplineType,
} from './styles/TLSplineStyle'
export { verticalAlignValidator, type TLVerticalAlignType } from './styles/TLVerticalAlignStyle'
export { type TLStyleCollections, type TLStyleItem, type TLStyleProps } from './styles/style-types'
DefaultVerticalAlignStyle,
type TLDefaultVerticalAlignStyle,
} from './styles/TLVerticalAlignStyle'
export {
LANGUAGES,
getDefaultTranslationLocale,

View file

@ -1191,6 +1191,47 @@ describe('Removes overridePermissions from embed', () => {
})
})
describe('propsForNextShape -> stylesForNextShape', () => {
test('deletes propsForNextShape and adds stylesForNextShape without trying to bring across contents', () => {
const { up, down } =
instanceMigrations.migrators[
instanceTypeVersions.ReplacePropsForNextShapeWithStylesForNextShape
]
const beforeUp = {
isToolLocked: true,
propsForNextShape: {
color: 'red',
size: 'm',
},
}
const afterUp = {
isToolLocked: true,
stylesForNextShape: {},
}
const afterDown = {
isToolLocked: true,
propsForNextShape: {
color: 'black',
labelColor: 'black',
dash: 'draw',
fill: 'none',
size: 'm',
icon: 'file',
font: 'draw',
align: 'middle',
verticalAlign: 'middle',
geo: 'rectangle',
arrowheadStart: 'none',
arrowheadEnd: 'arrow',
spline: 'line',
},
}
expect(up(beforeUp)).toEqual(afterUp)
expect(down(afterUp)).toEqual(afterDown)
})
})
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
for (const migrator of allMigrators) {

View file

@ -1,7 +1,7 @@
import { T } from '@tldraw/validate'
import { SetValue } from '../util-types'
import { TLCanvasUiColor, canvasUiColorTypeValidator } from './TLColor'
import { Vec2dModel } from './geometry-types'
import { Vec2dModel, vec2dModelValidator } from './geometry-types'
/**
* The scribble states used by tldraw.
@ -24,7 +24,7 @@ export type TLScribble = {
/** @internal */
export const scribbleValidator: T.Validator<TLScribble> = T.object({
points: T.arrayOf(T.point),
points: T.arrayOf(vec2dModelValidator),
size: T.positiveNumber,
color: canvasUiColorTypeValidator,
opacity: T.number,

View file

@ -1,3 +1,5 @@
import { T } from '@tldraw/validate'
/**
* A serializable model for 2D boxes.
*
@ -18,3 +20,18 @@ export interface Vec2dModel {
y: number
z?: number
}
/** @public */
export const vec2dModelValidator: T.Validator<Vec2dModel> = T.object({
x: T.number,
y: T.number,
z: T.number.optional(),
})
/** @public */
export const box2dModelValidator: T.Validator<Box2dModel> = T.object({
x: T.number,
y: T.number,
w: T.number,
h: T.number,
})

View file

@ -1,27 +1,12 @@
import { BaseRecord, createRecordType, defineMigrations, RecordId } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { Box2dModel } from '../misc/geometry-types'
import { Box2dModel, box2dModelValidator } from '../misc/geometry-types'
import { idValidator } from '../misc/id-validator'
import { cursorValidator, TLCursor } from '../misc/TLCursor'
import { opacityValidator, TLOpacityType } from '../misc/TLOpacity'
import { scribbleValidator, TLScribble } from '../misc/TLScribble'
import { alignValidator } from '../styles/TLAlignStyle'
import { arrowheadValidator } from '../styles/TLArrowheadStyle'
import { TL_STYLE_TYPES, TLStyleType } from '../styles/TLBaseStyle'
import { colorValidator } from '../styles/TLColorStyle'
import { dashValidator } from '../styles/TLDashStyle'
import { fillValidator } from '../styles/TLFillStyle'
import { fontValidator } from '../styles/TLFontStyle'
import { geoValidator } from '../styles/TLGeoStyle'
import { iconValidator } from '../styles/TLIconStyle'
import { opacityValidator, TLOpacityType } from '../styles/TLOpacityStyle'
import { sizeValidator } from '../styles/TLSizeStyle'
import { splineValidator } from '../styles/TLSplineStyle'
import { verticalAlignValidator } from '../styles/TLVerticalAlignStyle'
import { StyleProp } from '../styles/StyleProp'
import { pageIdValidator, TLPageId } from './TLPage'
import { TLShapeProps } from './TLShape'
/** @public */
export type TLInstancePropsForNextShape = Pick<TLShapeProps, TLStyleType>
/**
* TLInstance
@ -36,7 +21,7 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
highlightedUserIds: string[]
brush: Box2dModel | null
opacityForNextShape: TLOpacityType
propsForNextShape: TLInstancePropsForNextShape
stylesForNextShape: Record<string, unknown>
cursor: TLCursor
scribble: TLScribble | null
isFocusMode: boolean
@ -45,10 +30,8 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
exportBackground: boolean
screenBounds: Box2dModel
zoomBrush: Box2dModel | null
chatMessage: string
isChatting: boolean
isPenMode: boolean
isGridMode: boolean
}
@ -59,46 +42,68 @@ export type TLInstanceId = RecordId<TLInstance>
/** @internal */
export const instanceIdValidator = idValidator<TLInstanceId>('instance')
/** @internal */
export const instanceTypeValidator: T.Validator<TLInstance> = T.model(
'instance',
T.object({
typeName: T.literal('instance'),
id: idValidator<TLInstanceId>('instance'),
currentPageId: pageIdValidator,
followingUserId: T.string.nullable(),
highlightedUserIds: T.arrayOf(T.string),
brush: T.boxModel.nullable(),
opacityForNextShape: opacityValidator,
propsForNextShape: T.object({
color: colorValidator,
labelColor: colorValidator,
dash: dashValidator,
fill: fillValidator,
size: sizeValidator,
font: fontValidator,
align: alignValidator,
verticalAlign: verticalAlignValidator,
icon: iconValidator,
geo: geoValidator,
arrowheadStart: arrowheadValidator,
arrowheadEnd: arrowheadValidator,
spline: splineValidator,
}),
cursor: cursorValidator,
scribble: scribbleValidator.nullable(),
isFocusMode: T.boolean,
isDebugMode: T.boolean,
isToolLocked: T.boolean,
exportBackground: T.boolean,
screenBounds: T.boxModel,
zoomBrush: T.boxModel.nullable(),
chatMessage: T.string,
isChatting: T.boolean,
isPenMode: T.boolean,
isGridMode: T.boolean,
})
)
export function createInstanceRecordType(stylesById: Map<string, StyleProp<unknown>>) {
const stylesForNextShapeValidators = {} as Record<string, T.Validator<unknown>>
for (const [id, style] of stylesById) {
stylesForNextShapeValidators[id] = T.optional(style)
}
const instanceTypeValidator: T.Validator<TLInstance> = T.model(
'instance',
T.object({
typeName: T.literal('instance'),
id: idValidator<TLInstanceId>('instance'),
currentPageId: pageIdValidator,
followingUserId: T.string.nullable(),
brush: box2dModelValidator.nullable(),
opacityForNextShape: opacityValidator,
stylesForNextShape: T.object(stylesForNextShapeValidators),
cursor: cursorValidator,
scribble: scribbleValidator.nullable(),
isFocusMode: T.boolean,
isDebugMode: T.boolean,
isToolLocked: T.boolean,
exportBackground: T.boolean,
screenBounds: box2dModelValidator,
zoomBrush: box2dModelValidator.nullable(),
isPenMode: T.boolean,
isGridMode: T.boolean,
chatMessage: T.string,
isChatting: T.boolean,
highlightedUserIds: T.arrayOf(T.string),
})
)
return createRecordType<TLInstance>('instance', {
migrations: instanceMigrations,
validator: instanceTypeValidator,
scope: 'session',
}).withDefaultProperties(
(): Omit<TLInstance, 'typeName' | 'id' | 'currentPageId'> => ({
followingUserId: null,
opacityForNextShape: 1,
stylesForNextShape: {},
brush: null,
scribble: null,
cursor: {
type: 'default',
color: 'black',
rotation: 0,
},
isFocusMode: false,
exportBackground: false,
isDebugMode: process.env.NODE_ENV === 'development',
isToolLocked: false,
screenBounds: { x: 0, y: 0, w: 1080, h: 720 },
zoomBrush: null,
isGridMode: false,
isPenMode: false,
chatMessage: '',
isChatting: false,
highlightedUserIds: [],
})
)
}
const Versions = {
AddTransparentExportBgs: 1,
@ -116,13 +121,14 @@ const Versions = {
HoistOpacity: 13,
AddChat: 14,
AddHighlightedUserIds: 15,
ReplacePropsForNextShapeWithStylesForNextShape: 16,
} as const
export { Versions as instanceTypeVersions }
/** @public */
export const instanceMigrations = defineMigrations({
currentVersion: Versions.AddHighlightedUserIds,
currentVersion: Versions.ReplacePropsForNextShapeWithStylesForNextShape,
migrators: {
[Versions.AddTransparentExportBgs]: {
up: (instance: TLInstance) => {
@ -154,7 +160,21 @@ export const instanceMigrations = defineMigrations({
...instance,
propsForNextShape: Object.fromEntries(
Object.entries(propsForNextShape).filter(([key]) =>
TL_STYLE_TYPES.has(key as TLStyleType)
[
'color',
'labelColor',
'dash',
'fill',
'size',
'font',
'align',
'verticalAlign',
'icon',
'geo',
'arrowheadStart',
'arrowheadEnd',
'spline',
].includes(key)
)
),
}
@ -174,7 +194,7 @@ export const instanceMigrations = defineMigrations({
},
}
},
down: (instance: TLInstance) => {
down: (instance) => {
const { labelColor: _, ...rest } = instance.propsForNextShape
return {
...instance,
@ -220,7 +240,7 @@ export const instanceMigrations = defineMigrations({
},
},
[Versions.AddVerticalAlign]: {
up: (instance: TLInstance) => {
up: (instance) => {
return {
...instance,
propsForNextShape: {
@ -229,7 +249,7 @@ export const instanceMigrations = defineMigrations({
},
}
},
down: (instance: TLInstance) => {
down: (instance) => {
const { verticalAlign: _, ...propsForNextShape } = instance.propsForNextShape
return {
...instance,
@ -307,53 +327,33 @@ export const instanceMigrations = defineMigrations({
return instance
},
},
[Versions.ReplacePropsForNextShapeWithStylesForNextShape]: {
up: ({ propsForNextShape: _, ...instance }) => {
return { ...instance, stylesForNextShape: {} }
},
down: ({ stylesForNextShape: _, ...instance }: TLInstance) => {
return {
...instance,
propsForNextShape: {
color: 'black',
labelColor: 'black',
dash: 'draw',
fill: 'none',
size: 'm',
icon: 'file',
font: 'draw',
align: 'middle',
verticalAlign: 'middle',
geo: 'rectangle',
arrowheadStart: 'none',
arrowheadEnd: 'arrow',
spline: 'line',
},
}
},
},
},
})
/** @public */
export const InstanceRecordType = createRecordType<TLInstance>('instance', {
migrations: instanceMigrations,
validator: instanceTypeValidator,
scope: 'session',
}).withDefaultProperties(
(): Omit<TLInstance, 'typeName' | 'id' | 'currentPageId'> => ({
followingUserId: null,
highlightedUserIds: [],
opacityForNextShape: 1,
propsForNextShape: {
color: 'black',
labelColor: 'black',
dash: 'draw',
fill: 'none',
size: 'm',
icon: 'file',
font: 'draw',
align: 'middle',
verticalAlign: 'middle',
geo: 'rectangle',
arrowheadStart: 'none',
arrowheadEnd: 'arrow',
spline: 'line',
},
brush: null,
scribble: null,
cursor: {
type: 'default',
color: 'black',
rotation: 0,
},
isFocusMode: false,
exportBackground: false,
isDebugMode: process.env.NODE_ENV === 'development',
isToolLocked: false,
screenBounds: { x: 0, y: 0, w: 1080, h: 720 },
zoomBrush: null,
chatMessage: '',
isChatting: false,
isGridMode: false,
isPenMode: false,
})
)
/** @public */
export const TLINSTANCE_ID = InstanceRecordType.createId('instance')
export const TLINSTANCE_ID = 'instance:instance' as TLInstanceId

View file

@ -1,6 +1,6 @@
import { BaseRecord, createRecordType, defineMigrations, RecordId } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { Box2dModel } from '../misc/geometry-types'
import { Box2dModel, box2dModelValidator } from '../misc/geometry-types'
import { idValidator } from '../misc/id-validator'
import { cursorTypeValidator, TLCursor } from '../misc/TLCursor'
import { scribbleValidator, TLScribble } from '../misc/TLScribble'
@ -55,10 +55,10 @@ export const instancePresenceValidator: T.Validator<TLInstancePresence> = T.mode
y: T.number,
z: T.number,
}),
screenBounds: T.boxModel,
screenBounds: box2dModelValidator,
selectedIds: T.arrayOf(idValidator<TLShapeId>('shape')),
currentPageId: idValidator<TLPageId>('page'),
brush: T.boxModel.nullable(),
brush: box2dModelValidator.nullable(),
scribble: scribbleValidator.nullable(),
chatMessage: T.string,
})

View file

@ -1,7 +1,10 @@
import { defineMigrations, RecordId, UnknownRecord } from '@tldraw/store'
import { createRecordType, defineMigrations, RecordId, UnknownRecord } from '@tldraw/store'
import { mapObjectMapValues } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { nanoid } from 'nanoid'
import { SchemaShapeInfo } from '../createTLSchema'
import { TLArrowShape } from '../shapes/TLArrowShape'
import { TLBaseShape } from '../shapes/TLBaseShape'
import { createShapeValidator, TLBaseShape } from '../shapes/TLBaseShape'
import { TLBookmarkShape } from '../shapes/TLBookmarkShape'
import { TLDrawShape } from '../shapes/TLDrawShape'
import { TLEmbedShape } from '../shapes/TLEmbedShape'
@ -9,12 +12,12 @@ import { TLFrameShape } from '../shapes/TLFrameShape'
import { TLGeoShape } from '../shapes/TLGeoShape'
import { TLGroupShape } from '../shapes/TLGroupShape'
import { TLHighlightShape } from '../shapes/TLHighlightShape'
import { TLIconShape } from '../shapes/TLIconShape'
import { TLImageShape } from '../shapes/TLImageShape'
import { TLLineShape } from '../shapes/TLLineShape'
import { TLNoteShape } from '../shapes/TLNoteShape'
import { TLTextShape } from '../shapes/TLTextShape'
import { TLVideoShape } from '../shapes/TLVideoShape'
import { StyleProp } from '../styles/StyleProp'
import { TLPageId } from './TLPage'
/**
@ -34,7 +37,6 @@ export type TLDefaultShape =
| TLNoteShape
| TLTextShape
| TLVideoShape
| TLIconShape
| TLHighlightShape
/**
@ -79,9 +81,6 @@ export type TLShapeProp = keyof TLShapeProps
/** @public */
export type TLParentId = TLPageId | TLShapeId
/** @public */
export type TLNullableShapeProps = { [K in TLShapeProp]?: TLShapeProps[K] | null }
export const Versions = {
AddIsLocked: 1,
HoistOpacity: 2,
@ -151,3 +150,46 @@ export function isShapeId(id?: string): id is TLShapeId {
export function createShapeId(id?: string): TLShapeId {
return `shape:${id ?? nanoid()}` as TLShapeId
}
/** @internal */
export function getShapePropKeysByStyle(props: Record<string, T.Validatable<any>>) {
const propKeysByStyle = new Map<StyleProp<unknown>, string>()
for (const [key, prop] of Object.entries(props)) {
if (prop instanceof StyleProp) {
if (propKeysByStyle.has(prop)) {
throw new Error(
`Duplicate style prop ${prop.id}. Each style prop can only be used once within a shape.`
)
}
propKeysByStyle.set(prop, key)
}
}
return propKeysByStyle
}
/** @internal */
export function createShapeRecordType(shapes: Record<string, SchemaShapeInfo>) {
return createRecordType<TLShape>('shape', {
migrations: defineMigrations({
currentVersion: rootShapeMigrations.currentVersion,
firstVersion: rootShapeMigrations.firstVersion,
migrators: rootShapeMigrations.migrators,
subTypeKey: 'type',
subTypeMigrations: mapObjectMapValues(shapes, (k, v) => v.migrations ?? defineMigrations({})),
}),
scope: 'document',
validator: T.model(
'shape',
T.union(
'type',
mapObjectMapValues(shapes, (type, { props }) => createShapeValidator(type, props))
)
),
}).withDefaultProperties(() => ({
x: 0,
y: 0,
rotation: 0,
isLocked: false,
opacity: 1,
}))
}

View file

@ -1,57 +1,47 @@
import { defineMigrations } from '@tldraw/store'
import { T } from '@tldraw/validate'
import { Vec2dModel } from '../misc/geometry-types'
import { TLShapeId } from '../records/TLShape'
import { TLArrowheadType, arrowheadValidator } from '../styles/TLArrowheadStyle'
import { TLColorType, colorValidator } from '../styles/TLColorStyle'
import { TLDashType, dashValidator } from '../styles/TLDashStyle'
import { TLFillType, fillValidator } from '../styles/TLFillStyle'
import { TLFontType, fontValidator } from '../styles/TLFontStyle'
import { TLSizeType, sizeValidator } from '../styles/TLSizeStyle'
import { SetValue } from '../util-types'
import { ShapeProps, TLBaseShape, shapeIdValidator } from './TLBaseShape'
import { vec2dModelValidator } from '../misc/geometry-types'
import { StyleProp } from '../styles/StyleProp'
import { DefaultColorStyle, DefaultLabelColorStyle } from '../styles/TLColorStyle'
import { DefaultDashStyle } from '../styles/TLDashStyle'
import { DefaultFillStyle } from '../styles/TLFillStyle'
import { DefaultFontStyle } from '../styles/TLFontStyle'
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
import { ShapePropsType, TLBaseShape, shapeIdValidator } from './TLBaseShape'
const arrowheadTypes = [
'arrow',
'triangle',
'square',
'dot',
'pipe',
'diamond',
'inverted',
'bar',
'none',
] as const
/** @public */
export const TL_ARROW_TERMINAL_TYPE = new Set(['binding', 'point'] as const)
export const ArrowShapeArrowheadStartStyle = StyleProp.defineEnum('tldraw:arrowheadStart', {
defaultValue: 'none',
values: arrowheadTypes,
})
/** @public */
export type TLArrowTerminalType = SetValue<typeof TL_ARROW_TERMINAL_TYPE>
export const ArrowShapeArrowheadEndStyle = StyleProp.defineEnum('tldraw:arrowheadEnd', {
defaultValue: 'arrow',
values: arrowheadTypes,
})
/** @public */
export type TLArrowTerminal =
| {
type: 'binding'
boundShapeId: TLShapeId
normalizedAnchor: Vec2dModel
isExact: boolean
}
| { type: 'point'; x: number; y: number }
export type TLArrowShapeArrowheadStyle = T.TypeOf<typeof ArrowShapeArrowheadStartStyle>
/** @public */
export type TLArrowShapeProps = {
labelColor: TLColorType
color: TLColorType
fill: TLFillType
dash: TLDashType
size: TLSizeType
arrowheadStart: TLArrowheadType
arrowheadEnd: TLArrowheadType
font: TLFontType
start: TLArrowTerminal
end: TLArrowTerminal
bend: number
text: string
}
/** @public */
export type TLArrowShape = TLBaseShape<'arrow', TLArrowShapeProps>
/** @internal */
export const arrowTerminalValidator: T.Validator<TLArrowTerminal> = T.union('type', {
const ArrowShapeTerminal = T.union('type', {
binding: T.object({
type: T.literal('binding'),
boundShapeId: shapeIdValidator,
normalizedAnchor: T.point,
normalizedAnchor: vec2dModelValidator,
isExact: T.boolean,
}),
point: T.object({
@ -61,22 +51,31 @@ export const arrowTerminalValidator: T.Validator<TLArrowTerminal> = T.union('typ
}),
})
/** @internal */
export const arrowShapeProps: ShapeProps<TLArrowShape> = {
labelColor: colorValidator,
color: colorValidator,
fill: fillValidator,
dash: dashValidator,
size: sizeValidator,
arrowheadStart: arrowheadValidator,
arrowheadEnd: arrowheadValidator,
font: fontValidator,
start: arrowTerminalValidator,
end: arrowTerminalValidator,
/** @public */
export type TLArrowShapeTerminal = T.TypeOf<typeof ArrowShapeTerminal>
/** @public */
export const arrowShapeProps = {
labelColor: DefaultLabelColorStyle,
color: DefaultColorStyle,
fill: DefaultFillStyle,
dash: DefaultDashStyle,
size: DefaultSizeStyle,
arrowheadStart: ArrowShapeArrowheadStartStyle,
arrowheadEnd: ArrowShapeArrowheadEndStyle,
font: DefaultFontStyle,
start: ArrowShapeTerminal,
end: ArrowShapeTerminal,
bend: T.number,
text: T.string,
}
/** @public */
export type TLArrowShapeProps = ShapePropsType<typeof arrowShapeProps>
/** @public */
export type TLArrowShape = TLBaseShape<'arrow', TLArrowShapeProps>
const Versions = {
AddLabelColor: 1,
} as const

View file

@ -1,8 +1,9 @@
import { BaseRecord } from '@tldraw/store'
import { Expand } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { TLOpacityType, opacityValidator } from '../misc/TLOpacity'
import { idValidator } from '../misc/id-validator'
import { TLParentId, TLShapeId } from '../records/TLShape'
import { TLOpacityType, opacityValidator } from '../styles/TLOpacityStyle'
/** @public */
export interface TLBaseShape<Type extends string, Props extends object>
@ -51,5 +52,9 @@ export function createShapeValidator<Type extends string, Props extends object>(
/** @public */
export type ShapeProps<Shape extends TLBaseShape<any, any>> = {
[K in keyof Shape['props']]: T.Validator<Shape['props'][K]>
[K in keyof Shape['props']]: T.Validatable<Shape['props'][K]>
}
export type ShapePropsType<Config extends Record<string, T.Validatable<any>>> = Expand<{
[K in keyof Config]: T.TypeOf<Config[K]>
}>

Some files were not shown because too many files have changed in this diff Show more