Dynamic size mode + fill fill (#3835)

This PR adds a user preference for "dynamic size mode" where the scale
of shapes (text size, stroke width) is relative to the current zoom
level. This means that the stroke width in screen pixels (or text size
in screen pixels) is identical regardless of zoom level.

![Kapture 2024-05-27 at 05 23
21](https://github.com/tldraw/tldraw/assets/23072548/f247ecce-bfcd-4f85-b7a5-d7677b38e4d8)

- [x] Draw shape
- [x] Text shape
- [x] Highlighter shape
- [x] Geo shape
- [x] Arrow shape
- [x] Note shape
- [x] Line shape

Embed shape?

### Change Type

- [x] `sdk` — Changes the tldraw SDK
- [x] `feature` — New feature

### Test Plan

1. Use the tools.
2. Change zoom

- [ ] Unit Tests

### Release Notes

- Adds a dynamic size user preferences.
- Removes double click to reset scale on text shapes.
- Removes double click to reset autosize on text shapes.

---------

Co-authored-by: Taha <98838967+Taha-Hassan-Git@users.noreply.github.com>
Co-authored-by: huppy-bot[bot] <128400622+huppy-bot[bot]@users.noreply.github.com>
This commit is contained in:
Steve Ruiz 2024-06-16 19:58:13 +03:00 committed by GitHub
parent cbf7c2c605
commit ac149c1014
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
81 changed files with 1262 additions and 671 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -0,0 +1,4 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M26 4H8V5.99951H22C23.1046 5.99951 24 6.89494 24 7.99951V22H26V4ZM6 4V5.99951H4C2.89543 5.99951 2 6.89494 2 7.99951V25.9995C2 27.1041 2.89543 27.9995 4 27.9995H22C23.1046 27.9995 24 27.1041 24 25.9995V24H26C27.1046 24 28 23.1046 28 22V4C28 2.89543 27.1046 2 26 2H8C6.89543 2 6 2.89543 6 4ZM22 25.9995L4 26V8L22 7.99951V25.9995Z" fill="black"/>
<rect x="3" y="7" width="20" height="20" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 552 B

View file

@ -93,6 +93,8 @@
"action.toggle-debug-mode": "Toggle debug mode", "action.toggle-debug-mode": "Toggle debug mode",
"action.toggle-focus-mode.menu": "Focus mode", "action.toggle-focus-mode.menu": "Focus mode",
"action.toggle-focus-mode": "Toggle focus mode", "action.toggle-focus-mode": "Toggle focus mode",
"action.toggle-dynamic-size-mode.menu": "Dynamic size",
"action.toggle-dynamic-size-mode": "Toggle dynamic size",
"action.toggle-grid.menu": "Show grid", "action.toggle-grid.menu": "Show grid",
"action.toggle-grid": "Toggle grid", "action.toggle-grid": "Toggle grid",
"action.toggle-lock": "Toggle locked", "action.toggle-lock": "Toggle locked",

View file

@ -74,6 +74,7 @@ import iconsDragHandleDots from './icons/icon/drag-handle-dots.svg'
import iconsDuplicate from './icons/icon/duplicate.svg' import iconsDuplicate from './icons/icon/duplicate.svg'
import iconsEdit from './icons/icon/edit.svg' import iconsEdit from './icons/icon/edit.svg'
import iconsExternalLink from './icons/icon/external-link.svg' import iconsExternalLink from './icons/icon/external-link.svg'
import iconsFillFill from './icons/icon/fill-fill.svg'
import iconsFillNone from './icons/icon/fill-none.svg' import iconsFillNone from './icons/icon/fill-none.svg'
import iconsFillPattern from './icons/icon/fill-pattern.svg' import iconsFillPattern from './icons/icon/fill-pattern.svg'
import iconsFillSemi from './icons/icon/fill-semi.svg' import iconsFillSemi from './icons/icon/fill-semi.svg'
@ -266,6 +267,7 @@ export function getAssetUrlsByImport(opts) {
duplicate: formatAssetUrl(iconsDuplicate, opts), duplicate: formatAssetUrl(iconsDuplicate, opts),
edit: formatAssetUrl(iconsEdit, opts), edit: formatAssetUrl(iconsEdit, opts),
'external-link': formatAssetUrl(iconsExternalLink, opts), 'external-link': formatAssetUrl(iconsExternalLink, opts),
'fill-fill': formatAssetUrl(iconsFillFill, opts),
'fill-none': formatAssetUrl(iconsFillNone, opts), 'fill-none': formatAssetUrl(iconsFillNone, opts),
'fill-pattern': formatAssetUrl(iconsFillPattern, opts), 'fill-pattern': formatAssetUrl(iconsFillPattern, opts),
'fill-semi': formatAssetUrl(iconsFillSemi, opts), 'fill-semi': formatAssetUrl(iconsFillSemi, opts),

View file

@ -74,6 +74,7 @@ import iconsDragHandleDots from './icons/icon/drag-handle-dots.svg?url'
import iconsDuplicate from './icons/icon/duplicate.svg?url' import iconsDuplicate from './icons/icon/duplicate.svg?url'
import iconsEdit from './icons/icon/edit.svg?url' import iconsEdit from './icons/icon/edit.svg?url'
import iconsExternalLink from './icons/icon/external-link.svg?url' import iconsExternalLink from './icons/icon/external-link.svg?url'
import iconsFillFill from './icons/icon/fill-fill.svg?url'
import iconsFillNone from './icons/icon/fill-none.svg?url' import iconsFillNone from './icons/icon/fill-none.svg?url'
import iconsFillPattern from './icons/icon/fill-pattern.svg?url' import iconsFillPattern from './icons/icon/fill-pattern.svg?url'
import iconsFillSemi from './icons/icon/fill-semi.svg?url' import iconsFillSemi from './icons/icon/fill-semi.svg?url'
@ -266,6 +267,7 @@ export function getAssetUrlsByImport(opts) {
duplicate: formatAssetUrl(iconsDuplicate, opts), duplicate: formatAssetUrl(iconsDuplicate, opts),
edit: formatAssetUrl(iconsEdit, opts), edit: formatAssetUrl(iconsEdit, opts),
'external-link': formatAssetUrl(iconsExternalLink, opts), 'external-link': formatAssetUrl(iconsExternalLink, opts),
'fill-fill': formatAssetUrl(iconsFillFill, opts),
'fill-none': formatAssetUrl(iconsFillNone, opts), 'fill-none': formatAssetUrl(iconsFillNone, opts),
'fill-pattern': formatAssetUrl(iconsFillPattern, opts), 'fill-pattern': formatAssetUrl(iconsFillPattern, opts),
'fill-semi': formatAssetUrl(iconsFillSemi, opts), 'fill-semi': formatAssetUrl(iconsFillSemi, opts),

View file

@ -68,6 +68,7 @@ export function getAssetUrls(opts) {
duplicate: formatAssetUrl('./icons/icon/duplicate.svg', opts), duplicate: formatAssetUrl('./icons/icon/duplicate.svg', opts),
edit: formatAssetUrl('./icons/icon/edit.svg', opts), edit: formatAssetUrl('./icons/icon/edit.svg', opts),
'external-link': formatAssetUrl('./icons/icon/external-link.svg', opts), 'external-link': formatAssetUrl('./icons/icon/external-link.svg', opts),
'fill-fill': formatAssetUrl('./icons/icon/fill-fill.svg', opts),
'fill-none': formatAssetUrl('./icons/icon/fill-none.svg', opts), 'fill-none': formatAssetUrl('./icons/icon/fill-none.svg', opts),
'fill-pattern': formatAssetUrl('./icons/icon/fill-pattern.svg', opts), 'fill-pattern': formatAssetUrl('./icons/icon/fill-pattern.svg', opts),
'fill-semi': formatAssetUrl('./icons/icon/fill-semi.svg', opts), 'fill-semi': formatAssetUrl('./icons/icon/fill-semi.svg', opts),

View file

@ -58,6 +58,7 @@ export type AssetUrls = {
duplicate: string duplicate: string
edit: string edit: string
'external-link': string 'external-link': string
'fill-fill': string
'fill-none': string 'fill-none': string
'fill-pattern': string 'fill-pattern': string
'fill-semi': string 'fill-semi': string

View file

@ -191,6 +191,10 @@ export function getAssetUrlsByMetaUrl(opts) {
new URL('./icons/icon/external-link.svg', import.meta.url).href, new URL('./icons/icon/external-link.svg', import.meta.url).href,
opts opts
), ),
'fill-fill': formatAssetUrl(
new URL('./icons/icon/fill-fill.svg', import.meta.url).href,
opts
),
'fill-none': formatAssetUrl( 'fill-none': formatAssetUrl(
new URL('./icons/icon/fill-none.svg', import.meta.url).href, new URL('./icons/icon/fill-none.svg', import.meta.url).href,
opts opts

View file

@ -717,6 +717,7 @@ export const defaultUserPreferences: Readonly<{
color: "#02B1CC" | "#11B3A3" | "#39B178" | "#55B467" | "#7B66DC" | "#9D5BD2" | "#BD54C6" | "#E34BA9" | "#EC5E41" | "#F04F88" | "#F2555A" | "#FF802B"; color: "#02B1CC" | "#11B3A3" | "#39B178" | "#55B467" | "#7B66DC" | "#9D5BD2" | "#BD54C6" | "#E34BA9" | "#EC5E41" | "#F04F88" | "#F2555A" | "#FF802B";
edgeScrollSpeed: 1; edgeScrollSpeed: 1;
isDarkMode: false; isDarkMode: false;
isDynamicSizeMode: false;
isSnapMode: false; isSnapMode: false;
isWrapMode: false; isWrapMode: false;
locale: "ar" | "ca" | "cs" | "da" | "de" | "en" | "es" | "fa" | "fi" | "fr" | "gl" | "he" | "hi-in" | "hr" | "hu" | "id" | "it" | "ja" | "ko-kr" | "ku" | "my" | "ne" | "no" | "pl" | "pt-br" | "pt-pt" | "ro" | "ru" | "sl" | "sv" | "te" | "th" | "tr" | "uk" | "vi" | "zh-cn" | "zh-tw"; locale: "ar" | "ca" | "cs" | "da" | "de" | "en" | "es" | "fa" | "fi" | "fr" | "gl" | "he" | "hi-in" | "hr" | "hu" | "id" | "it" | "ja" | "ko-kr" | "ku" | "my" | "ne" | "no" | "pl" | "pt-br" | "pt-pt" | "ro" | "ru" | "sl" | "sv" | "te" | "th" | "tr" | "uk" | "vi" | "zh-cn" | "zh-tw";
@ -3336,6 +3337,8 @@ export interface TLUserPreferences {
// (undocumented) // (undocumented)
isDarkMode?: boolean | null; isDarkMode?: boolean | null;
// (undocumented) // (undocumented)
isDynamicSizeMode?: boolean | null;
// (undocumented)
isSnapMode?: boolean | null; isSnapMode?: boolean | null;
// (undocumented) // (undocumented)
isWrapMode?: boolean | null; isWrapMode?: boolean | null;
@ -3442,6 +3445,8 @@ export class UserPreferencesManager {
// (undocumented) // (undocumented)
getIsDarkMode(): boolean; getIsDarkMode(): boolean;
// (undocumented) // (undocumented)
getIsDynamicResizeMode(): boolean;
// (undocumented)
getIsSnapMode(): boolean; getIsSnapMode(): boolean;
// (undocumented) // (undocumented)
getIsWrapMode(): boolean; getIsWrapMode(): boolean;
@ -3455,6 +3460,7 @@ export class UserPreferencesManager {
color: string; color: string;
id: string; id: string;
isDarkMode: boolean; isDarkMode: boolean;
isDynamicResizeMode: boolean;
isSnapMode: boolean; isSnapMode: boolean;
isWrapMode: boolean; isWrapMode: boolean;
locale: string; locale: string;

View file

@ -1138,7 +1138,7 @@ input,
position: relative; position: relative;
top: 0px; top: 0px;
left: 0px; left: 0px;
padding: 16px; padding: inherit;
height: fit-content; height: fit-content;
width: fit-content; width: fit-content;
border-radius: var(--radius-1); border-radius: var(--radius-1);
@ -1150,7 +1150,7 @@ input,
inset: 0px; inset: 0px;
height: 100%; height: 100%;
width: 100%; width: 100%;
padding: 16px; padding: inherit;
} }
.tl-text-wrapper[data-isselected='true'] .tl-text-input { .tl-text-wrapper[data-isselected='true'] .tl-text-input {
@ -1236,12 +1236,12 @@ input,
.tl-arrow-label .tl-arrow { .tl-arrow-label .tl-arrow {
position: relative; position: relative;
height: max-content; height: max-content;
padding: 4px; padding: inherit;
overflow: visible; overflow: visible;
} }
.tl-arrow-label textarea { .tl-arrow-label textarea {
padding: 4px; padding: inherit;
/* Don't allow textarea to be zero width */ /* Don't allow textarea to be zero width */
min-width: 4px; min-width: 4px;
} }

View file

@ -71,17 +71,26 @@ export const GeometryDebuggingView = track(function GeometryDebuggingView({
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
> >
{showStroke && <GeometryStroke geometry={geometry} />} {showStroke && (
<g
stroke={geometry.debugColor ?? 'red'}
opacity="1"
strokeWidth={2 / zoomLevel}
fill="none"
>
<GeometryStroke geometry={geometry} />
</g>
)}
{showVertices && {showVertices &&
vertices.map((v, i) => ( vertices.map((v, i) => (
<circle <circle
key={`v${i}`} key={`v${i}`}
cx={v.x} cx={v.x}
cy={v.y} cy={v.y}
r="2" r={2 / zoomLevel}
fill={`hsl(${modulate(i, [0, vertices.length - 1], [120, 200])}, 100%, 50%)`} fill={`hsl(${modulate(i, [0, vertices.length - 1], [120, 200])}, 100%, 50%)`}
stroke="black" stroke="black"
strokeWidth="1" strokeWidth={1 / zoomLevel}
/> />
))} ))}
{showClosestPointOnOutline && dist < 150 && ( {showClosestPointOnOutline && dist < 150 && (
@ -92,7 +101,7 @@ export const GeometryDebuggingView = track(function GeometryDebuggingView({
y2={pointInShapeSpace.y} y2={pointInShapeSpace.y}
opacity={1 - dist / 150} opacity={1 - dist / 150}
stroke={hitInside ? 'goldenrod' : 'dodgerblue'} stroke={hitInside ? 'goldenrod' : 'dodgerblue'}
strokeWidth="2" strokeWidth={2 / zoomLevel}
/> />
)} )}
</g> </g>
@ -113,13 +122,5 @@ function GeometryStroke({ geometry }: { geometry: Geometry2d }) {
) )
} }
return ( return <path d={geometry.toSimpleSvgPath()} />
<path
stroke={geometry.debugColor ?? 'red'}
strokeWidth="2"
fill="none"
opacity="1"
d={geometry.toSimpleSvgPath()}
/>
)
} }

View file

@ -19,11 +19,8 @@ export function DefaultHandle({ handle, isCoarse, className, zoom }: TLHandlePro
if (handle.type === 'clone') { if (handle.type === 'clone') {
// bouba // bouba
const fr = 3 / Math.max(zoom, 0.35) const fr = 3 / zoom
const path = `M0,${-fr} A${fr},${fr} 0 0,1 0,${fr}` const path = `M0,${-fr} A${fr},${fr} 0 0,1 0,${fr}`
// kiki
// const fr = 4 / Math.max(zoom, 0.35)
// const path = `M0,${-fr} L${fr},0 L0,${fr} Z`
const index = SIDES.indexOf(handle.id as (typeof SIDES)[number]) const index = SIDES.indexOf(handle.id as (typeof SIDES)[number])
return ( return (
@ -35,7 +32,7 @@ export function DefaultHandle({ handle, isCoarse, className, zoom }: TLHandlePro
) )
} }
const fr = (handle.type === 'create' && isCoarse ? 3 : 4) / Math.max(zoom, 0.35) const fr = (handle.type === 'create' && isCoarse ? 3 : 4) / Math.max(zoom, 0.25)
return ( return (
<g className={classNames(`tl-handle tl-handle__${handle.type}`, className)}> <g className={classNames(`tl-handle tl-handle__${handle.type}`, className)}>
<circle className="tl-handle__bg" r={br} /> <circle className="tl-handle__bg" r={br} />

View file

@ -21,6 +21,7 @@ export interface TLUserPreferences {
isDarkMode?: boolean | null isDarkMode?: boolean | null
isSnapMode?: boolean | null isSnapMode?: boolean | null
isWrapMode?: boolean | null isWrapMode?: boolean | null
isDynamicSizeMode?: boolean | null
} }
interface UserDataSnapshot { interface UserDataSnapshot {
@ -39,11 +40,12 @@ const userTypeValidator: T.Validator<TLUserPreferences> = T.object<TLUserPrefere
name: T.string.nullable().optional(), name: T.string.nullable().optional(),
locale: T.string.nullable().optional(), locale: T.string.nullable().optional(),
color: T.string.nullable().optional(), color: T.string.nullable().optional(),
isDarkMode: T.boolean.nullable().optional(),
animationSpeed: T.number.nullable().optional(), animationSpeed: T.number.nullable().optional(),
edgeScrollSpeed: T.number.nullable().optional(), edgeScrollSpeed: T.number.nullable().optional(),
isDarkMode: T.boolean.nullable().optional(),
isSnapMode: T.boolean.nullable().optional(), isSnapMode: T.boolean.nullable().optional(),
isWrapMode: T.boolean.nullable().optional(), isWrapMode: T.boolean.nullable().optional(),
isDynamicSizeMode: T.boolean.nullable().optional(),
}) })
const Versions = { const Versions = {
@ -52,6 +54,7 @@ const Versions = {
MakeFieldsNullable: 3, MakeFieldsNullable: 3,
AddEdgeScrollSpeed: 4, AddEdgeScrollSpeed: 4,
AddExcalidrawSelectMode: 5, AddExcalidrawSelectMode: 5,
AddDynamicSizeMode: 6,
} as const } as const
const CURRENT_VERSION = Math.max(...Object.values(Versions)) const CURRENT_VERSION = Math.max(...Object.values(Versions))
@ -73,6 +76,10 @@ function migrateSnapshot(data: { version: number; user: any }) {
data.user.isWrapMode = false data.user.isWrapMode = false
} }
if (data.version < Versions.AddDynamicSizeMode) {
data.user.isDynamicSizeMode = false
}
// finally // finally
data.version = CURRENT_VERSION data.version = CURRENT_VERSION
} }
@ -123,6 +130,7 @@ export const defaultUserPreferences = Object.freeze({
animationSpeed: userPrefersReducedMotion() ? 0 : 1, animationSpeed: userPrefersReducedMotion() ? 0 : 1,
isSnapMode: false, isSnapMode: false,
isWrapMode: false, isWrapMode: false,
isDynamicSizeMode: false,
}) satisfies Readonly<Omit<TLUserPreferences, 'id'>> }) satisfies Readonly<Omit<TLUserPreferences, 'id'>>
/** @public */ /** @public */

View file

@ -29,6 +29,7 @@ export class UserPreferencesManager {
isSnapMode: this.getIsSnapMode(), isSnapMode: this.getIsSnapMode(),
isDarkMode: this.getIsDarkMode(), isDarkMode: this.getIsDarkMode(),
isWrapMode: this.getIsWrapMode(), isWrapMode: this.getIsWrapMode(),
isDynamicResizeMode: this.getIsDynamicResizeMode(),
} }
} }
@computed getIsDarkMode() { @computed getIsDarkMode() {
@ -72,4 +73,10 @@ export class UserPreferencesManager {
@computed getIsWrapMode() { @computed getIsWrapMode() {
return this.user.userPreferences.get().isWrapMode ?? defaultUserPreferences.isWrapMode return this.user.userPreferences.get().isWrapMode ?? defaultUserPreferences.isWrapMode
} }
@computed getIsDynamicResizeMode() {
return (
this.user.userPreferences.get().isDynamicSizeMode ?? defaultUserPreferences.isDynamicSizeMode
)
}
} }

View file

@ -1,4 +1,5 @@
import { createShapeId } from '@tldraw/tlschema' import { createShapeId } from '@tldraw/tlschema'
import { structuredClone } from '@tldraw/utils'
import { Vec } from '../../../../primitives/Vec' import { Vec } from '../../../../primitives/Vec'
import { TLBaseBoxShape } from '../../../shapes/BaseBoxShapeUtil' import { TLBaseBoxShape } from '../../../shapes/BaseBoxShapeUtil'
import { TLEventHandlers } from '../../../types/event-types' import { TLEventHandlers } from '../../../types/event-types'
@ -85,6 +86,8 @@ export class Pointing extends StateNode {
this.editor.mark(this.markId) this.editor.mark(this.markId)
// todo: add scale here when dynamic size is enabled
this.editor.createShapes<TLBaseBoxShape>([ this.editor.createShapes<TLBaseBoxShape>([
{ {
id, id,
@ -95,20 +98,35 @@ export class Pointing extends StateNode {
]) ])
const shape = this.editor.getShape<TLBaseBoxShape>(id)! const shape = this.editor.getShape<TLBaseBoxShape>(id)!
const { w, h } = this.editor.getShapeUtil(shape).getDefaultProps() as TLBaseBoxShape['props'] if (!shape) {
const delta = new Vec(w / 2, h / 2) this.cancel()
return
}
let { w, h } = shape.props
const delta = new Vec(w / 2, h / 2)
const parentTransform = this.editor.getShapeParentTransform(shape) const parentTransform = this.editor.getShapeParentTransform(shape)
if (parentTransform) delta.rot(-parentTransform.rotation()) if (parentTransform) delta.rot(-parentTransform.rotation())
let scale = 1
this.editor.updateShapes<TLBaseBoxShape>([ if (this.editor.user.getIsDynamicResizeMode()) {
{ scale = 1 / this.editor.getZoomLevel()
id, w *= scale
type: shapeType, h *= scale
x: shape.x - delta.x, delta.mul(scale)
y: shape.y - delta.y, }
},
]) const next = structuredClone(shape)
next.x = shape.x - delta.x
next.y = shape.y - delta.y
next.props.w = w
next.props.h = h
if ('scale' in shape.props) {
;(next as TLBaseBoxShape & { props: { scale: number } }).props.scale = scale
}
this.editor.updateShape<TLBaseBoxShape>(next)
this.editor.setSelectedShapes([id]) this.editor.setSelectedShapes([id])

File diff suppressed because one or more lines are too long

View file

@ -38,7 +38,6 @@ export { LineShapeTool } from './lib/shapes/line/LineShapeTool'
export { LineShapeUtil } from './lib/shapes/line/LineShapeUtil' export { LineShapeUtil } from './lib/shapes/line/LineShapeUtil'
export { NoteShapeTool } from './lib/shapes/note/NoteShapeTool' export { NoteShapeTool } from './lib/shapes/note/NoteShapeTool'
export { NoteShapeUtil } from './lib/shapes/note/NoteShapeUtil' export { NoteShapeUtil } from './lib/shapes/note/NoteShapeUtil'
export { useDefaultColorTheme } from './lib/shapes/shared/ShapeFill'
export { TextLabel, type TextLabelProps } from './lib/shapes/shared/TextLabel' export { TextLabel, type TextLabelProps } from './lib/shapes/shared/TextLabel'
export { export {
FONT_FAMILIES, FONT_FAMILIES,
@ -46,6 +45,7 @@ export {
TEXT_PROPS, TEXT_PROPS,
} from './lib/shapes/shared/default-shape-constants' } from './lib/shapes/shared/default-shape-constants'
export { getPerfectDashProps } from './lib/shapes/shared/getPerfectDashProps' export { getPerfectDashProps } from './lib/shapes/shared/getPerfectDashProps'
export { useDefaultColorTheme } from './lib/shapes/shared/useDefaultColorTheme'
export { useEditableText } from './lib/shapes/shared/useEditableText' export { useEditableText } from './lib/shapes/shared/useEditableText'
export { TextShapeTool } from './lib/shapes/text/TextShapeTool' export { TextShapeTool } from './lib/shapes/text/TextShapeTool'
export { TextShapeUtil } from './lib/shapes/text/TextShapeUtil' export { TextShapeUtil } from './lib/shapes/text/TextShapeUtil'

View file

@ -35,16 +35,18 @@ import {
} from '@tldraw/editor' } from '@tldraw/editor'
import React from 'react' import React from 'react'
import { updateArrowTerminal } from '../../bindings/arrow/ArrowBindingUtil' import { updateArrowTerminal } from '../../bindings/arrow/ArrowBindingUtil'
import { ShapeFill, useDefaultColorTheme } from '../shared/ShapeFill' import { ShapeFill } from '../shared/ShapeFill'
import { SvgTextLabel } from '../shared/SvgTextLabel' import { SvgTextLabel } from '../shared/SvgTextLabel'
import { ARROW_LABEL_FONT_SIZES, STROKE_SIZES } from '../shared/default-shape-constants' import { TextLabel } from '../shared/TextLabel'
import { STROKE_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
import { import {
getFillDefForCanvas, getFillDefForCanvas,
getFillDefForExport, getFillDefForExport,
getFontDefForExport, getFontDefForExport,
} from '../shared/defaultStyleDefs' } from '../shared/defaultStyleDefs'
import { getPerfectDashProps } from '../shared/getPerfectDashProps' import { getPerfectDashProps } from '../shared/getPerfectDashProps'
import { getArrowLabelPosition } from './arrowLabel' import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
import { getArrowLabelFontSize, getArrowLabelPosition } from './arrowLabel'
import { getArrowheadPathForType } from './arrowheads' import { getArrowheadPathForType } from './arrowheads'
import { import {
getCurvedArrowHandlePath, getCurvedArrowHandlePath,
@ -52,7 +54,6 @@ import {
getSolidStraightArrowPath, getSolidStraightArrowPath,
getStraightArrowHandlePath, getStraightArrowHandlePath,
} from './arrowpaths' } from './arrowpaths'
import { ArrowTextLabel } from './components/ArrowTextLabel'
import { import {
TLArrowBindings, TLArrowBindings,
createOrUpdateArrowBinding, createOrUpdateArrowBinding,
@ -107,6 +108,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
text: '', text: '',
labelPosition: 0.5, labelPosition: 0.5,
font: 'draw', font: 'draw',
scale: 1,
} }
} }
@ -567,6 +569,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
} }
component(shape: TLArrowShape) { component(shape: TLArrowShape) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const theme = useDefaultColorTheme()
const onlySelectedShape = this.editor.getOnlySelectedShape() const onlySelectedShape = this.editor.getOnlySelectedShape()
const shouldDisplayHandles = const shouldDisplayHandles =
this.editor.isInAny( this.editor.isInAny(
@ -594,15 +598,23 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
/> />
</SVGContainer> </SVGContainer>
{showArrowLabel && ( {showArrowLabel && (
<ArrowTextLabel <TextLabel
id={shape.id} id={shape.id}
text={shape.props.text} classNamePrefix="tl-arrow"
type="arrow"
font={shape.props.font} font={shape.props.font}
size={shape.props.size} fontSize={getArrowLabelFontSize(shape)}
position={labelPosition.box.center} lineHeight={TEXT_PROPS.lineHeight}
width={labelPosition.box.w} align="middle"
verticalAlign="middle"
text={shape.props.text}
labelColor={theme[shape.props.labelColor].solid}
textWidth={labelPosition.box.w}
isSelected={isSelected} isSelected={isSelected}
labelColor={shape.props.labelColor} padding={0}
style={{
transform: `translate(${labelPosition.box.center.x}px, ${labelPosition.box.center.y}px)`,
}}
/> />
)} )}
</> </>
@ -624,7 +636,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
if (Vec.Equals(start, end)) return null if (Vec.Equals(start, end)) return null
const strokeWidth = STROKE_SIZES[shape.props.size] const strokeWidth = STROKE_SIZES[shape.props.size] * shape.props.scale
const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth) const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth)
const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth) const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth)
@ -645,8 +657,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
y={toDomPrecision(labelGeometry.y)} y={toDomPrecision(labelGeometry.y)}
width={labelGeometry.w} width={labelGeometry.w}
height={labelGeometry.h} height={labelGeometry.h}
rx={3.5} rx={3.5 * shape.props.scale}
ry={3.5} ry={3.5 * shape.props.scale}
/> />
) )
} }
@ -670,8 +682,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
width={labelGeometry.w} width={labelGeometry.w}
height={labelGeometry.h} height={labelGeometry.h}
fill="black" fill="black"
rx={3.5} rx={3.5 * shape.props.scale}
ry={3.5} ry={3.5 * shape.props.scale}
/> />
)} )}
{as && ( {as && (
@ -746,21 +758,22 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
ctx.addExportDef(getFillDefForExport(shape.props.fill)) ctx.addExportDef(getFillDefForExport(shape.props.fill))
if (shape.props.text) ctx.addExportDef(getFontDefForExport(shape.props.font)) if (shape.props.text) ctx.addExportDef(getFontDefForExport(shape.props.font))
const theme = getDefaultColorTheme(ctx) const theme = getDefaultColorTheme(ctx)
const scaleFactor = 1 / shape.props.scale
return ( return (
<> <g transform={`scale(${scaleFactor})`}>
<ArrowSvg shape={shape} shouldDisplayHandles={false} /> <ArrowSvg shape={shape} shouldDisplayHandles={false} />
<SvgTextLabel <SvgTextLabel
fontSize={ARROW_LABEL_FONT_SIZES[shape.props.size]} fontSize={getArrowLabelFontSize(shape)}
font={shape.props.font} font={shape.props.font}
align="middle" align="middle"
verticalAlign="middle" verticalAlign="middle"
text={shape.props.text} text={shape.props.text}
labelColor={theme[shape.props.labelColor].solid} labelColor={theme[shape.props.labelColor].solid}
bounds={getArrowLabelPosition(this.editor, shape).box} bounds={getArrowLabelPosition(this.editor, shape).box}
padding={4} padding={4 * shape.props.scale}
/> />
</> </g>
) )
} }
@ -807,7 +820,7 @@ const ArrowSvg = track(function ArrowSvg({
if (!info?.isValid) return null if (!info?.isValid) return null
const strokeWidth = STROKE_SIZES[shape.props.size] const strokeWidth = STROKE_SIZES[shape.props.size] * shape.props.scale
const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth) const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth)
const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth) const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth)
@ -817,7 +830,7 @@ const ArrowSvg = track(function ArrowSvg({
let handlePath: null | React.JSX.Element = null let handlePath: null | React.JSX.Element = null
if (shouldDisplayHandles) { if (shouldDisplayHandles) {
const sw = 2 const sw = 2 / editor.getZoomLevel()
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
getLength(editor, shape), getLength(editor, shape),
sw, sw,
@ -928,10 +941,22 @@ const ArrowSvg = track(function ArrowSvg({
<path d={path} strokeDasharray={strokeDasharray} strokeDashoffset={strokeDashoffset} /> <path d={path} strokeDasharray={strokeDasharray} strokeDashoffset={strokeDashoffset} />
</g> </g>
{as && maskStartArrowhead && shape.props.fill !== 'none' && ( {as && maskStartArrowhead && shape.props.fill !== 'none' && (
<ShapeFill theme={theme} d={as} color={shape.props.color} fill={shape.props.fill} /> <ShapeFill
theme={theme}
d={as}
color={shape.props.color}
fill={shape.props.fill}
scale={shape.props.scale}
/>
)} )}
{ae && maskEndArrowhead && shape.props.fill !== 'none' && ( {ae && maskEndArrowhead && shape.props.fill !== 'none' && (
<ShapeFill theme={theme} d={ae} color={shape.props.color} fill={shape.props.fill} /> <ShapeFill
theme={theme}
d={ae}
color={shape.props.color}
fill={shape.props.fill}
scale={shape.props.scale}
/>
)} )}
{as && <path d={as} />} {as && <path d={as} />}
{ae && <path d={ae} />} {ae && <path d={ae} />}

View file

@ -52,10 +52,12 @@ function getArrowLabelSize(editor: Editor, shape: TLArrowShape) {
if (shape.props.text.trim()) { if (shape.props.text.trim()) {
const bodyBounds = bodyGeom.bounds const bodyBounds = bodyGeom.bounds
const fontSize = getArrowLabelFontSize(shape)
const { w, h } = editor.textMeasure.measureText(shape.props.text, { const { w, h } = editor.textMeasure.measureText(shape.props.text, {
...TEXT_PROPS, ...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font], fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size], fontSize,
maxWidth: null, maxWidth: null,
}) })
@ -70,7 +72,7 @@ function getArrowLabelSize(editor: Editor, shape: TLArrowShape) {
{ {
...TEXT_PROPS, ...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font], fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size], fontSize,
maxWidth: width, maxWidth: width,
} }
) )
@ -79,15 +81,15 @@ function getArrowLabelSize(editor: Editor, shape: TLArrowShape) {
height = squishedHeight height = squishedHeight
} }
if (width > 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]) { if (width > 16 * fontSize) {
width = 16 * ARROW_LABEL_FONT_SIZES[shape.props.size] width = 16 * fontSize
const { w: squishedWidth, h: squishedHeight } = editor.textMeasure.measureText( const { w: squishedWidth, h: squishedHeight } = editor.textMeasure.measureText(
shape.props.text, shape.props.text,
{ {
...TEXT_PROPS, ...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font], fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size], fontSize,
maxWidth: width, maxWidth: width,
} }
) )
@ -97,17 +99,18 @@ function getArrowLabelSize(editor: Editor, shape: TLArrowShape) {
} }
} }
const size = new Vec(width, height).addScalar(ARROW_LABEL_PADDING * 2) const size = new Vec(width, height).addScalar(ARROW_LABEL_PADDING * 2 * shape.props.scale)
labelSizeCache.set(shape, size) labelSizeCache.set(shape, size)
return size return size
} }
function getLabelToArrowPadding(editor: Editor, shape: TLArrowShape) { function getLabelToArrowPadding(shape: TLArrowShape) {
const strokeWidth = STROKE_SIZES[shape.props.size] const strokeWidth = STROKE_SIZES[shape.props.size]
const labelToArrowPadding = const labelToArrowPadding =
LABEL_TO_ARROW_PADDING + (LABEL_TO_ARROW_PADDING +
(strokeWidth - STROKE_SIZES.s) * 2 + (strokeWidth - STROKE_SIZES.s) * 2 +
(strokeWidth === STROKE_SIZES.xl ? 20 : 0) (strokeWidth === STROKE_SIZES.xl ? 20 : 0)) *
shape.props.scale
return labelToArrowPadding return labelToArrowPadding
} }
@ -122,7 +125,7 @@ function getStraightArrowLabelRange(
info: Extract<TLArrowInfo, { isStraight: true }> info: Extract<TLArrowInfo, { isStraight: true }>
): { start: number; end: number } { ): { start: number; end: number } {
const labelSize = getArrowLabelSize(editor, shape) const labelSize = getArrowLabelSize(editor, shape)
const labelToArrowPadding = getLabelToArrowPadding(editor, shape) const labelToArrowPadding = getLabelToArrowPadding(shape)
// take the start and end points of the arrow, and nudge them in a bit to give some spare space: // take the start and end points of the arrow, and nudge them in a bit to give some spare space:
const startOffset = Vec.Nudge(info.start.point, info.end.point, labelToArrowPadding) const startOffset = Vec.Nudge(info.start.point, info.end.point, labelToArrowPadding)
@ -165,7 +168,7 @@ function getCurvedArrowLabelRange(
info: Extract<TLArrowInfo, { isStraight: false }> info: Extract<TLArrowInfo, { isStraight: false }>
): { start: number; end: number; dbg?: Geometry2d[] } { ): { start: number; end: number; dbg?: Geometry2d[] } {
const labelSize = getArrowLabelSize(editor, shape) const labelSize = getArrowLabelSize(editor, shape)
const labelToArrowPadding = getLabelToArrowPadding(editor, shape) const labelToArrowPadding = getLabelToArrowPadding(shape)
const direction = Math.sign(shape.props.bend) const direction = Math.sign(shape.props.bend)
// take the start and end points of the arrow, and nudge them in a bit to give some spare space: // take the start and end points of the arrow, and nudge them in a bit to give some spare space:
@ -351,3 +354,7 @@ function interpolateArcAngles(angleStart: number, angleEnd: number, direction: n
const dist = angleDistance(angleStart, angleEnd, direction) const dist = angleDistance(angleStart, angleEnd, direction)
return angleStart + dist * t * direction * -1 return angleStart + dist * t * direction * -1
} }
export function getArrowLabelFontSize(shape: TLArrowShape) {
return ARROW_LABEL_FONT_SIZES[shape.props.size] * shape.props.scale
}

View file

@ -1,43 +0,0 @@
import { TLArrowShape, TLDefaultColorStyle, TLShapeId, VecLike } from '@tldraw/editor'
import * as React from 'react'
import { useDefaultColorTheme } from '../../shared/ShapeFill'
import { TextLabel } from '../../shared/TextLabel'
import { ARROW_LABEL_FONT_SIZES, TEXT_PROPS } from '../../shared/default-shape-constants'
export const ArrowTextLabel = React.memo(function ArrowTextLabel({
id,
text,
size,
font,
position,
width,
isSelected,
labelColor,
}: {
id: TLShapeId
position: VecLike
width?: number
labelColor: TLDefaultColorStyle
isSelected: boolean
} & Pick<TLArrowShape['props'], 'text' | 'size' | 'font'>) {
const theme = useDefaultColorTheme()
return (
<TextLabel
id={id}
classNamePrefix="tl-arrow"
type="arrow"
font={font}
fontSize={ARROW_LABEL_FONT_SIZES[size]}
lineHeight={TEXT_PROPS.lineHeight}
align="middle"
verticalAlign="middle"
text={text}
labelColor={theme[labelColor].solid}
textWidth={width}
isSelected={isSelected}
style={{
transform: `translate(${position.x}px, ${position.y}px)`,
}}
/>
)
})

View file

@ -34,7 +34,10 @@ export function getCurvedArrowInfo(
const { arrowheadEnd, arrowheadStart } = shape.props const { arrowheadEnd, arrowheadStart } = shape.props
const bend = shape.props.bend const bend = shape.props.bend
if (Math.abs(bend) > Math.abs(shape.props.bend * WAY_TOO_BIG_ARROW_BEND_FACTOR)) { if (
Math.abs(bend) >
Math.abs(shape.props.bend * (WAY_TOO_BIG_ARROW_BEND_FACTOR * shape.props.scale))
) {
return getStraightArrowInfo(editor, shape, bindings) return getStraightArrowInfo(editor, shape, bindings)
} }
@ -101,7 +104,7 @@ export function getCurvedArrowInfo(
let offsetA = 0 let offsetA = 0
let offsetB = 0 let offsetB = 0
let minLength = MIN_ARROW_LENGTH let minLength = MIN_ARROW_LENGTH * shape.props.scale
if (startShapeInfo && !startShapeInfo.isExact) { if (startShapeInfo && !startShapeInfo.isExact) {
const startInPageSpace = Mat.applyToPoint(arrowPageTransform, tempA) const startInPageSpace = Mat.applyToPoint(arrowPageTransform, tempA)
@ -165,8 +168,8 @@ export function getCurvedArrowInfo(
('size' in startShapeInfo.shape.props ('size' in startShapeInfo.shape.props
? STROKE_SIZES[startShapeInfo.shape.props.size] / 2 ? STROKE_SIZES[startShapeInfo.shape.props.size] / 2
: 0) : 0)
offsetA = BOUND_ARROW_OFFSET + strokeOffset offsetA = (BOUND_ARROW_OFFSET + strokeOffset) * shape.props.scale
minLength += strokeOffset minLength += strokeOffset * shape.props.scale
} }
} }
} }
@ -237,8 +240,8 @@ export function getCurvedArrowInfo(
const strokeOffset = const strokeOffset =
STROKE_SIZES[shape.props.size] / 2 + STROKE_SIZES[shape.props.size] / 2 +
('size' in endShapeInfo.shape.props ? STROKE_SIZES[endShapeInfo.shape.props.size] / 2 : 0) ('size' in endShapeInfo.shape.props ? STROKE_SIZES[endShapeInfo.shape.props.size] / 2 : 0)
offsetB = BOUND_ARROW_OFFSET + strokeOffset offsetB = (BOUND_ARROW_OFFSET + strokeOffset) * shape.props.scale
minLength += strokeOffset minLength += strokeOffset * shape.props.scale
} }
} }
} }
@ -257,15 +260,15 @@ export function getCurvedArrowInfo(
const tB = tempB.clone() const tB = tempB.clone()
if (offsetA !== 0) { if (offsetA !== 0) {
const n = (offsetA / lAB) * (isClockwise ? 1 : -1) tA.setTo(handleArc.center).add(
const u = Vec.FromAngle(aCA + dAB * n) Vec.FromAngle(aCA + dAB * ((offsetA / lAB) * (isClockwise ? 1 : -1))).mul(handleArc.radius)
tA.setTo(handleArc.center).add(u.mul(handleArc.radius)) )
} }
if (offsetB !== 0) { if (offsetB !== 0) {
const n = (offsetB / lAB) * (isClockwise ? -1 : 1) tB.setTo(handleArc.center).add(
const u = Vec.FromAngle(aCB + dAB * n) Vec.FromAngle(aCB + dAB * ((offsetB / lAB) * (isClockwise ? -1 : 1))).mul(handleArc.radius)
tB.setTo(handleArc.center).add(u.mul(handleArc.radius)) )
} }
if (Vec.DistMin(tA, tB, minLength)) { if (Vec.DistMin(tA, tB, minLength)) {
@ -282,15 +285,19 @@ export function getCurvedArrowInfo(
} }
if (offsetA !== 0) { if (offsetA !== 0) {
const n = (offsetA / lAB) * (isClockwise ? 1 : -1) tempA
const u = Vec.FromAngle(aCA + dAB * n) .setTo(handleArc.center)
tempA.setTo(handleArc.center).add(u.mul(handleArc.radius)) .add(
Vec.FromAngle(aCA + dAB * ((offsetA / lAB) * (isClockwise ? 1 : -1))).mul(handleArc.radius)
)
} }
if (offsetB !== 0) { if (offsetB !== 0) {
const n = (offsetB / lAB) * (isClockwise ? -1 : 1) tempB
const u = Vec.FromAngle(aCB + dAB * n) .setTo(handleArc.center)
tempB.setTo(handleArc.center).add(u.mul(handleArc.radius)) .add(
Vec.FromAngle(aCB + dAB * ((offsetB / lAB) * (isClockwise ? -1 : 1))).mul(handleArc.radius)
)
} }
// Did we miss intersections? This happens when we have overlapping shapes. // Did we miss intersections? This happens when we have overlapping shapes.
@ -318,9 +325,16 @@ export function getCurvedArrowInfo(
(endShapeInfo && !endShapeInfo.didIntersect) || (endShapeInfo && !endShapeInfo.didIntersect) ||
distFn(handle_aCA, aCA) > distFn(handle_aCA, aCB) distFn(handle_aCA, aCA) > distFn(handle_aCA, aCB)
) { ) {
const n = Math.min(0.9, MIN_ARROW_LENGTH / lAB) * (isClockwise ? 1 : -1) tempB
const u = Vec.FromAngle(aCA + dAB * n) .setTo(handleArc.center)
tempB.setTo(handleArc.center).add(u.mul(handleArc.radius)) .add(
Vec.FromAngle(
aCA +
dAB *
(Math.min(0.9, (MIN_ARROW_LENGTH * shape.props.scale) / lAB) *
(isClockwise ? 1 : -1))
).mul(handleArc.radius)
)
} }
} }
} }
@ -421,9 +435,7 @@ function placeCenterHandle(
let dAB = clockwiseAngleDist(aCA, aCB) // angle distance between a and b let dAB = clockwiseAngleDist(aCA, aCB) // angle distance between a and b
if (!isClockwise) dAB = PI2 - dAB if (!isClockwise) dAB = PI2 - dAB
const n = 0.5 * (isClockwise ? 1 : -1) tempC.setTo(center).add(Vec.FromAngle(aCA + dAB * (0.5 * (isClockwise ? 1 : -1))).mul(radius))
const u = Vec.FromAngle(aCA + dAB * n)
tempC.setTo(center).add(u.mul(radius))
if (dAB > originalArcLength) { if (dAB > originalArcLength) {
tempC.rotWith(center, PI) tempC.rotWith(center, PI)

View file

@ -13,8 +13,10 @@ import { createComputedCache } from '@tldraw/store'
import { getCurvedArrowInfo } from './curved-arrow' import { getCurvedArrowInfo } from './curved-arrow'
import { getStraightArrowInfo } from './straight-arrow' import { getStraightArrowInfo } from './straight-arrow'
const MIN_ARROW_BEND = 8
export function getIsArrowStraight(shape: TLArrowShape) { export function getIsArrowStraight(shape: TLArrowShape) {
return Math.abs(shape.props.bend) < 8 // snap to +-8px return Math.abs(shape.props.bend) < MIN_ARROW_BEND * shape.props.scale // snap to +-8px
} }
export interface BoundShapeInfo<T extends TLShape = TLShape> { export interface BoundShapeInfo<T extends TLShape = TLShape> {

View file

@ -82,7 +82,7 @@ export function getStraightArrowInfo(
let offsetB = 0 let offsetB = 0
let strokeOffsetA = 0 let strokeOffsetA = 0
let strokeOffsetB = 0 let strokeOffsetB = 0
let minLength = MIN_ARROW_LENGTH let minLength = MIN_ARROW_LENGTH * shape.props.scale
const isSelfIntersection = const isSelfIntersection =
startShapeInfo && endShapeInfo && startShapeInfo.shape === endShapeInfo.shape startShapeInfo && endShapeInfo && startShapeInfo.shape === endShapeInfo.shape
@ -105,14 +105,14 @@ export function getStraightArrowInfo(
// a short arrow ending at the end shape intersection. // a short arrow ending at the end shape intersection.
if (startShapeInfo.isClosed) { if (startShapeInfo.isClosed) {
a.setTo(b.clone().add(uAB.clone().mul(MIN_ARROW_LENGTH))) a.setTo(b.clone().add(uAB.clone().mul(MIN_ARROW_LENGTH * shape.props.scale)))
} }
} else if (!endShapeInfo.didIntersect) { } else if (!endShapeInfo.didIntersect) {
// ...and if only the end shape intersected, or if neither // ...and if only the end shape intersected, or if neither
// shape intersected, then make it a short arrow starting // shape intersected, then make it a short arrow starting
// at the start shape intersection. // at the start shape intersection.
if (endShapeInfo.isClosed) { if (endShapeInfo.isClosed) {
b.setTo(a.clone().sub(uAB.clone().mul(MIN_ARROW_LENGTH))) b.setTo(a.clone().sub(uAB.clone().mul(MIN_ARROW_LENGTH * shape.props.scale)))
} }
} }
} }
@ -136,8 +136,8 @@ export function getStraightArrowInfo(
('size' in startShapeInfo.shape.props ('size' in startShapeInfo.shape.props
? STROKE_SIZES[startShapeInfo.shape.props.size] / 2 ? STROKE_SIZES[startShapeInfo.shape.props.size] / 2
: 0) : 0)
offsetA = BOUND_ARROW_OFFSET + strokeOffsetA offsetA = (BOUND_ARROW_OFFSET + strokeOffsetA) * shape.props.scale
minLength += strokeOffsetA minLength += strokeOffsetA * shape.props.scale
} }
// If the arrow is bound non-exact to an end shape and the // If the arrow is bound non-exact to an end shape and the
@ -151,8 +151,8 @@ export function getStraightArrowInfo(
strokeOffsetB = strokeOffsetB =
STROKE_SIZES[shape.props.size] / 2 + STROKE_SIZES[shape.props.size] / 2 +
('size' in endShapeInfo.shape.props ? STROKE_SIZES[endShapeInfo.shape.props.size] / 2 : 0) ('size' in endShapeInfo.shape.props ? STROKE_SIZES[endShapeInfo.shape.props.size] / 2 : 0)
offsetB = BOUND_ARROW_OFFSET + strokeOffsetB offsetB = (BOUND_ARROW_OFFSET + strokeOffsetB) * shape.props.scale
minLength += strokeOffsetB minLength += strokeOffsetB * shape.props.scale
} }
} }
@ -187,7 +187,7 @@ export function getStraightArrowInfo(
if (startShapeInfo && endShapeInfo) { if (startShapeInfo && endShapeInfo) {
// If we have two bound shapes...then make the arrow a short arrow from // If we have two bound shapes...then make the arrow a short arrow from
// the start point towards where the end point should be. // the start point towards where the end point should be.
b.setTo(Vec.Add(a, u.clone().mul(-MIN_ARROW_LENGTH))) b.setTo(Vec.Add(a, u.clone().mul(-MIN_ARROW_LENGTH * shape.props.scale)))
} }
c.setTo(Vec.Med(terminalsInArrowSpace.start, terminalsInArrowSpace.end)) c.setTo(Vec.Med(terminalsInArrowSpace.start, terminalsInArrowSpace.end))
} else { } else {

View file

@ -90,14 +90,15 @@ export class Pointing extends StateNode {
this.markId = `creating:${id}` this.markId = `creating:${id}`
this.editor.mark(this.markId) this.editor.mark(this.markId)
this.editor.createShapes<TLArrowShape>([ this.editor.createShape<TLArrowShape>({
{ id,
id, type: 'arrow',
type: 'arrow', x: originPagePoint.x,
x: originPagePoint.x, y: originPagePoint.y,
y: originPagePoint.y, props: {
scale: this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1,
}, },
]) })
const shape = this.editor.getShape<TLArrowShape>(id) const shape = this.editor.getShape<TLArrowShape>(id)
if (!shape) throw Error(`expected shape`) if (!shape) throw Error(`expected shape`)

View file

@ -1,4 +1,3 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { import {
Box, Box,
Circle2d, Circle2d,
@ -18,13 +17,14 @@ import {
rng, rng,
toFixed, toFixed,
} from '@tldraw/editor' } from '@tldraw/editor'
import { ShapeFill, useDefaultColorTheme } from '../shared/ShapeFill'
import { ShapeFill } from '../shared/ShapeFill'
import { STROKE_SIZES } from '../shared/default-shape-constants' import { STROKE_SIZES } from '../shared/default-shape-constants'
import { getFillDefForCanvas, getFillDefForExport } from '../shared/defaultStyleDefs' import { getFillDefForCanvas, getFillDefForExport } from '../shared/defaultStyleDefs'
import { getStrokePoints } from '../shared/freehand/getStrokePoints' import { getStrokePoints } from '../shared/freehand/getStrokePoints'
import { getSvgPathFromStrokePoints } from '../shared/freehand/svg' import { getSvgPathFromStrokePoints } from '../shared/freehand/svg'
import { svgInk } from '../shared/freehand/svgInk' import { svgInk } from '../shared/freehand/svgInk'
import { useForceSolid } from '../shared/useForceSolid' import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
import { getDrawShapeStrokeDashArray, getFreehandOptions, getPointsFromSegments } from './getPath' import { getDrawShapeStrokeDashArray, getFreehandOptions, getPointsFromSegments } from './getPath'
/** @public */ /** @public */
@ -47,21 +47,23 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
isComplete: false, isComplete: false,
isClosed: false, isClosed: false,
isPen: false, isPen: false,
scale: 1,
} }
} }
getGeometry(shape: TLDrawShape) { getGeometry(shape: TLDrawShape) {
const points = getPointsFromSegments(shape.props.segments) const points = getPointsFromSegments(shape.props.segments)
const strokeWidth = STROKE_SIZES[shape.props.size]
const sw = (STROKE_SIZES[shape.props.size] + 1) * shape.props.scale
// A dot // A dot
if (shape.props.segments.length === 1) { if (shape.props.segments.length === 1) {
const box = Box.FromPoints(points) const box = Box.FromPoints(points)
if (box.width < strokeWidth * 2 && box.height < strokeWidth * 2) { if (box.width < sw * 2 && box.height < sw * 2) {
return new Circle2d({ return new Circle2d({
x: -strokeWidth, x: -sw,
y: -strokeWidth, y: -sw,
radius: strokeWidth, radius: sw,
isFilled: true, isFilled: true,
}) })
} }
@ -69,7 +71,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
const strokePoints = getStrokePoints( const strokePoints = getStrokePoints(
points, points,
getFreehandOptions(shape.props, strokeWidth, true, true) getFreehandOptions(shape.props, sw, shape.props.isPen, true)
).map((p) => p.point) ).map((p) => p.point)
// A closed draw stroke // A closed draw stroke
@ -89,24 +91,25 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
component(shape: TLDrawShape) { component(shape: TLDrawShape) {
return ( return (
<SVGContainer id={shape.id}> <SVGContainer id={shape.id}>
<DrawShapeSvg shape={shape} forceSolid={useForceSolid()} /> <DrawShapeSvg shape={shape} zoomLevel={this.editor.getZoomLevel()} />
</SVGContainer> </SVGContainer>
) )
} }
indicator(shape: TLDrawShape) { indicator(shape: TLDrawShape) {
const forceSolid = useForceSolid()
const strokeWidth = STROKE_SIZES[shape.props.size]
const allPointsFromSegments = getPointsFromSegments(shape.props.segments) const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
let sw = strokeWidth let sw = (STROKE_SIZES[shape.props.size] + 1) * shape.props.scale
const zoomLevel = this.editor.getZoomLevel()
const forceSolid = zoomLevel < 0.5 && zoomLevel < 1.5 / sw
if ( if (
!forceSolid && !forceSolid &&
!shape.props.isPen && !shape.props.isPen &&
shape.props.dash === 'draw' && shape.props.dash === 'draw' &&
allPointsFromSegments.length === 1 allPointsFromSegments.length === 1
) { ) {
sw += rng(shape.id)() * (strokeWidth / 6) sw += rng(shape.id)() * (sw / 6)
} }
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight' const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
@ -122,7 +125,12 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
override toSvg(shape: TLDrawShape, ctx: SvgExportContext) { override toSvg(shape: TLDrawShape, ctx: SvgExportContext) {
ctx.addExportDef(getFillDefForExport(shape.props.fill)) ctx.addExportDef(getFillDefForExport(shape.props.fill))
return <DrawShapeSvg shape={shape} forceSolid={false} /> const scaleFactor = 1 / shape.props.scale
return (
<g transform={`scale(${scaleFactor})`}>
<DrawShapeSvg shape={shape} zoomLevel={1} />
</g>
)
} }
override getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] { override getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] {
@ -156,7 +164,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
override expandSelectionOutlinePx(shape: TLDrawShape): number { override expandSelectionOutlinePx(shape: TLDrawShape): number {
const multiplier = shape.props.dash === 'draw' ? 1.6 : 1 const multiplier = shape.props.dash === 'draw' ? 1.6 : 1
return (STROKE_SIZES[shape.props.size] * multiplier) / 2 return ((STROKE_SIZES[shape.props.size] * multiplier) / 2) * shape.props.scale
} }
} }
@ -171,21 +179,23 @@ function getIsDot(shape: TLDrawShape) {
return shape.props.segments.length === 1 && shape.props.segments[0].points.length < 2 return shape.props.segments.length === 1 && shape.props.segments[0].points.length < 2
} }
function DrawShapeSvg({ shape, forceSolid }: { shape: TLDrawShape; forceSolid: boolean }) { function DrawShapeSvg({ shape, zoomLevel }: { shape: TLDrawShape; zoomLevel: number }) {
const theme = useDefaultColorTheme() const theme = useDefaultColorTheme()
const strokeWidth = STROKE_SIZES[shape.props.size]
const allPointsFromSegments = getPointsFromSegments(shape.props.segments) const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight' const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
let sw = strokeWidth let sw = (STROKE_SIZES[shape.props.size] + 1) * shape.props.scale
const forceSolid = zoomLevel < 0.5 && zoomLevel < 1.5 / sw
if ( if (
!forceSolid && !forceSolid &&
!shape.props.isPen && !shape.props.isPen &&
shape.props.dash === 'draw' && shape.props.dash === 'draw' &&
allPointsFromSegments.length === 1 allPointsFromSegments.length === 1
) { ) {
sw += rng(shape.id)() * (strokeWidth / 6) sw += rng(shape.id)() * (sw / 6)
} }
const options = getFreehandOptions(shape.props, sw, showAsComplete, forceSolid) const options = getFreehandOptions(shape.props, sw, showAsComplete, forceSolid)
@ -195,13 +205,14 @@ function DrawShapeSvg({ shape, forceSolid }: { shape: TLDrawShape; forceSolid: b
<> <>
{shape.props.isClosed && shape.props.fill && allPointsFromSegments.length > 1 ? ( {shape.props.isClosed && shape.props.fill && allPointsFromSegments.length > 1 ? (
<ShapeFill <ShapeFill
theme={theme}
fill={shape.props.isClosed ? shape.props.fill : 'none'}
color={shape.props.color}
d={getSvgPathFromStrokePoints( d={getSvgPathFromStrokePoints(
getStrokePoints(allPointsFromSegments, options), getStrokePoints(allPointsFromSegments, options),
shape.props.isClosed shape.props.isClosed
)} )}
theme={theme}
color={shape.props.color}
fill={shape.props.isClosed ? shape.props.fill : 'none'}
scale={shape.props.scale}
/> />
) : null} ) : null}
<path <path
@ -222,18 +233,19 @@ function DrawShapeSvg({ shape, forceSolid }: { shape: TLDrawShape; forceSolid: b
return ( return (
<> <>
<ShapeFill <ShapeFill
d={solidStrokePath}
theme={theme} theme={theme}
color={shape.props.color} color={shape.props.color}
fill={isDot || shape.props.isClosed ? shape.props.fill : 'none'} fill={isDot || shape.props.isClosed ? shape.props.fill : 'none'}
d={solidStrokePath} scale={shape.props.scale}
/> />
<path <path
d={solidStrokePath} d={solidStrokePath}
strokeLinecap="round" strokeLinecap="round"
fill={isDot ? theme[shape.props.color].solid : 'none'} fill={isDot ? theme[shape.props.color].solid : 'none'}
stroke={theme[shape.props.color].solid} stroke={theme[shape.props.color].solid}
strokeWidth={strokeWidth} strokeWidth={sw}
strokeDasharray={isDot ? 'none' : getDrawShapeStrokeDashArray(shape, strokeWidth)} strokeDasharray={isDot ? 'none' : getDrawShapeStrokeDashArray(shape, sw)}
strokeDashoffset="0" strokeDashoffset="0"
/> />
</> </>

View file

@ -6,6 +6,7 @@ import {
TLDrawShape, TLDrawShape,
TLDrawShapeSegment, TLDrawShapeSegment,
Vec, Vec,
modulate,
} from '@tldraw/editor' } from '@tldraw/editor'
import { StrokeOptions } from '../shared/freehand/types' import { StrokeOptions } from '../shared/freehand/types'
@ -13,9 +14,9 @@ const PEN_EASING = (t: number) => t * 0.65 + SIN((t * PI) / 2) * 0.35
const simulatePressureSettings = (strokeWidth: number): StrokeOptions => { const simulatePressureSettings = (strokeWidth: number): StrokeOptions => {
return { return {
size: 1 + strokeWidth, size: strokeWidth,
thinning: 0.5, thinning: 0.5,
streamline: 0.62 + ((1 + strokeWidth) / 8) * 0.06, streamline: modulate(strokeWidth, [9, 16], [0.64, 0.74], true), // 0.62 + ((1 + strokeWidth) / 8) * 0.06,
smoothing: 0.62, smoothing: 0.62,
easing: EASINGS.easeOutSine, easing: EASINGS.easeOutSine,
simulatePressure: true, simulatePressure: true,
@ -35,9 +36,9 @@ const realPressureSettings = (strokeWidth: number): StrokeOptions => {
const solidSettings = (strokeWidth: number): StrokeOptions => { const solidSettings = (strokeWidth: number): StrokeOptions => {
return { return {
size: 1 + strokeWidth, size: strokeWidth,
thinning: 0, thinning: 0,
streamline: 0.62 + ((1 + strokeWidth) / 8) * 0.06, streamline: modulate(strokeWidth, [9, 16], [0.68, 0.74], true), // 0.62 + ((1 + strokeWidth) / 8) * 0.06,
smoothing: 0.62, smoothing: 0.62,
simulatePressure: false, simulatePressure: false,
easing: EASINGS.linear, easing: EASINGS.linear,

View file

@ -268,6 +268,7 @@ export class Drawing extends StateNode {
y: originPagePoint.y, y: originPagePoint.y,
props: { props: {
isPen: this.isPenOrStylus, isPen: this.isPenOrStylus,
scale: this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1,
segments: [ segments: [
{ {
type: this.segmentMode, type: this.segmentMode,
@ -415,7 +416,13 @@ export class Drawing extends StateNode {
// ended and where the pointer is now // ended and where the pointer is now
const newFreeSegment: TLDrawShapeSegment = { const newFreeSegment: TLDrawShapeSegment = {
type: 'free', type: 'free',
points: [...Vec.PointsBetween(prevPoint, newPoint, 6).map((p) => p.toFixed().toJson())], points: [
...Vec.PointsBetween(prevPoint, newPoint, 6).map((p) => ({
x: toFixed(p.x),
y: toFixed(p.y),
z: toFixed(p.z),
})),
],
} }
const finalSegments = [...newSegments, newFreeSegment] const finalSegments = [...newSegments, newFreeSegment]

View file

@ -18,8 +18,8 @@ import {
useValue, useValue,
} from '@tldraw/editor' } from '@tldraw/editor'
import classNames from 'classnames' import classNames from 'classnames'
import { useDefaultColorTheme } from '../shared/ShapeFill'
import { createTextJsxFromSpans } from '../shared/createTextJsxFromSpans' import { createTextJsxFromSpans } from '../shared/createTextJsxFromSpans'
import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
import { FrameHeading } from './components/FrameHeading' import { FrameHeading } from './components/FrameHeading'
export function defaultEmptyAs(str: string, dflt: string) { export function defaultEmptyAs(str: string, dflt: string) {

View file

@ -51,6 +51,55 @@ describe(GeoShapeTool, () => {
expect(editor.getCurrentPageShapes().length).toBe(1) expect(editor.getCurrentPageShapes().length).toBe(1)
}) })
it('Creates geo shapes when scaled', () => {
editor.setCurrentTool('geo')
editor.pointerMove(50, 50)
editor.pointerDown()
editor.pointerMove(100, 100)
editor.pointerUp()
expect(editor.getLastCreatedShape()).toMatchObject({
props: {
w: 50,
h: 50,
scale: 1,
},
})
editor.user.updateUserPreferences({ isDynamicSizeMode: true })
editor.zoomIn() // 1 -> 2
editor.setCurrentTool('geo')
editor.pointerMove(50, 50)
editor.pointerDown()
editor.pointerMove(100, 100)
editor.pointerUp()
expect(editor.getLastCreatedShape()).toMatchObject({
props: {
w: 25,
h: 25,
scale: 0.5,
},
})
editor.zoomOut().zoomOut() // 2 -> 1 -> .5
editor.setCurrentTool('geo')
editor.pointerMove(50, 50)
editor.pointerDown()
editor.pointerMove(100, 100)
editor.pointerUp()
expect(editor.getLastCreatedShape()).toMatchObject({
props: {
w: 100,
h: 100,
scale: 2,
},
})
})
}) })
describe('When selecting the tool', () => { describe('When selecting the tool', () => {

View file

@ -1,6 +1,7 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { import {
BaseBoxShapeUtil, BaseBoxShapeUtil,
Box,
Editor, Editor,
Ellipse2d, Ellipse2d,
Geometry2d, Geometry2d,
@ -28,7 +29,6 @@ import {
} from '@tldraw/editor' } from '@tldraw/editor'
import { HyperlinkButton } from '../shared/HyperlinkButton' import { HyperlinkButton } from '../shared/HyperlinkButton'
import { useDefaultColorTheme } from '../shared/ShapeFill'
import { SvgTextLabel } from '../shared/SvgTextLabel' import { SvgTextLabel } from '../shared/SvgTextLabel'
import { TextLabel } from '../shared/TextLabel' import { TextLabel } from '../shared/TextLabel'
import { import {
@ -43,6 +43,7 @@ import {
getFillDefForExport, getFillDefForExport,
getFontDefForExport, getFontDefForExport,
} from '../shared/defaultStyleDefs' } from '../shared/defaultStyleDefs'
import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
import { GeoShapeBody } from './components/GeoShapeBody' import { GeoShapeBody } from './components/GeoShapeBody'
import { import {
cloudOutline, cloudOutline,
@ -81,6 +82,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
verticalAlign: 'middle', verticalAlign: 'middle',
growY: 0, growY: 0,
url: '', url: '',
scale: 1,
} }
} }
@ -90,7 +92,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
const cx = w / 2 const cx = w / 2
const cy = h / 2 const cy = h / 2
const strokeWidth = STROKE_SIZES[shape.props.size] const strokeWidth = STROKE_SIZES[shape.props.size] * shape.props.scale
const isFilled = shape.props.fill !== 'none' // || shape.props.text.trim().length > 0 const isFilled = shape.props.fill !== 'none' // || shape.props.text.trim().length > 0
let body: Geometry2d let body: Geometry2d
@ -318,12 +320,16 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
const labelSize = getLabelSize(this.editor, shape) const labelSize = getLabelSize(this.editor, shape)
const minWidth = Math.min(100, w / 2) const minWidth = Math.min(100, w / 2)
const labelWidth = Math.min(w, Math.max(labelSize.w, Math.min(minWidth, Math.max(1, w - 8))))
const minHeight = Math.min( const minHeight = Math.min(
LABEL_FONT_SIZES[shape.props.size] * TEXT_PROPS.lineHeight + LABEL_PADDING * 2, LABEL_FONT_SIZES[shape.props.size] * shape.props.scale * TEXT_PROPS.lineHeight +
LABEL_PADDING * 2,
h / 2 h / 2
) )
const labelHeight = Math.min(h, Math.max(labelSize.h, Math.min(minHeight, Math.max(1, w - 8)))) // not sure if bug
const labelWidth = Math.min(w, Math.max(labelSize.w, Math.min(minWidth, Math.max(1, w - 8))))
const labelHeight = Math.min(h, Math.max(labelSize.h, Math.min(minHeight, Math.max(1, w - 8))))
// not sure if bug
const lines = getLines(shape.props, strokeWidth) const lines = getLines(shape.props, strokeWidth)
const edges = lines ? lines.map((line) => new Polyline2d({ points: line })) : [] const edges = lines ? lines.map((line) => new Polyline2d({ points: line })) : []
@ -421,7 +427,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
return ( return (
<> <>
<SVGContainer id={id}> <SVGContainer id={id}>
<GeoShapeBody shape={shape} /> <GeoShapeBody shape={shape} shouldScale={true} />
</SVGContainer> </SVGContainer>
{showHtmlContainer && ( {showHtmlContainer && (
<HTMLContainer <HTMLContainer
@ -435,8 +441,9 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
id={id} id={id}
type={type} type={type}
font={font} font={font}
fontSize={LABEL_FONT_SIZES[size]} fontSize={LABEL_FONT_SIZES[size] * shape.props.scale}
lineHeight={TEXT_PROPS.lineHeight} lineHeight={TEXT_PROPS.lineHeight}
padding={16 * shape.props.scale}
fill={fill} fill={fill}
align={align} align={align}
verticalAlign={verticalAlign} verticalAlign={verticalAlign}
@ -488,7 +495,13 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
let path: string let path: string
if (props.dash === 'draw') { if (props.dash === 'draw') {
const polygonPoints = getRoundedPolygonPoints(id, outline, 0, strokeWidth * 2, 1) const polygonPoints = getRoundedPolygonPoints(
id,
outline,
0,
strokeWidth * 2 * shape.props.scale,
1
)
path = getRoundedInkyPolygonPath(polygonPoints) path = getRoundedInkyPolygonPath(polygonPoints)
} else { } else {
path = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z' path = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
@ -508,15 +521,24 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
} }
override toSvg(shape: TLGeoShape, ctx: SvgExportContext) { override toSvg(shape: TLGeoShape, ctx: SvgExportContext) {
const { props } = shape // We need to scale the shape to 1x for export
ctx.addExportDef(getFillDefForExport(shape.props.fill)) const newShape = {
...shape,
props: {
...shape.props,
w: shape.props.w / shape.props.scale,
h: shape.props.h / shape.props.scale,
},
}
const props = newShape.props
ctx.addExportDef(getFillDefForExport(props.fill))
let textEl let textEl
if (props.text) { if (props.text) {
ctx.addExportDef(getFontDefForExport(shape.props.font)) ctx.addExportDef(getFontDefForExport(props.font))
const theme = getDefaultColorTheme(ctx) const theme = getDefaultColorTheme(ctx)
const bounds = this.editor.getShapeGeometry(shape).bounds const bounds = new Box(0, 0, props.w, props.h + props.growY)
textEl = ( textEl = (
<SvgTextLabel <SvgTextLabel
fontSize={LABEL_FONT_SIZES[props.size]} fontSize={LABEL_FONT_SIZES[props.size]}
@ -526,13 +548,14 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
text={props.text} text={props.text}
labelColor={theme[props.labelColor].solid} labelColor={theme[props.labelColor].solid}
bounds={bounds} bounds={bounds}
padding={16}
/> />
) )
} }
return ( return (
<> <>
<GeoShapeBody shape={shape} /> <GeoShapeBody shouldScale={false} shape={newShape} />
{textEl} {textEl}
</> </>
) )
@ -782,8 +805,8 @@ function getLabelSize(editor: Editor, shape: TLGeoShape) {
const minSize = editor.textMeasure.measureText('w', { const minSize = editor.textMeasure.measureText('w', {
...TEXT_PROPS, ...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font], fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: LABEL_FONT_SIZES[shape.props.size], fontSize: LABEL_FONT_SIZES[shape.props.size] * shape.props.scale,
maxWidth: 100, maxWidth: 100, // ?
}) })
// TODO: Can I get these from somewhere? // TODO: Can I get these from somewhere?
@ -797,7 +820,7 @@ function getLabelSize(editor: Editor, shape: TLGeoShape) {
const size = editor.textMeasure.measureText(text, { const size = editor.textMeasure.measureText(text, {
...TEXT_PROPS, ...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font], fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: LABEL_FONT_SIZES[shape.props.size], fontSize: LABEL_FONT_SIZES[shape.props.size] * shape.props.scale,
minWidth: minSize.w, minWidth: minSize.w,
maxWidth: Math.max( maxWidth: Math.max(
// Guard because a DOM nodes can't be less 0 // Guard because a DOM nodes can't be less 0

View file

@ -1,7 +1,8 @@
import { Group2d, TLGeoShape, Vec, canonicalizeRotation, useEditor } from '@tldraw/editor' import { Group2d, TLGeoShape, Vec, canonicalizeRotation, useEditor } from '@tldraw/editor'
import { ShapeFill, useDefaultColorTheme } from '../../shared/ShapeFill' import { ShapeFill } from '../../shared/ShapeFill'
import { STROKE_SIZES } from '../../shared/default-shape-constants' import { STROKE_SIZES } from '../../shared/default-shape-constants'
import { getPerfectDashProps } from '../../shared/getPerfectDashProps' import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
import { useDefaultColorTheme } from '../../shared/useDefaultColorTheme'
import { import {
getCloudArcs, getCloudArcs,
getCloudPath, getCloudPath,
@ -14,12 +15,13 @@ import {
} from '../geo-shape-helpers' } from '../geo-shape-helpers'
import { getLines } from '../getLines' import { getLines } from '../getLines'
export function GeoShapeBody({ shape }: { shape: TLGeoShape }) { export function GeoShapeBody({ shape, shouldScale }: { shape: TLGeoShape; shouldScale: boolean }) {
const scaleToUse = shouldScale ? shape.props.scale : 1
const editor = useEditor() const editor = useEditor()
const theme = useDefaultColorTheme() const theme = useDefaultColorTheme()
const { id, props } = shape const { id, props } = shape
const { w, color, fill, dash, growY, size } = props const { w, color, fill, dash, growY, size } = props
const strokeWidth = STROKE_SIZES[size] const strokeWidth = STROKE_SIZES[size] * scaleToUse
const h = props.h + growY const h = props.h + growY
switch (props.geo) { switch (props.geo) {
@ -28,7 +30,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
const d = getCloudPath(w, h, id, size) const d = getCloudPath(w, h, id, size)
return ( return (
<> <>
<ShapeFill theme={theme} d={d} color={color} fill={fill} /> <ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" /> <path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
</> </>
) )
@ -36,17 +38,17 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
const d = inkyCloudSvgPath(w, h, id, size) const d = inkyCloudSvgPath(w, h, id, size)
return ( return (
<> <>
<ShapeFill theme={theme} d={d} color={color} fill={fill} /> <ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" /> <path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
</> </>
) )
} else { } else {
const innerPath = getCloudPath(w, h, id, size) const d = getCloudPath(w, h, id, size)
const arcs = getCloudArcs(w, h, id, size) const arcs = getCloudArcs(w, h, id, size)
return ( return (
<> <>
<ShapeFill theme={theme} d={innerPath} color={color} fill={fill} /> <ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
<g <g
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
stroke={theme[color].solid} stroke={theme[color].solid}
@ -91,7 +93,11 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
} }
} }
case 'ellipse': { case 'ellipse': {
const geometry = editor.getShapeGeometry(shape) const geometry = shouldScale
? // cached
editor.getShapeGeometry(shape)
: // not cached
editor.getShapeUtil(shape).getGeometry(shape)
const d = geometry.getSvgPathData(true) const d = geometry.getSvgPathData(true)
if (dash === 'dashed' || dash === 'dotted') { if (dash === 'dashed' || dash === 'dotted') {
@ -108,7 +114,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
return ( return (
<> <>
<ShapeFill theme={theme} d={d} color={color} fill={fill} /> <ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
<path <path
d={d} d={d}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
@ -120,18 +126,26 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
</> </>
) )
} else { } else {
const geometry = editor.getShapeGeometry(shape) const geometry = shouldScale
? // cached
editor.getShapeGeometry(shape)
: // not cached
editor.getShapeUtil(shape).getGeometry(shape)
const d = geometry.getSvgPathData(true) const d = geometry.getSvgPathData(true)
return ( return (
<> <>
<ShapeFill theme={theme} d={d} color={color} fill={fill} /> <ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" /> <path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
</> </>
) )
} }
} }
case 'oval': { case 'oval': {
const geometry = editor.getShapeGeometry(shape) const geometry = shouldScale
? // cached
editor.getShapeGeometry(shape)
: // not cached
editor.getShapeUtil(shape).getGeometry(shape)
const d = geometry.getSvgPathData(true) const d = geometry.getSvgPathData(true)
if (dash === 'dashed' || dash === 'dotted') { if (dash === 'dashed' || dash === 'dotted') {
const perimeter = geometry.getLength() const perimeter = geometry.getLength()
@ -149,7 +163,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
return ( return (
<> <>
<ShapeFill theme={theme} d={d} color={color} fill={fill} /> <ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
<path <path
d={d} d={d}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
@ -163,7 +177,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
} else { } else {
return ( return (
<> <>
<ShapeFill theme={theme} d={d} color={color} fill={fill} /> <ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" /> <path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
</> </>
) )
@ -176,7 +190,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
return ( return (
<> <>
<ShapeFill theme={theme} d={d} color={color} fill={fill} /> <ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
{curves.map((c, i) => { {curves.map((c, i) => {
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
c.length, c.length,
@ -208,14 +222,19 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
const d = getDrawHeartPath(w, h, strokeWidth, shape.id) const d = getDrawHeartPath(w, h, strokeWidth, shape.id)
return ( return (
<> <>
<ShapeFill d={d} color={color} fill={fill} theme={theme} /> <ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" /> <path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
</> </>
) )
} }
} }
default: { default: {
const geometry = editor.getShapeGeometry(shape) const geometry = shouldScale
? // cached
editor.getShapeGeometry(shape)
: // not cached
editor.getShapeUtil(shape).getGeometry(shape)
const outline = const outline =
geometry instanceof Group2d ? geometry.children[0].vertices : geometry.vertices geometry instanceof Group2d ? geometry.children[0].vertices : geometry.vertices
const lines = getLines(shape.props, strokeWidth) const lines = getLines(shape.props, strokeWidth)
@ -231,16 +250,16 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
return ( return (
<> <>
<ShapeFill theme={theme} d={d} color={color} fill={fill} /> <ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" /> <path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
</> </>
) )
} else if (dash === 'dashed' || dash === 'dotted') { } else if (dash === 'dashed' || dash === 'dotted') {
const innerPath = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z' const d = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
return ( return (
<> <>
<ShapeFill theme={theme} d={innerPath} fill={fill} color={color} /> <ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
<g <g
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
stroke={theme[color].solid} stroke={theme[color].solid}
@ -304,14 +323,9 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
</> </>
) )
} else if (dash === 'draw') { } else if (dash === 'draw') {
const polygonPoints = getRoundedPolygonPoints( let d = getRoundedInkyPolygonPath(
id, getRoundedPolygonPoints(id, outline, strokeWidth / 3, strokeWidth * 2, 2)
outline,
strokeWidth / 3,
strokeWidth * 2,
2
) )
let d = getRoundedInkyPolygonPath(polygonPoints)
if (lines) { if (lines) {
for (const [A, B] of lines) { for (const [A, B] of lines) {
@ -319,12 +333,19 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
} }
} }
const innerPolygonPoints = getRoundedPolygonPoints(id, outline, 0, strokeWidth * 2, 1) const innerPathData = getRoundedInkyPolygonPath(
const innerPathData = getRoundedInkyPolygonPath(innerPolygonPoints) getRoundedPolygonPoints(id, outline, 0, strokeWidth * 2, 1)
)
return ( return (
<> <>
<ShapeFill d={innerPathData} fill={fill} color={color} theme={theme} /> <ShapeFill
theme={theme}
d={innerPathData}
color={color}
fill={fill}
scale={scaleToUse}
/>
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" /> <path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
</> </>
) )

View file

@ -1,9 +1,9 @@
import { import {
Box,
GeoShapeGeoStyle, GeoShapeGeoStyle,
StateNode, StateNode,
TLEventHandlers, TLEventHandlers,
TLGeoShape, TLGeoShape,
Vec,
createShapeId, createShapeId,
} from '@tldraw/editor' } from '@tldraw/editor'
@ -37,6 +37,7 @@ export class Pointing extends StateNode {
w: 1, w: 1,
h: 1, h: 1,
geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle), geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle),
scale: this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1,
}, },
}, },
]) ])
@ -73,6 +74,17 @@ export class Pointing extends StateNode {
this.editor.mark(this.markId) this.editor.mark(this.markId)
const scale = this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1
const geo = this.editor.getStyleForNextShape(GeoShapeGeoStyle)
const size =
geo === 'star'
? { w: 200, h: 190 }
: geo === 'cloud'
? { w: 300, h: 180 }
: { w: 200, h: 200 }
this.editor.createShapes<TLGeoShape>([ this.editor.createShapes<TLGeoShape>([
{ {
id, id,
@ -81,8 +93,8 @@ export class Pointing extends StateNode {
y: originPagePoint.y, y: originPagePoint.y,
props: { props: {
geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle), geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle),
w: 1, scale,
h: 1, ...size,
}, },
}, },
]) ])
@ -90,31 +102,24 @@ export class Pointing extends StateNode {
const shape = this.editor.getShape<TLGeoShape>(id)! const shape = this.editor.getShape<TLGeoShape>(id)!
if (!shape) return if (!shape) return
const bounds = const { w, h } = shape.props
shape.props.geo === 'star'
? new Box(0, 0, 200, 190)
: shape.props.geo === 'cloud'
? new Box(0, 0, 300, 180)
: new Box(0, 0, 200, 200)
const delta = bounds.center const delta = new Vec(w / 2, h / 2).mul(scale)
const parentTransform = this.editor.getShapeParentTransform(shape) const parentTransform = this.editor.getShapeParentTransform(shape)
if (parentTransform) delta.rot(-parentTransform.rotation()) if (parentTransform) delta.rot(-parentTransform.rotation())
this.editor.select(id) this.editor.select(id)
this.editor.updateShapes<TLGeoShape>([ this.editor.updateShape<TLGeoShape>({
{ id: shape.id,
id: shape.id, type: 'geo',
type: 'geo', x: shape.x - delta.x,
x: shape.x - delta.x, y: shape.y - delta.y,
y: shape.y - delta.y, props: {
props: { geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle),
geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle), w: w * scale,
w: bounds.width, h: h * scale,
h: bounds.height,
},
}, },
]) })
if (this.editor.getInstanceState().isToolLocked) { if (this.editor.getInstanceState().isToolLocked) {
this.parent.transition('idle') this.parent.transition('idle')

View file

@ -1,6 +1,7 @@
/* eslint-disable react-hooks/rules-of-hooks */ /* eslint-disable react-hooks/rules-of-hooks */
import { import {
Circle2d, Circle2d,
Editor,
Polygon2d, Polygon2d,
SVGContainer, SVGContainer,
ShapeUtil, ShapeUtil,
@ -12,16 +13,16 @@ import {
highlightShapeProps, highlightShapeProps,
last, last,
rng, rng,
useValue,
} from '@tldraw/editor' } from '@tldraw/editor'
import { getHighlightFreehandSettings, getPointsFromSegments } from '../draw/getPath' import { getHighlightFreehandSettings, getPointsFromSegments } from '../draw/getPath'
import { useDefaultColorTheme } from '../shared/ShapeFill'
import { FONT_SIZES } from '../shared/default-shape-constants' import { FONT_SIZES } from '../shared/default-shape-constants'
import { getStrokeOutlinePoints } from '../shared/freehand/getStrokeOutlinePoints' import { getStrokeOutlinePoints } from '../shared/freehand/getStrokeOutlinePoints'
import { getStrokePoints } from '../shared/freehand/getStrokePoints' import { getStrokePoints } from '../shared/freehand/getStrokePoints'
import { setStrokePointRadii } from '../shared/freehand/setStrokePointRadii' import { setStrokePointRadii } from '../shared/freehand/setStrokePointRadii'
import { getSvgPathFromStrokePoints } from '../shared/freehand/svg' import { getSvgPathFromStrokePoints } from '../shared/freehand/svg'
import { useColorSpace } from '../shared/useColorSpace' import { useColorSpace } from '../shared/useColorSpace'
import { useForceSolid } from '../shared/useForceSolid' import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
const OVERLAY_OPACITY = 0.35 const OVERLAY_OPACITY = 0.35
const UNDERLAY_OPACITY = 0.82 const UNDERLAY_OPACITY = 0.82
@ -43,6 +44,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
size: 'm', size: 'm',
isComplete: false, isComplete: false,
isPen: false, isPen: false,
scale: 1,
} }
} }
@ -68,38 +70,43 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
} }
component(shape: TLHighlightShape) { component(shape: TLHighlightShape) {
const forceSolid = useHighlightForceSolid(this.editor, shape)
const strokeWidth = getStrokeWidth(shape)
return ( return (
<SVGContainer id={shape.id} style={{ opacity: OVERLAY_OPACITY }}> <SVGContainer id={shape.id}>
<HighlightRenderer strokeWidth={getStrokeWidth(shape)} shape={shape} /> <HighlightRenderer
shape={shape}
forceSolid={forceSolid}
strokeWidth={strokeWidth}
opacity={OVERLAY_OPACITY}
/>
</SVGContainer> </SVGContainer>
) )
} }
override backgroundComponent(shape: TLHighlightShape) { override backgroundComponent(shape: TLHighlightShape) {
const forceSolid = useHighlightForceSolid(this.editor, shape)
const strokeWidth = getStrokeWidth(shape)
return ( return (
<SVGContainer id={shape.id} style={{ opacity: UNDERLAY_OPACITY }}> <SVGContainer id={shape.id}>
<HighlightRenderer strokeWidth={getStrokeWidth(shape)} shape={shape} /> <HighlightRenderer
shape={shape}
forceSolid={forceSolid}
strokeWidth={strokeWidth}
opacity={UNDERLAY_OPACITY}
/>
</SVGContainer> </SVGContainer>
) )
} }
indicator(shape: TLHighlightShape) { indicator(shape: TLHighlightShape) {
const forceSolid = useForceSolid() const forceSolid = useHighlightForceSolid(this.editor, shape)
const strokeWidth = getStrokeWidth(shape) const strokeWidth = getStrokeWidth(shape)
const { strokePoints, sw } = getHighlightStrokePoints(shape, strokeWidth, forceSolid)
const allPointsFromSegments = getPointsFromSegments(shape.props.segments) const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
let sw = strokeWidth
if (!forceSolid && !shape.props.isPen && allPointsFromSegments.length === 1) {
sw += rng(shape.id)() * (strokeWidth / 6)
}
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
const options = getHighlightFreehandSettings({
strokeWidth,
showAsComplete,
})
const strokePoints = getStrokePoints(allPointsFromSegments, options)
let strokePath let strokePath
if (strokePoints.length < 2) { if (strokePoints.length < 2) {
strokePath = getIndicatorDot(allPointsFromSegments[0], sw) strokePath = getIndicatorDot(allPointsFromSegments[0], sw)
@ -111,22 +118,34 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
} }
override toSvg(shape: TLHighlightShape) { override toSvg(shape: TLHighlightShape) {
const strokeWidth = getStrokeWidth(shape)
const forceSolid = strokeWidth < 1.5
const scaleFactor = 1 / shape.props.scale
return ( return (
<HighlightRenderer <g transform={`scale(${scaleFactor})`}>
strokeWidth={getStrokeWidth(shape)} <HighlightRenderer
shape={shape} forceSolid={forceSolid}
opacity={OVERLAY_OPACITY} strokeWidth={strokeWidth}
/> shape={shape}
opacity={OVERLAY_OPACITY}
/>
</g>
) )
} }
override toBackgroundSvg(shape: TLHighlightShape) { override toBackgroundSvg(shape: TLHighlightShape) {
const strokeWidth = getStrokeWidth(shape)
const forceSolid = strokeWidth < 1.5
const scaleFactor = 1 / shape.props.scale
return ( return (
<HighlightRenderer <g transform={`scale(${scaleFactor})`}>
strokeWidth={getStrokeWidth(shape)} <HighlightRenderer
shape={shape} forceSolid={forceSolid}
opacity={UNDERLAY_OPACITY} strokeWidth={strokeWidth}
/> shape={shape}
opacity={UNDERLAY_OPACITY}
/>
</g>
) )
} }
@ -187,34 +206,52 @@ function getHighlightStrokePoints(
strokeWidth: sw, strokeWidth: sw,
showAsComplete, showAsComplete,
}) })
const strokePoints = getStrokePoints(allPointsFromSegments, options) const strokePoints = getStrokePoints(allPointsFromSegments, options)
return { strokePoints, sw } return { strokePoints, sw }
} }
function getHighlightSvgPath(shape: TLHighlightShape, strokeWidth: number, forceSolid: boolean) { function getStrokeWidth(shape: TLHighlightShape) {
const { strokePoints, sw } = getHighlightStrokePoints(shape, strokeWidth, forceSolid) return FONT_SIZES[shape.props.size] * 1.12 * shape.props.scale
}
function getIsDot(shape: TLHighlightShape) {
return shape.props.segments.length === 1 && shape.props.segments[0].points.length < 2
}
function HighlightRenderer({
strokeWidth,
forceSolid,
shape,
opacity,
}: {
strokeWidth: number
forceSolid: boolean
shape: TLHighlightShape
opacity: number
}) {
const theme = useDefaultColorTheme()
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
let sw = strokeWidth
if (!forceSolid && !shape.props.isPen && allPointsFromSegments.length === 1) {
sw += rng(shape.id)() * (sw / 6)
}
const options = getHighlightFreehandSettings({
strokeWidth: sw,
showAsComplete: shape.props.isComplete || last(shape.props.segments)?.type === 'straight',
})
const strokePoints = getStrokePoints(allPointsFromSegments, options)
const solidStrokePath = const solidStrokePath =
strokePoints.length > 1 strokePoints.length > 1
? getSvgPathFromStrokePoints(strokePoints, false) ? getSvgPathFromStrokePoints(strokePoints, false)
: getShapeDot(shape.props.segments[0].points[0]) : getShapeDot(shape.props.segments[0].points[0])
return { solidStrokePath, sw }
}
function HighlightRenderer({
strokeWidth,
shape,
opacity,
}: {
strokeWidth: number
shape: TLHighlightShape
opacity?: number
}) {
const theme = useDefaultColorTheme()
const forceSolid = useForceSolid()
const { solidStrokePath, sw } = getHighlightSvgPath(shape, strokeWidth, forceSolid)
const colorSpace = useColorSpace() const colorSpace = useColorSpace()
const color = theme[shape.props.color].highlight[colorSpace] const color = theme[shape.props.color].highlight[colorSpace]
@ -231,10 +268,17 @@ function HighlightRenderer({
) )
} }
function getStrokeWidth(shape: TLHighlightShape) { function useHighlightForceSolid(editor: Editor, shape: TLHighlightShape) {
return FONT_SIZES[shape.props.size] * 1.12 return useValue(
} 'forceSolid',
() => {
function getIsDot(shape: TLHighlightShape) { const sw = getStrokeWidth(shape)
return shape.props.segments.length === 1 && shape.props.segments[0].points.length < 2 const zoomLevel = editor.getZoomLevel()
if (sw / zoomLevel < 1.5) {
return true
}
return false
},
[editor]
)
} }

View file

@ -19,9 +19,9 @@ import {
sortByIndex, sortByIndex,
} from '@tldraw/editor' } from '@tldraw/editor'
import { ShapeFill, useDefaultColorTheme } from '../shared/ShapeFill'
import { STROKE_SIZES } from '../shared/default-shape-constants' import { STROKE_SIZES } from '../shared/default-shape-constants'
import { getPerfectDashProps } from '../shared/getPerfectDashProps' import { getPerfectDashProps } from '../shared/getPerfectDashProps'
import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
import { getLineDrawPath, getLineIndicatorPath } from './components/getLinePath' import { getLineDrawPath, getLineIndicatorPath } from './components/getLinePath'
import { getDrawLinePathData } from './line-helpers' import { getDrawLinePathData } from './line-helpers'
@ -49,6 +49,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
[start]: { id: start, index: start, x: 0, y: 0 }, [start]: { id: start, index: start, x: 0, y: 0 },
[end]: { id: end, index: end, x: 0.1, y: 0.1 }, [end]: { id: end, index: end, x: 0.1, y: 0.1 },
}, },
scale: 1,
} }
} }
@ -129,7 +130,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
} }
indicator(shape: TLLineShape) { indicator(shape: TLLineShape) {
const strokeWidth = STROKE_SIZES[shape.props.size] const strokeWidth = STROKE_SIZES[shape.props.size] * shape.props.scale
const spline = getGeometryForLineShape(shape) const spline = getGeometryForLineShape(shape)
const { dash } = shape.props const { dash } = shape.props
@ -151,7 +152,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
} }
override toSvg(shape: TLLineShape) { override toSvg(shape: TLLineShape) {
return <LineShapeSvg shape={shape} /> return <LineShapeSvg shouldScale shape={shape} />
} }
override getHandleSnapGeometry(shape: TLLineShape): HandleSnapGeometry { override getHandleSnapGeometry(shape: TLLineShape): HandleSnapGeometry {
@ -204,12 +205,23 @@ export function getGeometryForLineShape(shape: TLLineShape): CubicSpline2d | Pol
} }
} }
function LineShapeSvg({ shape }: { shape: TLLineShape }) { function LineShapeSvg({
shape,
shouldScale = false,
}: {
shape: TLLineShape
shouldScale?: boolean
}) {
const theme = useDefaultColorTheme() const theme = useDefaultColorTheme()
const spline = getGeometryForLineShape(shape)
const strokeWidth = STROKE_SIZES[shape.props.size]
const { dash, color } = shape.props const spline = getGeometryForLineShape(shape)
const { dash, color, size } = shape.props
const scaleFactor = 1 / shape.props.scale
const scale = shouldScale ? scaleFactor : 1
const strokeWidth = STROKE_SIZES[size] * shape.props.scale
// Line style lines // Line style lines
if (shape.props.spline === 'line') { if (shape.props.spline === 'line') {
@ -218,61 +230,56 @@ function LineShapeSvg({ shape }: { shape: TLLineShape }) {
const pathData = 'M' + outline[0] + 'L' + outline.slice(1) const pathData = 'M' + outline[0] + 'L' + outline.slice(1)
return ( return (
<> <path
<ShapeFill d={pathData} fill={'none'} color={color} theme={theme} /> d={pathData}
<path d={pathData} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" /> stroke={theme[color].solid}
</> strokeWidth={strokeWidth}
fill="none"
transform={`scale(${scale})`}
/>
) )
} }
if (dash === 'dashed' || dash === 'dotted') { if (dash === 'dashed' || dash === 'dotted') {
const outline = spline.points
const pathData = 'M' + outline[0] + 'L' + outline.slice(1)
return ( return (
<> <g stroke={theme[color].solid} strokeWidth={strokeWidth} transform={`scale(${scale})`}>
<ShapeFill d={pathData} fill={'none'} color={color} theme={theme} /> {spline.segments.map((segment, i) => {
<g stroke={theme[color].solid} strokeWidth={strokeWidth}> const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
{spline.segments.map((segment, i) => { segment.length,
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( strokeWidth,
segment.length, {
strokeWidth, style: dash,
{ start: i > 0 ? 'outset' : 'none',
style: dash, end: i < spline.segments.length - 1 ? 'outset' : 'none',
start: i > 0 ? 'outset' : 'none', }
end: i < spline.segments.length - 1 ? 'outset' : 'none', )
}
)
return ( return (
<path <path
key={i} key={i}
strokeDasharray={strokeDasharray} strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset} strokeDashoffset={strokeDashoffset}
d={segment.getSvgPathData(true)} d={segment.getSvgPathData(true)}
fill="none" fill="none"
/> />
) )
})} })}
</g> </g>
</>
) )
} }
if (dash === 'draw') { if (dash === 'draw') {
const outline = spline.points const outline = spline.points
const [innerPathData, outerPathData] = getDrawLinePathData(shape.id, outline, strokeWidth) const [_, outerPathData] = getDrawLinePathData(shape.id, outline, strokeWidth)
return ( return (
<> <path
<ShapeFill d={innerPathData} fill={'none'} color={color} theme={theme} /> d={outerPathData}
<path stroke={theme[color].solid}
d={outerPathData} strokeWidth={strokeWidth}
stroke={theme[color].solid} fill="none"
strokeWidth={strokeWidth} transform={`scale(${scale})`}
fill="none" />
/>
</>
) )
} }
} }
@ -281,55 +288,53 @@ function LineShapeSvg({ shape }: { shape: TLLineShape }) {
const splinePath = spline.getSvgPathData() const splinePath = spline.getSvgPathData()
if (dash === 'solid') { if (dash === 'solid') {
return ( return (
<> <path
<ShapeFill d={splinePath} fill={'none'} color={color} theme={theme} /> strokeWidth={strokeWidth}
<path strokeWidth={strokeWidth} stroke={theme[color].solid} fill="none" d={splinePath} /> stroke={theme[color].solid}
</> fill="none"
d={splinePath}
transform={`scale(${scale})`}
/>
) )
} }
if (dash === 'dashed' || dash === 'dotted') { if (dash === 'dashed' || dash === 'dotted') {
return ( return (
<> <g stroke={theme[color].solid} strokeWidth={strokeWidth} transform={`scale(${scale})`}>
<ShapeFill d={splinePath} fill={'none'} color={color} theme={theme} /> {spline.segments.map((segment, i) => {
<g stroke={theme[color].solid} strokeWidth={strokeWidth}> const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
{spline.segments.map((segment, i) => { segment.length,
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( strokeWidth,
segment.length, {
strokeWidth, style: dash,
{ start: i > 0 ? 'outset' : 'none',
style: dash, end: i < spline.segments.length - 1 ? 'outset' : 'none',
start: i > 0 ? 'outset' : 'none', }
end: i < spline.segments.length - 1 ? 'outset' : 'none', )
}
)
return ( return (
<path <path
key={i} key={i}
strokeDasharray={strokeDasharray} strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset} strokeDashoffset={strokeDashoffset}
d={segment.getSvgPathData()} d={segment.getSvgPathData()}
fill="none" fill="none"
/> />
) )
})} })}
</g> </g>
</>
) )
} }
if (dash === 'draw') { if (dash === 'draw') {
return ( return (
<> <path
<ShapeFill d={splinePath} fill={'none'} color={color} theme={theme} /> d={getLineDrawPath(shape, spline, strokeWidth)}
<path strokeWidth={1}
d={getLineDrawPath(shape, spline, strokeWidth)} stroke={theme[color].solid}
strokeWidth={1} fill={theme[color].solid}
stroke={theme[color].solid} transform={`scale(${scale})`}
fill={theme[color].solid} />
/>
</>
) )
} }
} }

View file

@ -25,6 +25,7 @@ exports[`Misc resizes: line shape after resize 1`] = `
"y": 700, "y": 700,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "line", "spline": "line",
}, },

View file

@ -95,6 +95,9 @@ export class Pointing extends StateNode {
type: 'line', type: 'line',
x: currentPagePoint.x, x: currentPagePoint.x,
y: currentPagePoint.y, y: currentPagePoint.y,
props: {
scale: this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1,
},
}, },
]) ])

View file

@ -1,4 +1,6 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { import {
Box,
Editor, Editor,
Group2d, Group2d,
IndexKey, IndexKey,
@ -24,7 +26,6 @@ import { useCallback } from 'react'
import { useCurrentTranslation } from '../../ui/hooks/useTranslation/useTranslation' import { useCurrentTranslation } from '../../ui/hooks/useTranslation/useTranslation'
import { isRightToLeftLanguage } from '../../utils/text/text' import { isRightToLeftLanguage } from '../../utils/text/text'
import { HyperlinkButton } from '../shared/HyperlinkButton' import { HyperlinkButton } from '../shared/HyperlinkButton'
import { useDefaultColorTheme } from '../shared/ShapeFill'
import { SvgTextLabel } from '../shared/SvgTextLabel' import { SvgTextLabel } from '../shared/SvgTextLabel'
import { TextLabel } from '../shared/TextLabel' import { TextLabel } from '../shared/TextLabel'
import { import {
@ -35,8 +36,8 @@ import {
} from '../shared/default-shape-constants' } from '../shared/default-shape-constants'
import { getFontDefForExport } from '../shared/defaultStyleDefs' import { getFontDefForExport } from '../shared/defaultStyleDefs'
import { useDefaultColorTheme } from '../../..'
import { startEditingShapeWithLabel } from '../../tools/SelectTool/selectHelpers' import { startEditingShapeWithLabel } from '../../tools/SelectTool/selectHelpers'
import { useForceSolid } from '../shared/useForceSolid'
import { import {
CLONE_HANDLE_MARGIN, CLONE_HANDLE_MARGIN,
NOTE_CENTER_OFFSET, NOTE_CENTER_OFFSET,
@ -65,31 +66,37 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
growY: 0, growY: 0,
fontSizeAdjustment: 0, fontSizeAdjustment: 0,
url: '', url: '',
scale: 1,
} }
} }
getGeometry(shape: TLNoteShape) { getGeometry(shape: TLNoteShape) {
const noteHeight = getNoteHeight(shape)
const { labelHeight, labelWidth } = getLabelSize(this.editor, shape) const { labelHeight, labelWidth } = getLabelSize(this.editor, shape)
const { scale } = shape.props
const lh = labelHeight * scale
const lw = labelWidth * scale
const nw = NOTE_SIZE * scale
const nh = getNoteHeight(shape)
return new Group2d({ return new Group2d({
children: [ children: [
new Rectangle2d({ width: NOTE_SIZE, height: noteHeight, isFilled: true }), new Rectangle2d({ width: nw, height: nh, isFilled: true }),
new Rectangle2d({ new Rectangle2d({
x: x:
shape.props.align === 'start' shape.props.align === 'start'
? 0 ? 0
: shape.props.align === 'end' : shape.props.align === 'end'
? NOTE_SIZE - labelWidth ? nw - lw
: (NOTE_SIZE - labelWidth) / 2, : (nw - lw) / 2,
y: y:
shape.props.verticalAlign === 'start' shape.props.verticalAlign === 'start'
? 0 ? 0
: shape.props.verticalAlign === 'end' : shape.props.verticalAlign === 'end'
? noteHeight - labelHeight ? nh - lh
: (noteHeight - labelHeight) / 2, : (nh - lh) / 2,
width: labelWidth, width: lw,
height: labelHeight, height: lh,
isFilled: true, isFilled: true,
isLabel: true, isLabel: true,
}), }),
@ -98,21 +105,25 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
} }
override getHandles(shape: TLNoteShape): TLHandle[] { override getHandles(shape: TLNoteShape): TLHandle[] {
const zoom = this.editor.getZoomLevel() const { scale } = shape.props
const offset = CLONE_HANDLE_MARGIN / zoom
const noteHeight = getNoteHeight(shape)
const isCoarsePointer = this.editor.getInstanceState().isCoarsePointer const isCoarsePointer = this.editor.getInstanceState().isCoarsePointer
if (isCoarsePointer) return []
if (zoom < 0.25 || isCoarsePointer) return [] const zoom = this.editor.getZoomLevel()
if (zoom * scale < 0.25) return []
if (zoom < 0.5) { const nh = getNoteHeight(shape)
const nw = NOTE_SIZE * scale
const offset = (CLONE_HANDLE_MARGIN / zoom) * scale
if (zoom * scale < 0.5) {
return [ return [
{ {
id: 'bottom', id: 'bottom',
index: 'a3' as IndexKey, index: 'a3' as IndexKey,
type: 'clone', type: 'clone',
x: NOTE_SIZE / 2, x: nw / 2,
y: noteHeight + offset, y: nh + offset,
}, },
] ]
} }
@ -122,29 +133,29 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
id: 'top', id: 'top',
index: 'a1' as IndexKey, index: 'a1' as IndexKey,
type: 'clone', type: 'clone',
x: NOTE_SIZE / 2, x: nw / 2,
y: -offset, y: -offset,
}, },
{ {
id: 'right', id: 'right',
index: 'a2' as IndexKey, index: 'a2' as IndexKey,
type: 'clone', type: 'clone',
x: NOTE_SIZE + offset, x: nw + offset,
y: noteHeight / 2, y: nh / 2,
}, },
{ {
id: 'bottom', id: 'bottom',
index: 'a3' as IndexKey, index: 'a3' as IndexKey,
type: 'clone', type: 'clone',
x: NOTE_SIZE / 2, x: nw / 2,
y: noteHeight + offset, y: nh + offset,
}, },
{ {
id: 'left', id: 'left',
index: 'a4' as IndexKey, index: 'a4' as IndexKey,
type: 'clone', type: 'clone',
x: -offset, x: -offset,
y: noteHeight / 2, y: nh / 2,
}, },
] ]
} }
@ -153,17 +164,15 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
const { const {
id, id,
type, type,
props: { color, font, size, align, text, verticalAlign, fontSizeAdjustment }, props: { scale, color, font, size, align, text, verticalAlign, fontSizeAdjustment },
} = shape } = shape
// eslint-disable-next-line react-hooks/rules-of-hooks
const handleKeyDown = useNoteKeydownHandler(id) const handleKeyDown = useNoteKeydownHandler(id)
// eslint-disable-next-line react-hooks/rules-of-hooks
const theme = useDefaultColorTheme() const theme = useDefaultColorTheme()
const noteHeight = getNoteHeight(shape) const nw = NOTE_SIZE * scale
const nh = getNoteHeight(shape)
// eslint-disable-next-line react-hooks/rules-of-hooks
const rotation = useValue( const rotation = useValue(
'shape rotation', 'shape rotation',
() => this.editor.getShapePageTransform(id)?.rotation() ?? 0, () => this.editor.getShapePageTransform(id)?.rotation() ?? 0,
@ -171,8 +180,11 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
) )
// todo: consider hiding shadows on dark mode if they're invisible anyway // todo: consider hiding shadows on dark mode if they're invisible anyway
// eslint-disable-next-line react-hooks/rules-of-hooks
const hideShadows = useForceSolid() const hideShadows = useValue('zoom', () => this.editor.getZoomLevel() < 0.35 / scale, [
scale,
this.editor,
])
const isSelected = shape.id === this.editor.getOnlySelectedShapeId() const isSelected = shape.id === this.editor.getOnlySelectedShapeId()
@ -182,18 +194,18 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
id={id} id={id}
className="tl-note__container" className="tl-note__container"
style={{ style={{
width: NOTE_SIZE, width: nw,
height: noteHeight, height: nh,
backgroundColor: theme[color].note.fill, backgroundColor: theme[color].note.fill,
borderBottom: hideShadows ? `3px solid rgb(15, 23, 31, .2)` : `none`, borderBottom: hideShadows ? `${3 * scale}px solid rgb(15, 23, 31, .2)` : `none`,
boxShadow: hideShadows ? 'none' : getNoteShadow(shape.id, rotation), boxShadow: hideShadows ? 'none' : getNoteShadow(shape.id, rotation, scale),
}} }}
> >
<TextLabel <TextLabel
id={id} id={id}
type={type} type={type}
font={font} font={font}
fontSize={fontSizeAdjustment || LABEL_FONT_SIZES[size]} fontSize={(fontSizeAdjustment || LABEL_FONT_SIZES[size]) * scale}
lineHeight={TEXT_PROPS.lineHeight} lineHeight={TEXT_PROPS.lineHeight}
align={align} align={align}
verticalAlign={verticalAlign} verticalAlign={verticalAlign}
@ -202,6 +214,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
isSelected={isSelected} isSelected={isSelected}
labelColor={theme[color].note.text} labelColor={theme[color].note.text}
wrap wrap
padding={16 * scale}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
/> />
</div> </div>
@ -213,10 +226,11 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
} }
indicator(shape: TLNoteShape) { indicator(shape: TLNoteShape) {
const { scale } = shape.props
return ( return (
<rect <rect
rx="1" rx={scale}
width={toDomPrecision(NOTE_SIZE)} width={toDomPrecision(NOTE_SIZE * scale)}
height={toDomPrecision(getNoteHeight(shape))} height={toDomPrecision(getNoteHeight(shape))}
/> />
) )
@ -226,7 +240,8 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
ctx.addExportDef(getFontDefForExport(shape.props.font)) ctx.addExportDef(getFontDefForExport(shape.props.font))
if (shape.props.text) ctx.addExportDef(getFontDefForExport(shape.props.font)) if (shape.props.text) ctx.addExportDef(getFontDefForExport(shape.props.font))
const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode }) const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode })
const bounds = this.editor.getShapeGeometry(shape).bounds const bounds = getBoundsForSVG(shape)
return ( return (
<> <>
<rect x={5} y={5} rx={1} width={NOTE_SIZE - 10} height={bounds.h} fill="rgba(0,0,0,.1)" /> <rect x={5} y={5} rx={1} width={NOTE_SIZE - 10} height={bounds.h} fill="rgba(0,0,0,.1)" />
@ -311,7 +326,7 @@ function getNoteSizeAdjustments(editor: Editor, shape: TLNoteShape) {
* Get the label size for a note. * Get the label size for a note.
*/ */
function getNoteLabelSize(editor: Editor, shape: TLNoteShape) { function getNoteLabelSize(editor: Editor, shape: TLNoteShape) {
const text = shape.props.text const { text } = shape.props
if (!text) { if (!text) {
const minHeight = LABEL_FONT_SIZES[shape.props.size] * TEXT_PROPS.lineHeight + LABEL_PADDING * 2 const minHeight = LABEL_FONT_SIZES[shape.props.size] * TEXT_PROPS.lineHeight + LABEL_PADDING * 2
@ -365,9 +380,9 @@ function getNoteLabelSize(editor: Editor, shape: TLNoteShape) {
} while (iterations++ < 50) } while (iterations++ < 50)
return { return {
labelHeight, labelHeight: labelHeight,
labelWidth, labelWidth: labelWidth,
fontSizeAdjustment, fontSizeAdjustment: fontSizeAdjustment,
} }
} }
@ -400,17 +415,18 @@ function useNoteKeydownHandler(id: TLShapeId) {
const isRTL = !!(translation.dir === 'rtl' || isRightToLeftLanguage(shape.props.text)) const isRTL = !!(translation.dir === 'rtl' || isRightToLeftLanguage(shape.props.text))
const offsetLength = const offsetLength =
NOTE_SIZE + (NOTE_SIZE +
editor.options.adjacentShapeMargin + editor.options.adjacentShapeMargin +
// If we're growing down, we need to account for the current shape's growY // If we're growing down, we need to account for the current shape's growY
(isCmdEnter && !e.shiftKey ? shape.props.growY : 0) (isCmdEnter && !e.shiftKey ? shape.props.growY : 0)) *
shape.props.scale
const adjacentCenter = new Vec( const adjacentCenter = new Vec(
isTab ? (e.shiftKey != isRTL ? -1 : 1) : 0, isTab ? (e.shiftKey != isRTL ? -1 : 1) : 0,
isCmdEnter ? (e.shiftKey ? -1 : 1) : 0 isCmdEnter ? (e.shiftKey ? -1 : 1) : 0
) )
.mul(offsetLength) .mul(offsetLength)
.add(NOTE_CENTER_OFFSET) .add(NOTE_CENTER_OFFSET.clone().mul(shape.props.scale))
.rot(pageRotation) .rot(pageRotation)
.add(pageTransform.point()) .add(pageTransform.point())
@ -427,14 +443,23 @@ function useNoteKeydownHandler(id: TLShapeId) {
} }
function getNoteHeight(shape: TLNoteShape) { function getNoteHeight(shape: TLNoteShape) {
return NOTE_SIZE + shape.props.growY return (NOTE_SIZE + shape.props.growY) * shape.props.scale
} }
function getNoteShadow(id: string, rotation: number) { function getNoteShadow(id: string, rotation: number, scale: number) {
const random = rng(id) // seeded based on id const random = rng(id) // seeded based on id
const lift = Math.abs(random()) + 0.5 // 0 to 1.5 const lift = Math.abs(random()) + 0.5 // 0 to 1.5
const oy = Math.cos(rotation) const oy = Math.cos(rotation)
return `0px ${5 - lift}px 5px -5px rgba(15, 23, 31, .6), const a = 5 * scale
0px ${(4 + lift * 7) * Math.max(0, oy)}px ${6 + lift * 7}px -${4 + lift * 6}px rgba(15, 23, 31, ${(0.3 + lift * 0.1).toFixed(2)}), const b = 4 * scale
0px 48px 10px -10px inset rgba(15, 23, 44, ${((0.022 + random() * 0.005) * ((1 + oy) / 2)).toFixed(2)})` const c = 6 * scale
const d = 7 * scale
return `0px ${a - lift}px ${a}px -${a}px rgba(15, 23, 31, .6),
0px ${(b + lift * d) * Math.max(0, oy)}px ${c + lift * d}px -${b + lift * c}px rgba(15, 23, 31, ${(0.3 + lift * 0.1).toFixed(2)}),
0px ${48 * scale}px ${10 * scale}px -${10 * scale}px inset rgba(15, 23, 44, ${((0.022 + random() * 0.005) * ((1 + oy) / 2)).toFixed(2)})`
}
function getBoundsForSVG(shape: TLNoteShape) {
// When rendering the SVG we don't want to adjust for scale
return new Box(0, 0, NOTE_SIZE, NOTE_SIZE + shape.props.growY)
} }

View file

@ -8,15 +8,27 @@ export const CLONE_HANDLE_MARGIN = 0
/** @internal */ /** @internal */
export const NOTE_SIZE = 200 export const NOTE_SIZE = 200
/** @internal */ /** @internal */
export const NOTE_CENTER_OFFSET = { x: NOTE_SIZE / 2, y: NOTE_SIZE / 2 } export const NOTE_CENTER_OFFSET = new Vec(NOTE_SIZE / 2, NOTE_SIZE / 2)
/** @internal */ /** @internal */
export const NOTE_PIT_RADIUS = 10 export const NOTE_ADJACENT_POSITION_SNAP_RADIUS = 10
const DEFAULT_PITS = { const BASE_NOTE_POSITIONS = [
['a1' as IndexKey]: new Vec(NOTE_SIZE * 0.5, NOTE_SIZE * -0.5 - ADJACENT_NOTE_MARGIN), // t [['a1' as IndexKey], new Vec(NOTE_SIZE * 0.5, NOTE_SIZE * -0.5 - ADJACENT_NOTE_MARGIN)], // t
['a2' as IndexKey]: new Vec(NOTE_SIZE * 1.5 + ADJACENT_NOTE_MARGIN, NOTE_SIZE * 0.5), // r [['a2' as IndexKey], new Vec(NOTE_SIZE * 1.5 + ADJACENT_NOTE_MARGIN, NOTE_SIZE * 0.5)], // r
['a3' as IndexKey]: new Vec(NOTE_SIZE * 0.5, NOTE_SIZE * 1.5 + ADJACENT_NOTE_MARGIN), // b [['a3' as IndexKey], new Vec(NOTE_SIZE * 0.5, NOTE_SIZE * 1.5 + ADJACENT_NOTE_MARGIN)], // b
['a4' as IndexKey]: new Vec(NOTE_SIZE * -0.5 - ADJACENT_NOTE_MARGIN, NOTE_SIZE * 0.5), // l [['a4' as IndexKey], new Vec(NOTE_SIZE * -0.5 - ADJACENT_NOTE_MARGIN, NOTE_SIZE * 0.5)], // l
] as const
function getBaseAdjacentNotePositions(scale: number) {
if (scale === 1) return BASE_NOTE_POSITIONS
const s = NOTE_SIZE * scale
const m = ADJACENT_NOTE_MARGIN * scale
return [
[['a1' as IndexKey], new Vec(s * 0.5, s * -0.5 - m)], // t
[['a2' as IndexKey], new Vec(s * 1.5 + m, s * 0.5)], // r
[['a3' as IndexKey], new Vec(s * 0.5, s * 1.5 + m)], // b
[['a4' as IndexKey], new Vec(s * -0.5 - m, s * 0.5)], // l
] as const
} }
/** /**
@ -32,10 +44,11 @@ export function getNoteAdjacentPositions(
pagePoint: Vec, pagePoint: Vec,
pageRotation: number, pageRotation: number,
growY: number, growY: number,
extraHeight: number extraHeight: number,
scale: number
): Record<IndexKey, Vec> { ): Record<IndexKey, Vec> {
return Object.fromEntries( return Object.fromEntries(
Object.entries(DEFAULT_PITS).map(([id, v], i) => { getBaseAdjacentNotePositions(scale).map(([id, v], i) => {
const point = v.clone() const point = v.clone()
if (i === 0 && extraHeight) { if (i === 0 && extraHeight) {
// apply top margin (the growY of the moving note shape) // apply top margin (the growY of the moving note shape)
@ -60,6 +73,7 @@ export function getNoteAdjacentPositions(
export function getAvailableNoteAdjacentPositions( export function getAvailableNoteAdjacentPositions(
editor: Editor, editor: Editor,
rotation: number, rotation: number,
scale: number,
extraHeight: number extraHeight: number
) { ) {
const selectedShapeIds = new Set(editor.getSelectedShapeIds()) const selectedShapeIds = new Set(editor.getSelectedShapeIds())
@ -69,7 +83,11 @@ export function getAvailableNoteAdjacentPositions(
// Get all the positions that are adjacent to the selected note shapes // Get all the positions that are adjacent to the selected note shapes
for (const shape of editor.getCurrentPageShapes()) { for (const shape of editor.getCurrentPageShapes()) {
if (!editor.isShapeOfType<TLNoteShape>(shape, 'note') || selectedShapeIds.has(shape.id)) { if (
!editor.isShapeOfType<TLNoteShape>(shape, 'note') ||
scale !== shape.props.scale ||
selectedShapeIds.has(shape.id)
) {
continue continue
} }
@ -84,7 +102,7 @@ export function getAvailableNoteAdjacentPositions(
// And push its position to the positions array // And push its position to the positions array
positions.push( positions.push(
...Object.values( ...Object.values(
getNoteAdjacentPositions(transform.point(), rotation, shape.props.growY, extraHeight) getNoteAdjacentPositions(transform.point(), rotation, shape.props.growY, extraHeight, scale)
) )
) )
} }
@ -133,7 +151,7 @@ export function getNoteShapeForAdjacentPosition(
// Start from the top of the stack, and work our way down // Start from the top of the stack, and work our way down
const allShapesOnPage = editor.getCurrentPageShapesSorted() const allShapesOnPage = editor.getCurrentPageShapesSorted()
const minDistance = NOTE_SIZE + ADJACENT_NOTE_MARGIN ** 2 const minDistance = (NOTE_SIZE + ADJACENT_NOTE_MARGIN ** 2) ** shape.props.scale
for (let i = allShapesOnPage.length - 1; i >= 0; i--) { for (let i = allShapesOnPage.length - 1; i >= 0; i--) {
const otherNote = allShapesOnPage[i] const otherNote = allShapesOnPage[i]
@ -158,7 +176,7 @@ export function getNoteShapeForAdjacentPosition(
const id = createShapeId() const id = createShapeId()
// We create it at the center first, so that it becomes // We create it at the center first, so that it becomes
// the child of whatever parent was at that center // the child of whatever parent was at that center
editor.createShape({ editor.createShape({
id, id,
type: 'note', type: 'note',
@ -179,13 +197,16 @@ export function getNoteShapeForAdjacentPosition(
// Now we need to correct its location within its new parent // Now we need to correct its location within its new parent
const createdShape = editor.getShape(id)! const createdShape = editor.getShape<TLNoteShape>(id)!
if (!createdShape) return // may have hit max shapes
// We need to put the page point in the same coordinate // We need to put the page point in the same coordinate space as the newly created shape (i.e its parent's space)
// space as the newly created shape (i.e its parent's space)
const topLeft = editor.getPointInParentSpace( const topLeft = editor.getPointInParentSpace(
createdShape, createdShape,
Vec.Sub(center, Vec.Rot(NOTE_CENTER_OFFSET, pageRotation)) Vec.Sub(
center,
Vec.Rot(NOTE_CENTER_OFFSET.clone().mul(createdShape.props.scale), pageRotation)
)
) )
editor.updateShape({ editor.updateShape({

View file

@ -9,7 +9,10 @@ import {
Vec, Vec,
createShapeId, createShapeId,
} from '@tldraw/editor' } from '@tldraw/editor'
import { NOTE_PIT_RADIUS, getAvailableNoteAdjacentPositions } from '../noteHelpers' import {
NOTE_ADJACENT_POSITION_SNAP_RADIUS,
getAvailableNoteAdjacentPositions,
} from '../noteHelpers'
export class Pointing extends StateNode { export class Pointing extends StateNode {
static override id = 'pointing' static override id = 'pointing'
@ -36,11 +39,15 @@ export class Pointing extends StateNode {
// Check for note pits; if the pointer is close to one, place the note centered on the pit // Check for note pits; if the pointer is close to one, place the note centered on the pit
const center = this.editor.inputs.originPagePoint.clone() const center = this.editor.inputs.originPagePoint.clone()
const offset = getNotePitOffset(this.editor, center) const offset = getNoteShapeAdjacentPositionOffset(
this.editor,
center,
this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1
)
if (offset) { if (offset) {
center.sub(offset) center.sub(offset)
} }
this.shape = createSticky(this.editor, id, center) this.shape = createNoteShape(this.editor, id, center)
} }
} }
@ -49,11 +56,15 @@ export class Pointing extends StateNode {
if (!this.wasFocusedOnEnter) { if (!this.wasFocusedOnEnter) {
const id = createShapeId() const id = createShapeId()
const center = this.editor.inputs.originPagePoint.clone() const center = this.editor.inputs.originPagePoint.clone()
const offset = getNotePitOffset(this.editor, center) const offset = getNoteShapeAdjacentPositionOffset(
this.editor,
center,
this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1
)
if (offset) { if (offset) {
center.sub(offset) center.sub(offset)
} }
this.shape = createSticky(this.editor, id, center) this.shape = createNoteShape(this.editor, id, center)
} }
this.editor.setCurrentTool('select.translating', { this.editor.setCurrentTool('select.translating', {
@ -107,10 +118,10 @@ export class Pointing extends StateNode {
} }
} }
export function getNotePitOffset(editor: Editor, center: Vec) { export function getNoteShapeAdjacentPositionOffset(editor: Editor, center: Vec, scale: number) {
let min = NOTE_PIT_RADIUS / editor.getZoomLevel() // in screen space let min = NOTE_ADJACENT_POSITION_SNAP_RADIUS / editor.getZoomLevel() // in screen space
let offset: Vec | undefined let offset: Vec | undefined
for (const pit of getAvailableNoteAdjacentPositions(editor, 0, 0)) { for (const pit of getAvailableNoteAdjacentPositions(editor, 0, scale, 0)) {
// only check page rotations of zero // only check page rotations of zero
const deltaToPit = Vec.Sub(center, pit) const deltaToPit = Vec.Sub(center, pit)
const dist = deltaToPit.len() const dist = deltaToPit.len()
@ -122,13 +133,16 @@ export function getNotePitOffset(editor: Editor, center: Vec) {
return offset return offset
} }
export function createSticky(editor: Editor, id: TLShapeId, center: Vec) { export function createNoteShape(editor: Editor, id: TLShapeId, center: Vec) {
editor editor
.createShape({ .createShape({
id, id,
type: 'note', type: 'note',
x: center.x, x: center.x,
y: center.y, y: center.y,
props: {
scale: editor.user.getIsDynamicResizeMode() ? 1 / editor.getZoomLevel() : 1,
},
}) })
.select(id) .select(id)

View file

@ -2,28 +2,28 @@ import {
TLDefaultColorStyle, TLDefaultColorStyle,
TLDefaultColorTheme, TLDefaultColorTheme,
TLDefaultFillStyle, TLDefaultFillStyle,
getDefaultColorTheme,
useEditor, useEditor,
useIsDarkMode,
useSvgExportContext, useSvgExportContext,
useValue, useValue,
} from '@tldraw/editor' } from '@tldraw/editor'
import React from 'react' import React from 'react'
import { getHashPatternZoomName } from './defaultStyleDefs' import { getHashPatternZoomName } from './defaultStyleDefs'
export interface ShapeFillProps { interface ShapeFillProps {
d: string d: string
fill: TLDefaultFillStyle fill: TLDefaultFillStyle
color: TLDefaultColorStyle color: TLDefaultColorStyle
theme: TLDefaultColorTheme theme: TLDefaultColorTheme
scale: number
} }
/** @public */ export const ShapeFill = React.memo(function ShapeFill({
export function useDefaultColorTheme() { theme,
return getDefaultColorTheme({ isDarkMode: useIsDarkMode() }) d,
} color,
fill,
export const ShapeFill = React.memo(function ShapeFill({ theme, d, color, fill }: ShapeFillProps) { scale,
}: ShapeFillProps) {
switch (fill) { switch (fill) {
case 'none': { case 'none': {
return null return null
@ -34,8 +34,11 @@ export const ShapeFill = React.memo(function ShapeFill({ theme, d, color, fill }
case 'semi': { case 'semi': {
return <path fill={theme.solid} d={d} /> return <path fill={theme.solid} d={d} />
} }
case 'fill': {
return <path fill={theme[color].fill} d={d} />
}
case 'pattern': { case 'pattern': {
return <PatternFill theme={theme} color={color} fill={fill} d={d} /> return <PatternFill theme={theme} color={color} fill={fill} d={d} scale={scale} />
} }
} }
}) })

View file

@ -6,10 +6,10 @@ import {
TLDefaultVerticalAlignStyle, TLDefaultVerticalAlignStyle,
useEditor, useEditor,
} from '@tldraw/editor' } from '@tldraw/editor'
import { useDefaultColorTheme } from './ShapeFill'
import { createTextJsxFromSpans } from './createTextJsxFromSpans' import { createTextJsxFromSpans } from './createTextJsxFromSpans'
import { TEXT_PROPS } from './default-shape-constants' import { TEXT_PROPS } from './default-shape-constants'
import { getLegacyOffsetX } from './legacyProps' import { getLegacyOffsetX } from './legacyProps'
import { useDefaultColorTheme } from './useDefaultColorTheme'
export function SvgTextLabel({ export function SvgTextLabel({
fontSize, fontSize,

View file

@ -33,6 +33,7 @@ export interface TextLabelProps {
style?: React.CSSProperties style?: React.CSSProperties
textWidth?: number textWidth?: number
textHeight?: number textHeight?: number
padding?: number
} }
/** @public @react */ /** @public @react */
@ -48,6 +49,7 @@ export const TextLabel = React.memo(function TextLabel({
verticalAlign, verticalAlign,
wrap, wrap,
isSelected, isSelected,
padding = 0,
onKeyDown: handleKeyDownCustom, onKeyDown: handleKeyDownCustom,
classNamePrefix, classNamePrefix,
style, style,
@ -90,6 +92,7 @@ export const TextLabel = React.memo(function TextLabel({
style={{ style={{
justifyContent: align === 'middle' || legacyAlign ? 'center' : align, justifyContent: align === 'middle' || legacyAlign ? 'center' : align,
alignItems: verticalAlign === 'middle' ? 'center' : verticalAlign, alignItems: verticalAlign === 'middle' ? 'center' : verticalAlign,
padding,
...style, ...style,
}} }}
> >
@ -98,7 +101,7 @@ export const TextLabel = React.memo(function TextLabel({
style={{ style={{
fontSize, fontSize,
lineHeight: Math.floor(fontSize * lineHeight) + 'px', lineHeight: Math.floor(fontSize * lineHeight) + 'px',
minHeight: lineHeight + 32, minHeight: Math.floor(fontSize * lineHeight) + 'px',
minWidth: Math.ceil(textWidth || 0), minWidth: Math.ceil(textWidth || 0),
color: labelColor, color: labelColor,
width: textWidth ? Math.ceil(textWidth) : undefined, width: textWidth ? Math.ceil(textWidth) : undefined,

View file

@ -15,7 +15,7 @@ import {
useValue, useValue,
} from '@tldraw/editor' } from '@tldraw/editor'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useDefaultColorTheme } from './ShapeFill' import { useDefaultColorTheme } from './useDefaultColorTheme'
/** @public */ /** @public */
export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef { export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef {

View file

@ -0,0 +1,6 @@
import { getDefaultColorTheme, useIsDarkMode } from '@tldraw/editor'
/** @public */
export function useDefaultColorTheme() {
return getDefaultColorTheme({ isDarkMode: useIsDarkMode() })
}

View file

@ -1,3 +1,4 @@
import { DefaultTextAlignStyle } from '@tldraw/editor'
import { TestEditor } from '../../../test/TestEditor' import { TestEditor } from '../../../test/TestEditor'
import { TextShapeTool } from './TextShapeTool' import { TextShapeTool } from './TextShapeTool'
@ -111,16 +112,48 @@ describe('When in the pointing state', () => {
it('on pointer up, preserves the center when the text has a auto width', () => { it('on pointer up, preserves the center when the text has a auto width', () => {
editor.setCurrentTool('text') editor.setCurrentTool('text')
editor.setStyleForNextShapes(DefaultTextAlignStyle, 'middle')
const x = 0 const x = 0
const y = 0 const y = 0
editor.pointerDown(x, y) editor.pointerDown(x, y)
editor.pointerUp() editor.pointerUp()
const bounds = editor.getShapePageBounds(editor.getCurrentPageShapes()[0])! const shape = editor.getLastCreatedShape()
expect(editor.getCurrentPageShapes()[0]).toMatchObject({ const bounds = editor.getShapePageBounds(shape)!
expect(shape).toMatchObject({
x: x - bounds.width / 2, x: x - bounds.width / 2,
y: y - bounds.height / 2, y: y - bounds.height / 2,
}) })
}) })
it('on pointer up, preserves the center when the text has a auto width (left aligned)', () => {
editor.setCurrentTool('text')
editor.setStyleForNextShapes(DefaultTextAlignStyle, 'start')
const x = 0
const y = 0
editor.pointerDown(x, y)
editor.pointerUp()
const shape = editor.getLastCreatedShape()
const bounds = editor.getShapePageBounds(shape)!
expect(shape).toMatchObject({
x,
y: y - bounds.height / 2,
})
})
it('on pointer up, preserves the center when the text has a auto width (right aligned)', () => {
editor.setCurrentTool('text')
editor.setStyleForNextShapes(DefaultTextAlignStyle, 'end')
const x = 0
const y = 0
editor.pointerDown(x, y)
editor.pointerUp()
const shape = editor.getLastCreatedShape()
const bounds = editor.getShapePageBounds(shape)!
expect(shape).toMatchObject({
x: x - bounds.width,
y: y - bounds.height / 2,
})
})
}) })
describe('When resizing', () => { describe('When resizing', () => {
@ -151,7 +184,7 @@ describe('When resizing', () => {
editor.pointerMove(x + 100, y + 100) editor.pointerMove(x + 100, y + 100)
expect(editor.getCurrentPageShapes()[0]).toMatchObject({ expect(editor.getCurrentPageShapes()[0]).toMatchObject({
x, x,
y, y: -12, // 24 is the height of the text, and it's centered at that point
}) })
}) })
}) })

View file

@ -20,13 +20,13 @@ import {
useEditor, useEditor,
} from '@tldraw/editor' } from '@tldraw/editor'
import { useCallback } from 'react' import { useCallback } from 'react'
import { useDefaultColorTheme } from '../shared/ShapeFill'
import { SvgTextLabel } from '../shared/SvgTextLabel' import { SvgTextLabel } from '../shared/SvgTextLabel'
import { TextHelpers } from '../shared/TextHelpers' import { TextHelpers } from '../shared/TextHelpers'
import { TextLabel } from '../shared/TextLabel' import { TextLabel } from '../shared/TextLabel'
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants' import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
import { getFontDefForExport } from '../shared/defaultStyleDefs' import { getFontDefForExport } from '../shared/defaultStyleDefs'
import { resizeScaled } from '../shared/resizeScaled' import { resizeScaled } from '../shared/resizeScaled'
import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
const sizeCache = new WeakCache<TLTextShape['props'], { height: number; width: number }>() const sizeCache = new WeakCache<TLTextShape['props'], { height: number; width: number }>()
@ -162,24 +162,6 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
} }
} }
override onBeforeCreate = (shape: TLTextShape) => {
// When a shape is created, center the text at the created point.
// Only center if the shape is set to autosize.
if (!shape.props.autoSize) return
// Only center if the shape is empty when created.
if (shape.props.text.trim()) return
const bounds = this.getMinDimensions(shape)
return {
...shape,
x: shape.x - bounds.width / 2,
y: shape.y - bounds.height / 2,
}
}
override onEditEnd: TLOnEditEndHandler<TLTextShape> = (shape) => { override onEditEnd: TLOnEditEndHandler<TLTextShape> = (shape) => {
const { const {
id, id,
@ -267,29 +249,31 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
} }
} }
override onDoubleClickEdge = (shape: TLTextShape) => { // todo: The edge doubleclicking feels like a mistake more often than
// If the shape has a fixed width, set it to autoSize. // not, especially on multiline text. Removed June 16 2024
if (!shape.props.autoSize) {
return {
id: shape.id,
type: shape.type,
props: {
autoSize: true,
},
}
}
// If the shape is scaled, reset the scale to 1. // override onDoubleClickEdge = (shape: TLTextShape) => {
if (shape.props.scale !== 1) { // // If the shape has a fixed width, set it to autoSize.
return { // if (!shape.props.autoSize) {
id: shape.id, // return {
type: shape.type, // id: shape.id,
props: { // type: shape.type,
scale: 1, // props: {
}, // autoSize: true,
} // },
} // }
} // }
// // If the shape is scaled, reset the scale to 1.
// if (shape.props.scale !== 1) {
// return {
// id: shape.id,
// type: shape.type,
// props: {
// scale: 1,
// },
// }
// }
// }
} }
function getTextSize(editor: Editor, props: TLTextShape['props']) { function getTextSize(editor: Editor, props: TLTextShape['props']) {
@ -310,9 +294,9 @@ function getTextSize(editor: Editor, props: TLTextShape['props']) {
maxWidth: cw, maxWidth: cw,
}) })
// // If we're autosizing the measureText will essentially `Math.floor` // If we're autosizing the measureText will essentially `Math.floor`
// // the numbers so `19` rather than `19.3`, this means we must +1 to // the numbers so `19` rather than `19.3`, this means we must +1 to
// // whatever we get to avoid wrapping. // whatever we get to avoid wrapping.
if (autoSize) { if (autoSize) {
result.w += 1 result.w += 1
} }

View file

@ -1,4 +1,12 @@
import { StateNode, TLEventHandlers, TLTextShape, createShapeId } from '@tldraw/editor' import {
StateNode,
TLEventHandlers,
TLShapeId,
TLTextShape,
Vec,
createShapeId,
isShapeId,
} from '@tldraw/editor'
export class Pointing extends StateNode { export class Pointing extends StateNode {
static override id = 'pointing' static override id = 'pointing'
@ -22,27 +30,17 @@ export class Pointing extends StateNode {
this.markId = `creating:${id}` this.markId = `creating:${id}`
this.editor.mark(this.markId) this.editor.mark(this.markId)
this.editor.createShapes<TLTextShape>([ const shape = this.createTextShape(id, originPagePoint, false)
{ if (!shape) {
id, this.cancel()
type: 'text', return
x: originPagePoint.x, }
y: originPagePoint.y,
props: { // Now save the fresh reference
text: '', this.shape = this.editor.getShape(shape)
autoSize: false,
w: 20,
},
},
])
this.editor.select(id) this.editor.select(id)
this.shape = this.editor.getShape(id)
if (!this.shape) return
const { shape } = this
this.editor.setCurrentTool('select.resizing', { this.editor.setCurrentTool('select.resizing', {
...info, ...info,
target: 'selection', target: 'selection',
@ -77,22 +75,11 @@ export class Pointing extends StateNode {
private complete() { private complete() {
this.editor.mark('creating text shape') this.editor.mark('creating text shape')
const id = createShapeId() const id = createShapeId()
const { x, y } = this.editor.inputs.currentPagePoint const { currentPagePoint } = this.editor.inputs
this.editor const shape = this.createTextShape(id, currentPagePoint, true)
.createShapes([ if (!shape) return
{
id,
type: 'text',
x,
y,
props: {
text: '',
autoSize: true,
},
},
])
.select(id)
this.editor.select(id)
this.editor.setEditingShape(id) this.editor.setEditingShape(id)
this.editor.setCurrentTool('select') this.editor.setCurrentTool('select')
this.editor.root.getCurrent()?.transition('editing_shape') this.editor.root.getCurrent()?.transition('editing_shape')
@ -102,4 +89,63 @@ export class Pointing extends StateNode {
this.parent.transition('idle') this.parent.transition('idle')
this.editor.bailToMark(this.markId) this.editor.bailToMark(this.markId)
} }
private createTextShape(id: TLShapeId, point: Vec, autoSize: boolean) {
this.editor.createShape<TLTextShape>({
id,
type: 'text',
x: point.x,
y: point.y,
props: {
text: '',
autoSize,
w: 20,
scale: this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1,
},
})
const shape = this.editor.getShape<TLTextShape>(id)
if (!shape) {
this.cancel()
return
}
const bounds = this.editor.getShapePageBounds(shape)!
const delta = new Vec()
if (autoSize) {
switch (shape.props.textAlign) {
case 'start': {
delta.x = 0
break
}
case 'middle': {
delta.x = -bounds.width / 2
break
}
case 'end': {
delta.x = -bounds.width
break
}
}
} else {
delta.x = 0
}
delta.y = -bounds.height / 2
if (isShapeId(shape.parentId)) {
const transform = this.editor.getShapeParentTransform(shape)
delta.rot(-transform.rotation())
}
this.editor.updateShape({
...shape,
x: shape.x + delta.x,
y: shape.y + delta.y,
})
return shape
}
} }

View file

@ -25,6 +25,7 @@ export const STYLES = {
{ value: 'semi', icon: 'fill-semi' }, { value: 'semi', icon: 'fill-semi' },
{ value: 'solid', icon: 'fill-solid' }, { value: 'solid', icon: 'fill-solid' },
{ value: 'pattern', icon: 'fill-pattern' }, { value: 'pattern', icon: 'fill-pattern' },
// { value: 'fill', icon: 'fill-fill' },
], ],
dash: [ dash: [
{ value: 'draw', icon: 'dash-draw' }, { value: 'draw', icon: 'dash-draw' },

View file

@ -78,7 +78,7 @@ export class PointingHandle extends StateNode {
// Center the shape on the current pointer // Center the shape on the current pointer
const centeredOnPointer = editor const centeredOnPointer = editor
.getPointInParentSpace(nextNote, editor.inputs.originPagePoint) .getPointInParentSpace(nextNote, editor.inputs.originPagePoint)
.sub(Vec.Rot(NOTE_CENTER_OFFSET, nextNote.rotation)) .sub(Vec.Rot(NOTE_CENTER_OFFSET.clone().mul(shape.props.scale), nextNote.rotation))
editor.updateShape({ ...nextNote, x: centeredOnPointer.x, y: centeredOnPointer.y }) editor.updateShape({ ...nextNote, x: centeredOnPointer.x, y: centeredOnPointer.y })
// Then select and begin translating the shape // Then select and begin translating the shape
@ -124,7 +124,13 @@ function getNoteForPit(editor: Editor, shape: TLNoteShape, handle: TLHandle, for
const pageTransform = editor.getShapePageTransform(shape.id)! const pageTransform = editor.getShapePageTransform(shape.id)!
const pagePoint = pageTransform.point() const pagePoint = pageTransform.point()
const pageRotation = pageTransform.rotation() const pageRotation = pageTransform.rotation()
const pits = getNoteAdjacentPositions(pagePoint, pageRotation, shape.props.growY, 0) const pits = getNoteAdjacentPositions(
pagePoint,
pageRotation,
shape.props.growY,
0,
shape.props.scale
)
const pit = pits[handle.index] const pit = pits[handle.index]
if (pit) { if (pit) {
return getNoteShapeForAdjacentPosition(editor, shape, pit, pageRotation, forceNew) return getNoteShapeForAdjacentPosition(editor, shape, pit, pageRotation, forceNew)

View file

@ -16,8 +16,8 @@ import {
moveCameraWhenCloseToEdge, moveCameraWhenCloseToEdge,
} from '@tldraw/editor' } from '@tldraw/editor'
import { import {
NOTE_PIT_RADIUS, NOTE_ADJACENT_POSITION_SNAP_RADIUS,
NOTE_SIZE, NOTE_CENTER_OFFSET,
getAvailableNoteAdjacentPositions, getAvailableNoteAdjacentPositions,
} from '../../../shapes/note/noteHelpers' } from '../../../shapes/note/noteHelpers'
import { DragAndDropManager } from '../DragAndDropManager' import { DragAndDropManager } from '../DragAndDropManager'
@ -353,7 +353,7 @@ function getTranslatingSnapshot(editor: Editor) {
} }
let noteAdjacentPositions: Vec[] | undefined let noteAdjacentPositions: Vec[] | undefined
let noteSnapshot: MovingShapeSnapshot | undefined let noteSnapshot: (MovingShapeSnapshot & { shape: TLNoteShape }) | undefined
const { originPagePoint } = editor.inputs const { originPagePoint } = editor.inputs
@ -361,7 +361,7 @@ function getTranslatingSnapshot(editor: Editor) {
(s) => (s) =>
editor.isShapeOfType<TLNoteShape>(s.shape, 'note') && editor.isShapeOfType<TLNoteShape>(s.shape, 'note') &&
editor.isPointInShape(s.shape, originPagePoint) editor.isPointInShape(s.shape, originPagePoint)
) ) as (MovingShapeSnapshot & { shape: TLNoteShape })[]
if (allHoveredNotes.length === 0) { if (allHoveredNotes.length === 0) {
// noop // noop
@ -383,7 +383,8 @@ function getTranslatingSnapshot(editor: Editor) {
noteAdjacentPositions = getAvailableNoteAdjacentPositions( noteAdjacentPositions = getAvailableNoteAdjacentPositions(
editor, editor,
noteSnapshot.pageRotation, noteSnapshot.pageRotation,
(noteSnapshot.shape as TLNoteShape).props.growY ?? 0 noteSnapshot.shape.props.scale,
noteSnapshot.shape.props.growY ?? 0
) )
} }
@ -461,14 +462,16 @@ export function moveShapesToPoint({
} else { } else {
// for sticky notes, snap to grid position next to other notes // for sticky notes, snap to grid position next to other notes
if (noteSnapshot && noteAdjacentPositions) { if (noteSnapshot && noteAdjacentPositions) {
let min = NOTE_PIT_RADIUS / editor.getZoomLevel() // in screen space const { scale } = noteSnapshot.shape.props
const pageCenter = noteSnapshot.pagePoint
.clone()
.add(delta)
// use the middle of the note, disregarding extra height
.add(NOTE_CENTER_OFFSET.clone().mul(scale).rot(noteSnapshot.pageRotation))
// Find the pit with the center closest to the put center
let min = NOTE_ADJACENT_POSITION_SNAP_RADIUS / editor.getZoomLevel() // in screen space
let offset = new Vec(0, 0) let offset = new Vec(0, 0)
const pageCenter = Vec.Add(
Vec.Add(noteSnapshot.pagePoint, delta),
new Vec(NOTE_SIZE / 2, NOTE_SIZE / 2).rot(noteSnapshot.pageRotation)
)
for (const pit of noteAdjacentPositions) { for (const pit of noteAdjacentPositions) {
// We've already filtered pits with the same page rotation // We've already filtered pits with the same page rotation
const deltaToPit = Vec.Sub(pageCenter, pit) const deltaToPit = Vec.Sub(pageCenter, pit)

View file

@ -16,6 +16,7 @@ import {
ToggleAutoSizeMenuItem, ToggleAutoSizeMenuItem,
ToggleDarkModeItem, ToggleDarkModeItem,
ToggleDebugModeItem, ToggleDebugModeItem,
ToggleDynamicSizeModeItem,
ToggleEdgeScrollingItem, ToggleEdgeScrollingItem,
ToggleFocusModeItem, ToggleFocusModeItem,
ToggleGridItem, ToggleGridItem,
@ -173,6 +174,7 @@ export function PreferencesGroup() {
<ToggleFocusModeItem /> <ToggleFocusModeItem />
<ToggleEdgeScrollingItem /> <ToggleEdgeScrollingItem />
<ToggleReduceMotionItem /> <ToggleReduceMotionItem />
<ToggleDynamicSizeModeItem />
<ToggleDebugModeItem /> <ToggleDebugModeItem />
</TldrawUiMenuGroup> </TldrawUiMenuGroup>
<TldrawUiMenuGroup id="language"> <TldrawUiMenuGroup id="language">

View file

@ -607,6 +607,23 @@ export function ToggleDebugModeItem() {
return <TldrawUiMenuCheckboxItem {...actions['toggle-debug-mode']} checked={isDebugMode} /> return <TldrawUiMenuCheckboxItem {...actions['toggle-debug-mode']} checked={isDebugMode} />
} }
/** @public @react */
export function ToggleDynamicSizeModeItem() {
const actions = useActions()
const editor = useEditor()
const isDynamicResizeMode = useValue(
'dynamic resize',
() => editor.user.getIsDynamicResizeMode(),
[editor]
)
return (
<TldrawUiMenuCheckboxItem
{...actions['toggle-dynamic-size-mode']}
checked={isDynamicResizeMode}
/>
)
}
/* ---------------------- Print --------------------- */ /* ---------------------- Print --------------------- */
/** @public @react */ /** @public @react */
export function PrintItem() { export function PrintItem() {

View file

@ -1130,6 +1130,21 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
}, },
checkbox: true, checkbox: true,
}, },
{
id: 'toggle-dynamic-size-mode',
label: {
default: 'action.toggle-dynamic-size-mode',
menu: 'action.toggle-dynamic-size-mode.menu',
},
readonlyOk: false,
onSelect(source) {
trackEvent('toggle-dynamic-size-mode', { source })
editor.user.updateUserPreferences({
isDynamicSizeMode: !editor.user.getIsDynamicResizeMode(),
})
},
checkbox: true,
},
{ {
id: 'toggle-reduce-motion', id: 'toggle-reduce-motion',
label: { label: {

View file

@ -88,6 +88,7 @@ export interface TLUiEventMap {
'toggle-wrap-mode': null 'toggle-wrap-mode': null
'toggle-focus-mode': null 'toggle-focus-mode': null
'toggle-debug-mode': null 'toggle-debug-mode': null
'toggle-dynamic-size-mode': null
'toggle-lock': null 'toggle-lock': null
'toggle-reduce-motion': null 'toggle-reduce-motion': null
'toggle-edge-scrolling': null 'toggle-edge-scrolling': null

View file

@ -67,6 +67,7 @@ exports[`pasteExcalidrawContent test fixtures bound-arrows.json 1`] = `
"growY": 0, "growY": 0,
"h": 129.7109375, "h": 129.7109375,
"labelColor": "black", "labelColor": "black",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -100,6 +101,7 @@ exports[`pasteExcalidrawContent test fixtures bound-arrows.json 1`] = `
"font": "draw", "font": "draw",
"labelColor": "black", "labelColor": "black",
"labelPosition": 0.5, "labelPosition": 0.5,
"scale": 1,
"size": "m", "size": "m",
"start": { "start": {
"x": 0, "x": 0,
@ -130,6 +132,7 @@ exports[`pasteExcalidrawContent test fixtures bound-arrows.json 1`] = `
"growY": 0, "growY": 0,
"h": 116.80078125, "h": 116.80078125,
"labelColor": "black", "labelColor": "black",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -203,6 +206,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 1.1747704163190065, "y": 1.1747704163190065,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -248,6 +252,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": -94.83601179012066, "y": -94.83601179012066,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -311,6 +316,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 44.74807313069914, "y": 44.74807313069914,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -356,6 +362,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 6.728230566191087, "y": 6.728230566191087,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -395,6 +402,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 1.81555427976582, "y": 1.81555427976582,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "line", "spline": "line",
}, },
@ -428,6 +436,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": -40.796572639443866, "y": -40.796572639443866,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "line", "spline": "line",
}, },
@ -461,6 +470,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 37.69945063278442, "y": 37.69945063278442,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "line", "spline": "line",
}, },
@ -487,6 +497,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"growY": 0, "growY": 0,
"h": 10.466136436297347, "h": 10.466136436297347,
"labelColor": "black", "labelColor": "black",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -541,6 +552,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 53.02035135709548, "y": 53.02035135709548,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "line", "spline": "line",
}, },
@ -586,6 +598,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 12.388488026637333, "y": 12.388488026637333,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -637,6 +650,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": -6.401707036148309, "y": -6.401707036148309,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -660,6 +674,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"isClosed": false, "isClosed": false,
"isComplete": false, "isComplete": false,
"isPen": false, "isPen": false,
"scale": 1,
"segments": [ "segments": [
{ {
"points": [ "points": [
@ -699,6 +714,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"isClosed": false, "isClosed": false,
"isComplete": false, "isComplete": false,
"isPen": false, "isPen": false,
"scale": 1,
"segments": [ "segments": [
{ {
"points": [ "points": [
@ -748,6 +764,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 39.21438083934686, "y": 39.21438083934686,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -787,6 +804,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 38.824834009817096, "y": 38.824834009817096,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -832,6 +850,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 1.8178852044727591, "y": 1.8178852044727591,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -883,6 +902,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 1.2616464425132108, "y": 1.2616464425132108,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -934,6 +954,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 90.45732205656805, "y": 90.45732205656805,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -985,6 +1006,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 1.1747704163190065, "y": 1.1747704163190065,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -1030,6 +1052,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": -94.83601179012066, "y": -94.83601179012066,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -1063,6 +1086,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 39.21438083934686, "y": 39.21438083934686,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -1102,6 +1126,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 38.824834009817096, "y": 38.824834009817096,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -1147,6 +1172,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 1.0989312447093198, "y": 1.0989312447093198,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -1198,6 +1224,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 0.9092450946582176, "y": 0.9092450946582176,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -1224,6 +1251,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"growY": 0, "growY": 0,
"h": 80.37089607781583, "h": 80.37089607781583,
"labelColor": "black", "labelColor": "black",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -1278,6 +1306,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": -6.401707036148309, "y": -6.401707036148309,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -1301,6 +1330,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"isClosed": false, "isClosed": false,
"isComplete": false, "isComplete": false,
"isPen": false, "isPen": false,
"scale": 1,
"segments": [ "segments": [
{ {
"points": [ "points": [
@ -1340,6 +1370,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"isClosed": false, "isClosed": false,
"isComplete": false, "isComplete": false,
"isPen": false, "isPen": false,
"scale": 1,
"segments": [ "segments": [
{ {
"points": [ "points": [
@ -1413,6 +1444,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 25.20210208864819, "y": 25.20210208864819,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -1458,6 +1490,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 8.260689017945879, "y": 8.260689017945879,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },
@ -1484,6 +1517,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"growY": 0, "growY": 0,
"h": 8.680724052756432, "h": 8.680724052756432,
"labelColor": "black", "labelColor": "black",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -1526,6 +1560,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": -42.45472883070397, "y": -42.45472883070397,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "line", "spline": "line",
}, },
@ -1566,6 +1601,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"growY": 0, "growY": 0,
"h": 80.37089607781583, "h": 80.37089607781583,
"labelColor": "black", "labelColor": "black",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -1620,6 +1656,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
"y": 90.45732205656805, "y": 90.45732205656805,
}, },
}, },
"scale": 1,
"size": "m", "size": "m",
"spline": "cubic", "spline": "cubic",
}, },

View file

@ -97,6 +97,8 @@ export type TLUiTranslationKey =
| 'action.toggle-debug-mode' | 'action.toggle-debug-mode'
| 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode.menu'
| 'action.toggle-focus-mode' | 'action.toggle-focus-mode'
| 'action.toggle-dynamic-size-mode.menu'
| 'action.toggle-dynamic-size-mode'
| 'action.toggle-grid.menu' | 'action.toggle-grid.menu'
| 'action.toggle-grid' | 'action.toggle-grid'
| 'action.toggle-lock' | 'action.toggle-lock'

View file

@ -97,6 +97,8 @@ export const DEFAULT_TRANSLATION = {
'action.toggle-debug-mode': 'Toggle debug mode', 'action.toggle-debug-mode': 'Toggle debug mode',
'action.toggle-focus-mode.menu': 'Focus mode', 'action.toggle-focus-mode.menu': 'Focus mode',
'action.toggle-focus-mode': 'Toggle focus mode', 'action.toggle-focus-mode': 'Toggle focus mode',
'action.toggle-dynamic-size-mode.menu': 'Dynamic size',
'action.toggle-dynamic-size-mode': 'Toggle dynamic size',
'action.toggle-grid.menu': 'Show grid', 'action.toggle-grid.menu': 'Show grid',
'action.toggle-grid': 'Toggle grid', 'action.toggle-grid': 'Toggle grid',
'action.toggle-lock': 'Toggle locked', 'action.toggle-lock': 'Toggle locked',

View file

@ -50,6 +50,7 @@ export type TLUiIconType =
| 'duplicate' | 'duplicate'
| 'edit' | 'edit'
| 'external-link' | 'external-link'
| 'fill-fill'
| 'fill-none' | 'fill-none'
| 'fill-pattern' | 'fill-pattern'
| 'fill-semi' | 'fill-semi'
@ -192,6 +193,7 @@ export const iconTypes = [
'duplicate', 'duplicate',
'edit', 'edit',
'external-link', 'external-link',
'fill-fill',
'fill-none', 'fill-none',
'fill-pattern', 'fill-pattern',
'fill-semi', 'fill-semi',

View file

@ -67,6 +67,7 @@ exports[`buildFromV1Document test fixtures arrow-binding.tldr 1`] = `
"growY": 0, "growY": 0,
"h": 114.39, "h": 114.39,
"labelColor": "red", "labelColor": "red",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -100,6 +101,7 @@ exports[`buildFromV1Document test fixtures arrow-binding.tldr 1`] = `
"font": "draw", "font": "draw",
"labelColor": "red", "labelColor": "red",
"labelPosition": 0.5, "labelPosition": 0.5,
"scale": 1,
"size": "m", "size": "m",
"start": { "start": {
"x": 146.32, "x": 146.32,
@ -130,6 +132,7 @@ exports[`buildFromV1Document test fixtures arrow-binding.tldr 1`] = `
"growY": 0, "growY": 0,
"h": 177.03, "h": 177.03,
"labelColor": "red", "labelColor": "red",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -212,6 +215,7 @@ exports[`buildFromV1Document test fixtures exact-arrow-binding.tldr 1`] = `
"growY": 0, "growY": 0,
"h": 114.39, "h": 114.39,
"labelColor": "red", "labelColor": "red",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -245,6 +249,7 @@ exports[`buildFromV1Document test fixtures exact-arrow-binding.tldr 1`] = `
"font": "draw", "font": "draw",
"labelColor": "red", "labelColor": "red",
"labelPosition": 0.5, "labelPosition": 0.5,
"scale": 1,
"size": "m", "size": "m",
"start": { "start": {
"x": 293.36, "x": 293.36,
@ -275,6 +280,7 @@ exports[`buildFromV1Document test fixtures exact-arrow-binding.tldr 1`] = `
"growY": 0, "growY": 0,
"h": 177.03, "h": 177.03,
"labelColor": "red", "labelColor": "red",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -340,6 +346,7 @@ exports[`buildFromV1Document test fixtures incorrect-arrow-binding.tldr 1`] = `
"growY": 0, "growY": 0,
"h": 114.39, "h": 114.39,
"labelColor": "red", "labelColor": "red",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -373,6 +380,7 @@ exports[`buildFromV1Document test fixtures incorrect-arrow-binding.tldr 1`] = `
"font": "draw", "font": "draw",
"labelColor": "red", "labelColor": "red",
"labelPosition": 0.5, "labelPosition": 0.5,
"scale": 1,
"size": "m", "size": "m",
"start": { "start": {
"x": 252.64, "x": 252.64,
@ -403,6 +411,7 @@ exports[`buildFromV1Document test fixtures incorrect-arrow-binding.tldr 1`] = `
"growY": 0, "growY": 0,
"h": 177.03, "h": 177.03,
"labelColor": "red", "labelColor": "red",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",

View file

@ -329,42 +329,6 @@ describe('When pressing enter on a selected shape', () => {
// }) // })
describe('When double clicking the selection edge', () => { describe('When double clicking the selection edge', () => {
it('Resets text scale when double clicking the edge of the text', () => {
const id = createShapeId()
editor
.selectAll()
.deleteShapes(editor.getSelectedShapeIds())
.selectNone()
.createShapes([{ id, type: 'text', x: 100, y: 100, props: { scale: 2, text: 'hello' } }])
.select(id)
.doubleClick(100, 100, { target: 'selection', handle: 'left' })
editor.expectShapeToMatch({ id, props: { scale: 1 } })
})
it('Resets text autosize first when double clicking the edge of the text', () => {
const id = createShapeId()
editor
.selectAll()
.deleteShapes(editor.getSelectedShapeIds())
.selectNone()
.createShapes([
{
id,
type: 'text',
props: { scale: 2, autoSize: false, w: 200, text: 'hello' },
},
])
.select(id)
.doubleClick(100, 100, { target: 'selection', handle: 'left' })
editor.expectShapeToMatch({ id, props: { scale: 2, autoSize: true } })
editor.doubleClick(100, 100, { target: 'selection', handle: 'left' })
editor.expectShapeToMatch({ id, props: { scale: 1, autoSize: true } })
})
it('Begins editing the text if handler returns no change', () => { it('Begins editing the text if handler returns no change', () => {
const id = createShapeId() const id = createShapeId()
editor editor
@ -382,10 +346,12 @@ describe('When double clicking the selection edge', () => {
.doubleClick(100, 100, { target: 'selection', handle: 'left' }) .doubleClick(100, 100, { target: 'selection', handle: 'left' })
.doubleClick(100, 100, { target: 'selection', handle: 'left' }) .doubleClick(100, 100, { target: 'selection', handle: 'left' })
expect(editor.getEditingShapeId()).toBe(null) // Update:
editor.expectShapeToMatch({ id, props: { scale: 1, autoSize: true } }) // Previously, double clicking text edges would reset the scale and prevent editing. This is no longer the case.
//
editor.doubleClick(100, 100, { target: 'selection', handle: 'left' }) // expect(editor.getEditingShapeId()).toBe(null)
// editor.expectShapeToMatch({ id, props: { scale: 1, autoSize: true } })
// editor.doubleClick(100, 100, { target: 'selection', handle: 'left' })
expect(editor.getEditingShapeId()).toBe(id) expect(editor.getEditingShapeId()).toBe(id)
}) })

View file

@ -14,6 +14,7 @@ exports[`Draws a bunch: draw shape 1`] = `
"isClosed": false, "isClosed": false,
"isComplete": true, "isComplete": true,
"isPen": false, "isPen": false,
"scale": 1,
"segments": [ "segments": [
{ {
"points": [ "points": [

View file

@ -15,6 +15,7 @@ exports[`When resizing a shape with children Resizes a rotated draw shape: draw
"isClosed": false, "isClosed": false,
"isComplete": false, "isComplete": false,
"isPen": false, "isPen": false,
"scale": 1,
"segments": [ "segments": [
{ {
"points": [ "points": [

View file

@ -19,6 +19,7 @@ exports[`editor.packShapes packs rotated shapes: packed shapes 1`] = `
"growY": 0, "growY": 0,
"h": 100, "h": 100,
"labelColor": "black", "labelColor": "black",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -48,6 +49,7 @@ exports[`editor.packShapes packs rotated shapes: packed shapes 1`] = `
"growY": 0, "growY": 0,
"h": 100, "h": 100,
"labelColor": "black", "labelColor": "black",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -77,6 +79,7 @@ exports[`editor.packShapes packs rotated shapes: packed shapes 1`] = `
"growY": 0, "growY": 0,
"h": 100, "h": 100,
"labelColor": "black", "labelColor": "black",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -111,6 +114,7 @@ exports[`editor.packShapes packs shapes: packed shapes 1`] = `
"growY": 0, "growY": 0,
"h": 100, "h": 100,
"labelColor": "black", "labelColor": "black",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -140,6 +144,7 @@ exports[`editor.packShapes packs shapes: packed shapes 1`] = `
"growY": 0, "growY": 0,
"h": 100, "h": 100,
"labelColor": "black", "labelColor": "black",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",
@ -169,6 +174,7 @@ exports[`editor.packShapes packs shapes: packed shapes 1`] = `
"growY": 0, "growY": 0,
"h": 100, "h": 100,
"labelColor": "black", "labelColor": "black",
"scale": 1,
"size": "m", "size": "m",
"text": "", "text": "",
"url": "", "url": "",

View file

@ -46,10 +46,11 @@ export const arrowShapeProps: {
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">; color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">; dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
end: T.Validator<VecModel>; end: T.Validator<VecModel>;
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">; fill: EnumStyleProp<"fill" | "none" | "pattern" | "semi" | "solid">;
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">; font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">; labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
labelPosition: T.Validator<number>; labelPosition: T.Validator<number>;
scale: T.Validator<number>;
size: EnumStyleProp<"l" | "m" | "s" | "xl">; size: EnumStyleProp<"l" | "m" | "s" | "xl">;
start: T.Validator<VecModel>; start: T.Validator<VecModel>;
text: T.Validator<string>; text: T.Validator<string>;
@ -195,7 +196,7 @@ export const DefaultColorThemePalette: {
export const DefaultDashStyle: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">; export const DefaultDashStyle: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
// @public (undocumented) // @public (undocumented)
export const DefaultFillStyle: EnumStyleProp<"none" | "pattern" | "semi" | "solid">; export const DefaultFillStyle: EnumStyleProp<"fill" | "none" | "pattern" | "semi" | "solid">;
// @public (undocumented) // @public (undocumented)
export const DefaultFontFamilies: { export const DefaultFontFamilies: {
@ -235,10 +236,11 @@ export const drawShapeMigrations: TLPropsMigrations;
export const drawShapeProps: { export const drawShapeProps: {
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">; color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">; dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">; fill: EnumStyleProp<"fill" | "none" | "pattern" | "semi" | "solid">;
isClosed: T.Validator<boolean>; isClosed: T.Validator<boolean>;
isComplete: T.Validator<boolean>; isComplete: T.Validator<boolean>;
isPen: T.Validator<boolean>; isPen: T.Validator<boolean>;
scale: T.Validator<number>;
segments: T.ArrayOfValidator<TLDrawShapeSegment>; segments: T.ArrayOfValidator<TLDrawShapeSegment>;
size: EnumStyleProp<"l" | "m" | "s" | "xl">; size: EnumStyleProp<"l" | "m" | "s" | "xl">;
}; };
@ -535,12 +537,13 @@ export const geoShapeProps: {
align: EnumStyleProp<"end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start">; align: EnumStyleProp<"end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start">;
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">; color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">; dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">; fill: EnumStyleProp<"fill" | "none" | "pattern" | "semi" | "solid">;
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">; font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
geo: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "heart" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">; geo: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "heart" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
growY: T.Validator<number>; growY: T.Validator<number>;
h: T.Validator<number>; h: T.Validator<number>;
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">; labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
scale: T.Validator<number>;
size: EnumStyleProp<"l" | "m" | "s" | "xl">; size: EnumStyleProp<"l" | "m" | "s" | "xl">;
text: T.Validator<string>; text: T.Validator<string>;
url: T.Validator<string>; url: T.Validator<string>;
@ -573,6 +576,7 @@ export const highlightShapeProps: {
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">; color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
isComplete: T.Validator<boolean>; isComplete: T.Validator<boolean>;
isPen: T.Validator<boolean>; isPen: T.Validator<boolean>;
scale: T.Validator<number>;
segments: T.ArrayOfValidator<TLDrawShapeSegment>; segments: T.ArrayOfValidator<TLDrawShapeSegment>;
size: EnumStyleProp<"l" | "m" | "s" | "xl">; size: EnumStyleProp<"l" | "m" | "s" | "xl">;
}; };
@ -750,6 +754,7 @@ export const lineShapeProps: {
x: number; x: number;
y: number; y: number;
} & {}>; } & {}>;
scale: T.Validator<number>;
size: EnumStyleProp<"l" | "m" | "s" | "xl">; size: EnumStyleProp<"l" | "m" | "s" | "xl">;
spline: EnumStyleProp<"cubic" | "line">; spline: EnumStyleProp<"cubic" | "line">;
}; };
@ -767,6 +772,7 @@ export const noteShapeProps: {
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">; font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
fontSizeAdjustment: T.Validator<number>; fontSizeAdjustment: T.Validator<number>;
growY: T.Validator<number>; growY: T.Validator<number>;
scale: T.Validator<number>;
size: EnumStyleProp<"l" | "m" | "s" | "xl">; size: EnumStyleProp<"l" | "m" | "s" | "xl">;
text: T.Validator<string>; text: T.Validator<string>;
url: T.Validator<string>; url: T.Validator<string>;
@ -1061,6 +1067,8 @@ export type TLDefaultColorTheme = Expand<{
// @public (undocumented) // @public (undocumented)
export interface TLDefaultColorThemeColor { export interface TLDefaultColorThemeColor {
// (undocumented)
fill: string;
// (undocumented) // (undocumented)
highlight: { highlight: {
p3: string; p3: string;

View file

@ -17,6 +17,7 @@ import { bookmarkShapeVersions } from './shapes/TLBookmarkShape'
import { drawShapeVersions } from './shapes/TLDrawShape' import { drawShapeVersions } from './shapes/TLDrawShape'
import { embedShapeVersions } from './shapes/TLEmbedShape' import { embedShapeVersions } from './shapes/TLEmbedShape'
import { geoShapeVersions } from './shapes/TLGeoShape' import { geoShapeVersions } from './shapes/TLGeoShape'
import { highlightShapeVersions } from './shapes/TLHighlightShape'
import { imageShapeVersions } from './shapes/TLImageShape' import { imageShapeVersions } from './shapes/TLImageShape'
import { lineShapeVersions } from './shapes/TLLineShape' import { lineShapeVersions } from './shapes/TLLineShape'
import { noteShapeVersions } from './shapes/TLNoteShape' import { noteShapeVersions } from './shapes/TLNoteShape'
@ -1901,6 +1902,78 @@ describe('Extract bindings from arrows', () => {
}) })
}) })
describe('Add scale to draw shape', () => {
const { up, down } = getTestMigration(drawShapeVersions.AddScale)
test('up works as expected', () => {
expect(up({ props: {} })).toEqual({ props: { scale: 1 } })
})
test('down works as expected', () => {
expect(down({ props: { scale: 1 } })).toEqual({ props: {} })
})
})
describe('Add scale to highlight shape', () => {
const { up, down } = getTestMigration(highlightShapeVersions.AddScale)
test('up works as expected', () => {
expect(up({ props: {} })).toEqual({ props: { scale: 1 } })
})
test('down works as expected', () => {
expect(down({ props: { scale: 1 } })).toEqual({ props: {} })
})
})
describe('Add scale to geo shape', () => {
const { up, down } = getTestMigration(geoShapeVersions.AddScale)
test('up works as expected', () => {
expect(up({ props: {} })).toEqual({ props: { scale: 1 } })
})
test('down works as expected', () => {
expect(down({ props: { scale: 1 } })).toEqual({ props: {} })
})
})
describe('Add scale to arrow shape', () => {
const { up, down } = getTestMigration(arrowShapeVersions.AddScale)
test('up works as expected', () => {
expect(up({ props: {} })).toEqual({ props: { scale: 1 } })
})
test('down works as expected', () => {
expect(down({ props: { scale: 1 } })).toEqual({ props: {} })
})
})
describe('Add scale to note shape', () => {
const { up, down } = getTestMigration(noteShapeVersions.AddScale)
test('up works as expected', () => {
expect(up({ props: {} })).toEqual({ props: { scale: 1 } })
})
test('down works as expected', () => {
expect(down({ props: { scale: 1 } })).toEqual({ props: {} })
})
})
describe('Add scale to line shape', () => {
const { up, down } = getTestMigration(lineShapeVersions.AddScale)
test('up works as expected', () => {
expect(up({ props: {} })).toEqual({ props: { scale: 1 } })
})
test('down works as expected', () => {
expect(down({ props: { scale: 1 } })).toEqual({ props: {} })
})
})
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */ /* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
// check that all migrator fns were called at least once // check that all migrator fns were called at least once

View file

@ -55,6 +55,7 @@ export const arrowShapeProps = {
bend: T.number, bend: T.number,
text: T.string, text: T.string,
labelPosition: T.number, labelPosition: T.number,
scale: T.nonZeroNumber,
} }
/** @public */ /** @public */
@ -68,6 +69,7 @@ export const arrowShapeVersions = createShapePropsMigrationIds('arrow', {
AddIsPrecise: 2, AddIsPrecise: 2,
AddLabelPosition: 3, AddLabelPosition: 3,
ExtractBindings: 4, ExtractBindings: 4,
AddScale: 5,
}) })
function propsMigration(migration: TLPropsMigration) { function propsMigration(migration: TLPropsMigration) {
@ -197,5 +199,14 @@ export const arrowShapeMigrations = createMigrationSequence({
} }
}, },
}, },
propsMigration({
id: arrowShapeVersions.AddScale,
up: (props) => {
props.scale = 1
},
down: (props) => {
delete props.scale
},
}),
], ],
}) })

View file

@ -29,6 +29,7 @@ export const drawShapeProps = {
isComplete: T.boolean, isComplete: T.boolean,
isClosed: T.boolean, isClosed: T.boolean,
isPen: T.boolean, isPen: T.boolean,
scale: T.nonZeroNumber,
} }
/** @public */ /** @public */
@ -39,6 +40,7 @@ export type TLDrawShape = TLBaseShape<'draw', TLDrawShapeProps>
const Versions = createShapePropsMigrationIds('draw', { const Versions = createShapePropsMigrationIds('draw', {
AddInPen: 1, AddInPen: 1,
AddScale: 2,
}) })
export { Versions as drawShapeVersions } export { Versions as drawShapeVersions }
@ -71,5 +73,14 @@ export const drawShapeMigrations = createShapePropsMigrationSequence({
}, },
down: 'retired', down: 'retired',
}, },
{
id: Versions.AddScale,
up: (props) => {
props.scale = 1
},
down: (props) => {
delete props.scale
},
},
], ],
}) })

View file

@ -60,6 +60,7 @@ export const geoShapeProps = {
h: T.nonZeroNumber, h: T.nonZeroNumber,
growY: T.positiveNumber, growY: T.positiveNumber,
text: T.string, text: T.string,
scale: T.nonZeroNumber,
} }
/** @public */ /** @public */
@ -77,6 +78,7 @@ const geoShapeVersions = createShapePropsMigrationIds('geo', {
MigrateLegacyAlign: 6, MigrateLegacyAlign: 6,
AddCloud: 7, AddCloud: 7,
MakeUrlsValid: 8, MakeUrlsValid: 8,
AddScale: 9,
}) })
export { geoShapeVersions as geoShapeVersions } export { geoShapeVersions as geoShapeVersions }
@ -158,5 +160,14 @@ export const geoShapeMigrations = createShapePropsMigrationSequence({
// noop // noop
}, },
}, },
{
id: geoShapeVersions.AddScale,
up: (props) => {
props.scale = 1
},
down: (props) => {
delete props.scale
},
},
], ],
}) })

View file

@ -1,5 +1,5 @@
import { T } from '@tldraw/validate' import { T } from '@tldraw/validate'
import { createShapePropsMigrationSequence } from '../records/TLShape' import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
import { RecordPropsType } from '../recordsWithProps' import { RecordPropsType } from '../recordsWithProps'
import { DefaultColorStyle } from '../styles/TLColorStyle' import { DefaultColorStyle } from '../styles/TLColorStyle'
import { DefaultSizeStyle } from '../styles/TLSizeStyle' import { DefaultSizeStyle } from '../styles/TLSizeStyle'
@ -13,8 +13,15 @@ export const highlightShapeProps = {
segments: T.arrayOf(DrawShapeSegment), segments: T.arrayOf(DrawShapeSegment),
isComplete: T.boolean, isComplete: T.boolean,
isPen: T.boolean, isPen: T.boolean,
scale: T.nonZeroNumber,
} }
const Versions = createShapePropsMigrationIds('highlight', {
AddScale: 1,
})
export { Versions as highlightShapeVersions }
/** @public */ /** @public */
export type TLHighlightShapeProps = RecordPropsType<typeof highlightShapeProps> export type TLHighlightShapeProps = RecordPropsType<typeof highlightShapeProps>
@ -22,4 +29,16 @@ export type TLHighlightShapeProps = RecordPropsType<typeof highlightShapeProps>
export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps> export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>
/** @public */ /** @public */
export const highlightShapeMigrations = createShapePropsMigrationSequence({ sequence: [] }) export const highlightShapeMigrations = createShapePropsMigrationSequence({
sequence: [
{
id: Versions.AddScale,
up: (props) => {
props.scale = 1
},
down: (props) => {
delete props.scale
},
},
],
})

View file

@ -31,6 +31,7 @@ export const lineShapeProps = {
size: DefaultSizeStyle, size: DefaultSizeStyle,
spline: LineShapeSplineStyle, spline: LineShapeSplineStyle,
points: T.dict(T.string, lineShapePointValidator), points: T.dict(T.string, lineShapePointValidator),
scale: T.nonZeroNumber,
} }
/** @public */ /** @public */
@ -45,6 +46,7 @@ export const lineShapeVersions = createShapePropsMigrationIds('line', {
RemoveExtraHandleProps: 2, RemoveExtraHandleProps: 2,
HandlesToPoints: 3, HandlesToPoints: 3,
PointIndexIds: 4, PointIndexIds: 4,
AddScale: 5,
}) })
/** @public */ /** @public */
@ -155,5 +157,14 @@ export const lineShapeMigrations = createShapePropsMigrationSequence({
props.points = sortedHandles.map(({ x, y }) => ({ x, y })) props.points = sortedHandles.map(({ x, y }) => ({ x, y }))
}, },
}, },
{
id: lineShapeVersions.AddScale,
up: (props) => {
props.scale = 1
},
down: (props) => {
delete props.scale
},
},
], ],
}) })

View file

@ -19,6 +19,7 @@ export const noteShapeProps = {
growY: T.positiveNumber, growY: T.positiveNumber,
url: T.linkUrl, url: T.linkUrl,
text: T.string, text: T.string,
scale: T.nonZeroNumber,
} }
/** @public */ /** @public */
@ -34,6 +35,7 @@ const Versions = createShapePropsMigrationIds('note', {
AddVerticalAlign: 4, AddVerticalAlign: 4,
MakeUrlsValid: 5, MakeUrlsValid: 5,
AddFontSizeAdjustment: 6, AddFontSizeAdjustment: 6,
AddScale: 7,
}) })
export { Versions as noteShapeVersions } export { Versions as noteShapeVersions }
@ -101,5 +103,14 @@ export const noteShapeMigrations = createShapePropsMigrationSequence({
delete props.fontSizeAdjustment delete props.fontSizeAdjustment
}, },
}, },
{
id: Versions.AddScale,
up: (props) => {
props.scale = 1
},
down: (props) => {
delete props.scale
},
},
], ],
}) })

View file

@ -24,6 +24,7 @@ export interface TLDefaultColorThemeColor {
solid: string solid: string
semi: string semi: string
pattern: string pattern: string
fill: string // same as solid
note: { note: {
fill: string fill: string
text: string text: string
@ -56,6 +57,7 @@ export const DefaultColorThemePalette: {
solid: '#fcfffe', solid: '#fcfffe',
black: { black: {
solid: '#1d1d1d', solid: '#1d1d1d',
fill: '#1d1d1d',
note: { note: {
fill: '#FCE19C', fill: '#FCE19C',
text: '#000000', text: '#000000',
@ -69,6 +71,7 @@ export const DefaultColorThemePalette: {
}, },
blue: { blue: {
solid: '#4465e9', solid: '#4465e9',
fill: '#4465e9',
note: { note: {
fill: '#8AA3FF', fill: '#8AA3FF',
text: '#000000', text: '#000000',
@ -82,6 +85,7 @@ export const DefaultColorThemePalette: {
}, },
green: { green: {
solid: '#099268', solid: '#099268',
fill: '#099268',
note: { note: {
fill: '#6FC896', fill: '#6FC896',
text: '#000000', text: '#000000',
@ -95,6 +99,7 @@ export const DefaultColorThemePalette: {
}, },
grey: { grey: {
solid: '#9fa8b2', solid: '#9fa8b2',
fill: '#9fa8b2',
note: { note: {
fill: '#C0CAD3', fill: '#C0CAD3',
text: '#000000', text: '#000000',
@ -108,6 +113,7 @@ export const DefaultColorThemePalette: {
}, },
'light-blue': { 'light-blue': {
solid: '#4ba1f1', solid: '#4ba1f1',
fill: '#4ba1f1',
note: { note: {
fill: '#9BC4FD', fill: '#9BC4FD',
text: '#000000', text: '#000000',
@ -121,6 +127,7 @@ export const DefaultColorThemePalette: {
}, },
'light-green': { 'light-green': {
solid: '#4cb05e', solid: '#4cb05e',
fill: '#4cb05e',
note: { note: {
fill: '#98D08A', fill: '#98D08A',
text: '#000000', text: '#000000',
@ -134,6 +141,7 @@ export const DefaultColorThemePalette: {
}, },
'light-red': { 'light-red': {
solid: '#f87777', solid: '#f87777',
fill: '#f87777',
note: { note: {
fill: '#F7A5A1', fill: '#F7A5A1',
text: '#000000', text: '#000000',
@ -147,6 +155,7 @@ export const DefaultColorThemePalette: {
}, },
'light-violet': { 'light-violet': {
solid: '#e085f4', solid: '#e085f4',
fill: '#e085f4',
note: { note: {
fill: '#DFB0F9', fill: '#DFB0F9',
text: '#000000', text: '#000000',
@ -160,6 +169,7 @@ export const DefaultColorThemePalette: {
}, },
orange: { orange: {
solid: '#e16919', solid: '#e16919',
fill: '#e16919',
note: { note: {
fill: '#FAA475', fill: '#FAA475',
text: '#000000', text: '#000000',
@ -173,6 +183,7 @@ export const DefaultColorThemePalette: {
}, },
red: { red: {
solid: '#e03131', solid: '#e03131',
fill: '#e03131',
note: { note: {
fill: '#FC8282', fill: '#FC8282',
text: '#000000', text: '#000000',
@ -186,6 +197,7 @@ export const DefaultColorThemePalette: {
}, },
violet: { violet: {
solid: '#ae3ec9', solid: '#ae3ec9',
fill: '#ae3ec9',
note: { note: {
fill: '#DB91FD', fill: '#DB91FD',
text: '#000000', text: '#000000',
@ -199,6 +211,7 @@ export const DefaultColorThemePalette: {
}, },
yellow: { yellow: {
solid: '#f1ac4b', solid: '#f1ac4b',
fill: '#f1ac4b',
note: { note: {
fill: '#FED49A', fill: '#FED49A',
text: '#000000', text: '#000000',
@ -212,6 +225,7 @@ export const DefaultColorThemePalette: {
}, },
white: { white: {
solid: '#FFFFFF', solid: '#FFFFFF',
fill: '#FFFFFF',
semi: '#f5f5f5', semi: '#f5f5f5',
pattern: '#f9f9f9', pattern: '#f9f9f9',
note: { note: {
@ -232,6 +246,7 @@ export const DefaultColorThemePalette: {
black: { black: {
solid: '#f2f2f2', solid: '#f2f2f2',
fill: '#f2f2f2',
note: { note: {
fill: '#2c2c2c', fill: '#2c2c2c',
text: '#f2f2f2', text: '#f2f2f2',
@ -245,6 +260,7 @@ export const DefaultColorThemePalette: {
}, },
blue: { blue: {
solid: '#4f72fc', // 3c60f0 solid: '#4f72fc', // 3c60f0
fill: '#4f72fc',
note: { note: {
fill: '#2A3F98', fill: '#2A3F98',
text: '#f2f2f2', text: '#f2f2f2',
@ -258,6 +274,7 @@ export const DefaultColorThemePalette: {
}, },
green: { green: {
solid: '#099268', solid: '#099268',
fill: '#099268',
note: { note: {
fill: '#014429', fill: '#014429',
text: '#f2f2f2', text: '#f2f2f2',
@ -271,6 +288,7 @@ export const DefaultColorThemePalette: {
}, },
grey: { grey: {
solid: '#9398b0', solid: '#9398b0',
fill: '#9398b0',
note: { note: {
fill: '#56595F', fill: '#56595F',
text: '#f2f2f2', text: '#f2f2f2',
@ -284,6 +302,7 @@ export const DefaultColorThemePalette: {
}, },
'light-blue': { 'light-blue': {
solid: '#4dabf7', solid: '#4dabf7',
fill: '#4dabf7',
note: { note: {
fill: '#1F5495', fill: '#1F5495',
text: '#f2f2f2', text: '#f2f2f2',
@ -297,6 +316,7 @@ export const DefaultColorThemePalette: {
}, },
'light-green': { 'light-green': {
solid: '#40c057', solid: '#40c057',
fill: '#40c057',
note: { note: {
fill: '#21581D', fill: '#21581D',
text: '#f2f2f2', text: '#f2f2f2',
@ -310,6 +330,7 @@ export const DefaultColorThemePalette: {
}, },
'light-red': { 'light-red': {
solid: '#ff8787', solid: '#ff8787',
fill: '#ff8787',
note: { note: {
fill: '#923632', fill: '#923632',
text: '#f2f2f2', text: '#f2f2f2',
@ -323,6 +344,7 @@ export const DefaultColorThemePalette: {
}, },
'light-violet': { 'light-violet': {
solid: '#e599f7', solid: '#e599f7',
fill: '#e599f7',
note: { note: {
fill: '#762F8E', fill: '#762F8E',
text: '#f2f2f2', text: '#f2f2f2',
@ -336,6 +358,7 @@ export const DefaultColorThemePalette: {
}, },
orange: { orange: {
solid: '#f76707', solid: '#f76707',
fill: '#f76707',
note: { note: {
fill: '#843906', fill: '#843906',
text: '#f2f2f2', text: '#f2f2f2',
@ -349,6 +372,7 @@ export const DefaultColorThemePalette: {
}, },
red: { red: {
solid: '#e03131', solid: '#e03131',
fill: '#e03131',
note: { note: {
fill: '#89231A', fill: '#89231A',
text: '#f2f2f2', text: '#f2f2f2',
@ -362,6 +386,7 @@ export const DefaultColorThemePalette: {
}, },
violet: { violet: {
solid: '#ae3ec9', solid: '#ae3ec9',
fill: '#ae3ec9',
note: { note: {
fill: '#681683', fill: '#681683',
text: '#f2f2f2', text: '#f2f2f2',
@ -375,6 +400,7 @@ export const DefaultColorThemePalette: {
}, },
yellow: { yellow: {
solid: '#ffc034', solid: '#ffc034',
fill: '#ffc034',
note: { note: {
fill: '#98571B', fill: '#98571B',
text: '#f2f2f2', text: '#f2f2f2',
@ -388,6 +414,7 @@ export const DefaultColorThemePalette: {
}, },
white: { white: {
solid: '#f3f3f3', solid: '#f3f3f3',
fill: '#f3f3f3',
semi: '#f5f5f5', semi: '#f5f5f5',
pattern: '#f9f9f9', pattern: '#f9f9f9',
note: { note: {

View file

@ -4,7 +4,7 @@ import { StyleProp } from './StyleProp'
/** @public */ /** @public */
export const DefaultFillStyle = StyleProp.defineEnum('tldraw:fill', { export const DefaultFillStyle = StyleProp.defineEnum('tldraw:fill', {
defaultValue: 'none', defaultValue: 'none',
values: ['none', 'semi', 'solid', 'pattern'], values: ['none', 'semi', 'solid', 'pattern', 'fill'],
}) })
/** @public */ /** @public */

View file

@ -57,6 +57,7 @@ const oldArrow: TLBaseShape<'arrow', Omit<TLArrowShapeProps, 'labelColor'>> = {
text: '', text: '',
font: 'draw', font: 'draw',
labelPosition: 0.5, labelPosition: 0.5,
scale: 1,
}, },
meta: {}, meta: {},
} }