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-focus-mode.menu": "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": "Toggle grid",
"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 iconsEdit from './icons/icon/edit.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 iconsFillPattern from './icons/icon/fill-pattern.svg'
import iconsFillSemi from './icons/icon/fill-semi.svg'
@ -266,6 +267,7 @@ export function getAssetUrlsByImport(opts) {
duplicate: formatAssetUrl(iconsDuplicate, opts),
edit: formatAssetUrl(iconsEdit, opts),
'external-link': formatAssetUrl(iconsExternalLink, opts),
'fill-fill': formatAssetUrl(iconsFillFill, opts),
'fill-none': formatAssetUrl(iconsFillNone, opts),
'fill-pattern': formatAssetUrl(iconsFillPattern, 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 iconsEdit from './icons/icon/edit.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 iconsFillPattern from './icons/icon/fill-pattern.svg?url'
import iconsFillSemi from './icons/icon/fill-semi.svg?url'
@ -266,6 +267,7 @@ export function getAssetUrlsByImport(opts) {
duplicate: formatAssetUrl(iconsDuplicate, opts),
edit: formatAssetUrl(iconsEdit, opts),
'external-link': formatAssetUrl(iconsExternalLink, opts),
'fill-fill': formatAssetUrl(iconsFillFill, opts),
'fill-none': formatAssetUrl(iconsFillNone, opts),
'fill-pattern': formatAssetUrl(iconsFillPattern, opts),
'fill-semi': formatAssetUrl(iconsFillSemi, opts),

View file

@ -68,6 +68,7 @@ export function getAssetUrls(opts) {
duplicate: formatAssetUrl('./icons/icon/duplicate.svg', opts),
edit: formatAssetUrl('./icons/icon/edit.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-pattern': formatAssetUrl('./icons/icon/fill-pattern.svg', opts),
'fill-semi': formatAssetUrl('./icons/icon/fill-semi.svg', opts),

View file

@ -58,6 +58,7 @@ export type AssetUrls = {
duplicate: string
edit: string
'external-link': string
'fill-fill': string
'fill-none': string
'fill-pattern': 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,
opts
),
'fill-fill': formatAssetUrl(
new URL('./icons/icon/fill-fill.svg', import.meta.url).href,
opts
),
'fill-none': formatAssetUrl(
new URL('./icons/icon/fill-none.svg', import.meta.url).href,
opts

View file

@ -717,6 +717,7 @@ export const defaultUserPreferences: Readonly<{
color: "#02B1CC" | "#11B3A3" | "#39B178" | "#55B467" | "#7B66DC" | "#9D5BD2" | "#BD54C6" | "#E34BA9" | "#EC5E41" | "#F04F88" | "#F2555A" | "#FF802B";
edgeScrollSpeed: 1;
isDarkMode: false;
isDynamicSizeMode: false;
isSnapMode: 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";
@ -3336,6 +3337,8 @@ export interface TLUserPreferences {
// (undocumented)
isDarkMode?: boolean | null;
// (undocumented)
isDynamicSizeMode?: boolean | null;
// (undocumented)
isSnapMode?: boolean | null;
// (undocumented)
isWrapMode?: boolean | null;
@ -3442,6 +3445,8 @@ export class UserPreferencesManager {
// (undocumented)
getIsDarkMode(): boolean;
// (undocumented)
getIsDynamicResizeMode(): boolean;
// (undocumented)
getIsSnapMode(): boolean;
// (undocumented)
getIsWrapMode(): boolean;
@ -3455,6 +3460,7 @@ export class UserPreferencesManager {
color: string;
id: string;
isDarkMode: boolean;
isDynamicResizeMode: boolean;
isSnapMode: boolean;
isWrapMode: boolean;
locale: string;

View file

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

View file

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

View file

@ -19,11 +19,8 @@ export function DefaultHandle({ handle, isCoarse, className, zoom }: TLHandlePro
if (handle.type === 'clone') {
// 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}`
// 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])
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 (
<g className={classNames(`tl-handle tl-handle__${handle.type}`, className)}>
<circle className="tl-handle__bg" r={br} />

View file

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

View file

@ -29,6 +29,7 @@ export class UserPreferencesManager {
isSnapMode: this.getIsSnapMode(),
isDarkMode: this.getIsDarkMode(),
isWrapMode: this.getIsWrapMode(),
isDynamicResizeMode: this.getIsDynamicResizeMode(),
}
}
@computed getIsDarkMode() {
@ -72,4 +73,10 @@ export class UserPreferencesManager {
@computed getIsWrapMode() {
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 { structuredClone } from '@tldraw/utils'
import { Vec } from '../../../../primitives/Vec'
import { TLBaseBoxShape } from '../../../shapes/BaseBoxShapeUtil'
import { TLEventHandlers } from '../../../types/event-types'
@ -85,6 +86,8 @@ export class Pointing extends StateNode {
this.editor.mark(this.markId)
// todo: add scale here when dynamic size is enabled
this.editor.createShapes<TLBaseBoxShape>([
{
id,
@ -95,20 +98,35 @@ export class Pointing extends StateNode {
])
const shape = this.editor.getShape<TLBaseBoxShape>(id)!
const { w, h } = this.editor.getShapeUtil(shape).getDefaultProps() as TLBaseBoxShape['props']
const delta = new Vec(w / 2, h / 2)
if (!shape) {
this.cancel()
return
}
let { w, h } = shape.props
const delta = new Vec(w / 2, h / 2)
const parentTransform = this.editor.getShapeParentTransform(shape)
if (parentTransform) delta.rot(-parentTransform.rotation())
let scale = 1
this.editor.updateShapes<TLBaseBoxShape>([
{
id,
type: shapeType,
x: shape.x - delta.x,
y: shape.y - delta.y,
},
])
if (this.editor.user.getIsDynamicResizeMode()) {
scale = 1 / this.editor.getZoomLevel()
w *= scale
h *= scale
delta.mul(scale)
}
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])

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 { NoteShapeTool } from './lib/shapes/note/NoteShapeTool'
export { NoteShapeUtil } from './lib/shapes/note/NoteShapeUtil'
export { useDefaultColorTheme } from './lib/shapes/shared/ShapeFill'
export { TextLabel, type TextLabelProps } from './lib/shapes/shared/TextLabel'
export {
FONT_FAMILIES,
@ -46,6 +45,7 @@ export {
TEXT_PROPS,
} from './lib/shapes/shared/default-shape-constants'
export { getPerfectDashProps } from './lib/shapes/shared/getPerfectDashProps'
export { useDefaultColorTheme } from './lib/shapes/shared/useDefaultColorTheme'
export { useEditableText } from './lib/shapes/shared/useEditableText'
export { TextShapeTool } from './lib/shapes/text/TextShapeTool'
export { TextShapeUtil } from './lib/shapes/text/TextShapeUtil'

View file

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

View file

@ -52,10 +52,12 @@ function getArrowLabelSize(editor: Editor, shape: TLArrowShape) {
if (shape.props.text.trim()) {
const bodyBounds = bodyGeom.bounds
const fontSize = getArrowLabelFontSize(shape)
const { w, h } = editor.textMeasure.measureText(shape.props.text, {
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
fontSize,
maxWidth: null,
})
@ -70,7 +72,7 @@ function getArrowLabelSize(editor: Editor, shape: TLArrowShape) {
{
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
fontSize,
maxWidth: width,
}
)
@ -79,15 +81,15 @@ function getArrowLabelSize(editor: Editor, shape: TLArrowShape) {
height = squishedHeight
}
if (width > 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]) {
width = 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]
if (width > 16 * fontSize) {
width = 16 * fontSize
const { w: squishedWidth, h: squishedHeight } = editor.textMeasure.measureText(
shape.props.text,
{
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
fontSize,
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)
return size
}
function getLabelToArrowPadding(editor: Editor, shape: TLArrowShape) {
function getLabelToArrowPadding(shape: TLArrowShape) {
const strokeWidth = STROKE_SIZES[shape.props.size]
const labelToArrowPadding =
LABEL_TO_ARROW_PADDING +
(strokeWidth - STROKE_SIZES.s) * 2 +
(strokeWidth === STROKE_SIZES.xl ? 20 : 0)
(LABEL_TO_ARROW_PADDING +
(strokeWidth - STROKE_SIZES.s) * 2 +
(strokeWidth === STROKE_SIZES.xl ? 20 : 0)) *
shape.props.scale
return labelToArrowPadding
}
@ -122,7 +125,7 @@ function getStraightArrowLabelRange(
info: Extract<TLArrowInfo, { isStraight: true }>
): { start: number; end: number } {
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:
const startOffset = Vec.Nudge(info.start.point, info.end.point, labelToArrowPadding)
@ -165,7 +168,7 @@ function getCurvedArrowLabelRange(
info: Extract<TLArrowInfo, { isStraight: false }>
): { start: number; end: number; dbg?: Geometry2d[] } {
const labelSize = getArrowLabelSize(editor, shape)
const labelToArrowPadding = getLabelToArrowPadding(editor, shape)
const labelToArrowPadding = getLabelToArrowPadding(shape)
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:
@ -351,3 +354,7 @@ function interpolateArcAngles(angleStart: number, angleEnd: number, direction: n
const dist = angleDistance(angleStart, angleEnd, direction)
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 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)
}
@ -101,7 +104,7 @@ export function getCurvedArrowInfo(
let offsetA = 0
let offsetB = 0
let minLength = MIN_ARROW_LENGTH
let minLength = MIN_ARROW_LENGTH * shape.props.scale
if (startShapeInfo && !startShapeInfo.isExact) {
const startInPageSpace = Mat.applyToPoint(arrowPageTransform, tempA)
@ -165,8 +168,8 @@ export function getCurvedArrowInfo(
('size' in startShapeInfo.shape.props
? STROKE_SIZES[startShapeInfo.shape.props.size] / 2
: 0)
offsetA = BOUND_ARROW_OFFSET + strokeOffset
minLength += strokeOffset
offsetA = (BOUND_ARROW_OFFSET + strokeOffset) * shape.props.scale
minLength += strokeOffset * shape.props.scale
}
}
}
@ -237,8 +240,8 @@ export function getCurvedArrowInfo(
const strokeOffset =
STROKE_SIZES[shape.props.size] / 2 +
('size' in endShapeInfo.shape.props ? STROKE_SIZES[endShapeInfo.shape.props.size] / 2 : 0)
offsetB = BOUND_ARROW_OFFSET + strokeOffset
minLength += strokeOffset
offsetB = (BOUND_ARROW_OFFSET + strokeOffset) * shape.props.scale
minLength += strokeOffset * shape.props.scale
}
}
}
@ -257,15 +260,15 @@ export function getCurvedArrowInfo(
const tB = tempB.clone()
if (offsetA !== 0) {
const n = (offsetA / lAB) * (isClockwise ? 1 : -1)
const u = Vec.FromAngle(aCA + dAB * n)
tA.setTo(handleArc.center).add(u.mul(handleArc.radius))
tA.setTo(handleArc.center).add(
Vec.FromAngle(aCA + dAB * ((offsetA / lAB) * (isClockwise ? 1 : -1))).mul(handleArc.radius)
)
}
if (offsetB !== 0) {
const n = (offsetB / lAB) * (isClockwise ? -1 : 1)
const u = Vec.FromAngle(aCB + dAB * n)
tB.setTo(handleArc.center).add(u.mul(handleArc.radius))
tB.setTo(handleArc.center).add(
Vec.FromAngle(aCB + dAB * ((offsetB / lAB) * (isClockwise ? -1 : 1))).mul(handleArc.radius)
)
}
if (Vec.DistMin(tA, tB, minLength)) {
@ -282,15 +285,19 @@ export function getCurvedArrowInfo(
}
if (offsetA !== 0) {
const n = (offsetA / lAB) * (isClockwise ? 1 : -1)
const u = Vec.FromAngle(aCA + dAB * n)
tempA.setTo(handleArc.center).add(u.mul(handleArc.radius))
tempA
.setTo(handleArc.center)
.add(
Vec.FromAngle(aCA + dAB * ((offsetA / lAB) * (isClockwise ? 1 : -1))).mul(handleArc.radius)
)
}
if (offsetB !== 0) {
const n = (offsetB / lAB) * (isClockwise ? -1 : 1)
const u = Vec.FromAngle(aCB + dAB * n)
tempB.setTo(handleArc.center).add(u.mul(handleArc.radius))
tempB
.setTo(handleArc.center)
.add(
Vec.FromAngle(aCB + dAB * ((offsetB / lAB) * (isClockwise ? -1 : 1))).mul(handleArc.radius)
)
}
// Did we miss intersections? This happens when we have overlapping shapes.
@ -318,9 +325,16 @@ export function getCurvedArrowInfo(
(endShapeInfo && !endShapeInfo.didIntersect) ||
distFn(handle_aCA, aCA) > distFn(handle_aCA, aCB)
) {
const n = Math.min(0.9, MIN_ARROW_LENGTH / lAB) * (isClockwise ? 1 : -1)
const u = Vec.FromAngle(aCA + dAB * n)
tempB.setTo(handleArc.center).add(u.mul(handleArc.radius))
tempB
.setTo(handleArc.center)
.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
if (!isClockwise) dAB = PI2 - dAB
const n = 0.5 * (isClockwise ? 1 : -1)
const u = Vec.FromAngle(aCA + dAB * n)
tempC.setTo(center).add(u.mul(radius))
tempC.setTo(center).add(Vec.FromAngle(aCA + dAB * (0.5 * (isClockwise ? 1 : -1))).mul(radius))
if (dAB > originalArcLength) {
tempC.rotWith(center, PI)

View file

@ -13,8 +13,10 @@ import { createComputedCache } from '@tldraw/store'
import { getCurvedArrowInfo } from './curved-arrow'
import { getStraightArrowInfo } from './straight-arrow'
const MIN_ARROW_BEND = 8
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> {

View file

@ -82,7 +82,7 @@ export function getStraightArrowInfo(
let offsetB = 0
let strokeOffsetA = 0
let strokeOffsetB = 0
let minLength = MIN_ARROW_LENGTH
let minLength = MIN_ARROW_LENGTH * shape.props.scale
const isSelfIntersection =
startShapeInfo && endShapeInfo && startShapeInfo.shape === endShapeInfo.shape
@ -105,14 +105,14 @@ export function getStraightArrowInfo(
// a short arrow ending at the end shape intersection.
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) {
// ...and if only the end shape intersected, or if neither
// shape intersected, then make it a short arrow starting
// at the start shape intersection.
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
? STROKE_SIZES[startShapeInfo.shape.props.size] / 2
: 0)
offsetA = BOUND_ARROW_OFFSET + strokeOffsetA
minLength += strokeOffsetA
offsetA = (BOUND_ARROW_OFFSET + strokeOffsetA) * shape.props.scale
minLength += strokeOffsetA * shape.props.scale
}
// If the arrow is bound non-exact to an end shape and the
@ -151,8 +151,8 @@ export function getStraightArrowInfo(
strokeOffsetB =
STROKE_SIZES[shape.props.size] / 2 +
('size' in endShapeInfo.shape.props ? STROKE_SIZES[endShapeInfo.shape.props.size] / 2 : 0)
offsetB = BOUND_ARROW_OFFSET + strokeOffsetB
minLength += strokeOffsetB
offsetB = (BOUND_ARROW_OFFSET + strokeOffsetB) * shape.props.scale
minLength += strokeOffsetB * shape.props.scale
}
}
@ -187,7 +187,7 @@ export function getStraightArrowInfo(
if (startShapeInfo && endShapeInfo) {
// If we have two bound shapes...then make the arrow a short arrow from
// 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))
} else {

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import {
TLDrawShape,
TLDrawShapeSegment,
Vec,
modulate,
} from '@tldraw/editor'
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 => {
return {
size: 1 + strokeWidth,
size: strokeWidth,
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,
easing: EASINGS.easeOutSine,
simulatePressure: true,
@ -35,9 +36,9 @@ const realPressureSettings = (strokeWidth: number): StrokeOptions => {
const solidSettings = (strokeWidth: number): StrokeOptions => {
return {
size: 1 + strokeWidth,
size: strokeWidth,
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,
simulatePressure: false,
easing: EASINGS.linear,

View file

@ -268,6 +268,7 @@ export class Drawing extends StateNode {
y: originPagePoint.y,
props: {
isPen: this.isPenOrStylus,
scale: this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1,
segments: [
{
type: this.segmentMode,
@ -415,7 +416,13 @@ export class Drawing extends StateNode {
// ended and where the pointer is now
const newFreeSegment: TLDrawShapeSegment = {
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]

View file

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

View file

@ -51,6 +51,55 @@ describe(GeoShapeTool, () => {
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', () => {

View file

@ -1,6 +1,7 @@
/* eslint-disable react-hooks/rules-of-hooks */
import {
BaseBoxShapeUtil,
Box,
Editor,
Ellipse2d,
Geometry2d,
@ -28,7 +29,6 @@ import {
} from '@tldraw/editor'
import { HyperlinkButton } from '../shared/HyperlinkButton'
import { useDefaultColorTheme } from '../shared/ShapeFill'
import { SvgTextLabel } from '../shared/SvgTextLabel'
import { TextLabel } from '../shared/TextLabel'
import {
@ -43,6 +43,7 @@ import {
getFillDefForExport,
getFontDefForExport,
} from '../shared/defaultStyleDefs'
import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
import { GeoShapeBody } from './components/GeoShapeBody'
import {
cloudOutline,
@ -81,6 +82,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
verticalAlign: 'middle',
growY: 0,
url: '',
scale: 1,
}
}
@ -90,7 +92,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
const cx = w / 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
let body: Geometry2d
@ -318,12 +320,16 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
const labelSize = getLabelSize(this.editor, shape)
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(
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
)
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 edges = lines ? lines.map((line) => new Polyline2d({ points: line })) : []
@ -421,7 +427,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
return (
<>
<SVGContainer id={id}>
<GeoShapeBody shape={shape} />
<GeoShapeBody shape={shape} shouldScale={true} />
</SVGContainer>
{showHtmlContainer && (
<HTMLContainer
@ -435,8 +441,9 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
id={id}
type={type}
font={font}
fontSize={LABEL_FONT_SIZES[size]}
fontSize={LABEL_FONT_SIZES[size] * shape.props.scale}
lineHeight={TEXT_PROPS.lineHeight}
padding={16 * shape.props.scale}
fill={fill}
align={align}
verticalAlign={verticalAlign}
@ -488,7 +495,13 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
let path: string
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)
} else {
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) {
const { props } = shape
ctx.addExportDef(getFillDefForExport(shape.props.fill))
// We need to scale the shape to 1x for export
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
if (props.text) {
ctx.addExportDef(getFontDefForExport(shape.props.font))
ctx.addExportDef(getFontDefForExport(props.font))
const theme = getDefaultColorTheme(ctx)
const bounds = this.editor.getShapeGeometry(shape).bounds
const bounds = new Box(0, 0, props.w, props.h + props.growY)
textEl = (
<SvgTextLabel
fontSize={LABEL_FONT_SIZES[props.size]}
@ -526,13 +548,14 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
text={props.text}
labelColor={theme[props.labelColor].solid}
bounds={bounds}
padding={16}
/>
)
}
return (
<>
<GeoShapeBody shape={shape} />
<GeoShapeBody shouldScale={false} shape={newShape} />
{textEl}
</>
)
@ -782,8 +805,8 @@ function getLabelSize(editor: Editor, shape: TLGeoShape) {
const minSize = editor.textMeasure.measureText('w', {
...TEXT_PROPS,
fontFamily: FONT_FAMILIES[shape.props.font],
fontSize: LABEL_FONT_SIZES[shape.props.size],
maxWidth: 100,
fontSize: LABEL_FONT_SIZES[shape.props.size] * shape.props.scale,
maxWidth: 100, // ?
})
// TODO: Can I get these from somewhere?
@ -797,7 +820,7 @@ function getLabelSize(editor: Editor, shape: TLGeoShape) {
const size = editor.textMeasure.measureText(text, {
...TEXT_PROPS,
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,
maxWidth: Math.max(
// 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 { ShapeFill, useDefaultColorTheme } from '../../shared/ShapeFill'
import { ShapeFill } from '../../shared/ShapeFill'
import { STROKE_SIZES } from '../../shared/default-shape-constants'
import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
import { useDefaultColorTheme } from '../../shared/useDefaultColorTheme'
import {
getCloudArcs,
getCloudPath,
@ -14,12 +15,13 @@ import {
} from '../geo-shape-helpers'
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 theme = useDefaultColorTheme()
const { id, props } = shape
const { w, color, fill, dash, growY, size } = props
const strokeWidth = STROKE_SIZES[size]
const strokeWidth = STROKE_SIZES[size] * scaleToUse
const h = props.h + growY
switch (props.geo) {
@ -28,7 +30,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
const d = getCloudPath(w, h, id, size)
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" />
</>
)
@ -36,17 +38,17 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
const d = inkyCloudSvgPath(w, h, id, size)
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" />
</>
)
} else {
const innerPath = getCloudPath(w, h, id, size)
const d = getCloudPath(w, h, id, size)
const arcs = getCloudArcs(w, h, id, size)
return (
<>
<ShapeFill theme={theme} d={innerPath} color={color} fill={fill} />
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
<g
strokeWidth={strokeWidth}
stroke={theme[color].solid}
@ -91,7 +93,11 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
}
}
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)
if (dash === 'dashed' || dash === 'dotted') {
@ -108,7 +114,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
return (
<>
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
<path
d={d}
strokeWidth={strokeWidth}
@ -120,18 +126,26 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
</>
)
} 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)
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" />
</>
)
}
}
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)
if (dash === 'dashed' || dash === 'dotted') {
const perimeter = geometry.getLength()
@ -149,7 +163,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
return (
<>
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
<path
d={d}
strokeWidth={strokeWidth}
@ -163,7 +177,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
} else {
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" />
</>
)
@ -176,7 +190,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
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) => {
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
c.length,
@ -208,14 +222,19 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
const d = getDrawHeartPath(w, h, strokeWidth, shape.id)
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" />
</>
)
}
}
default: {
const geometry = editor.getShapeGeometry(shape)
const geometry = shouldScale
? // cached
editor.getShapeGeometry(shape)
: // not cached
editor.getShapeUtil(shape).getGeometry(shape)
const outline =
geometry instanceof Group2d ? geometry.children[0].vertices : geometry.vertices
const lines = getLines(shape.props, strokeWidth)
@ -231,16 +250,16 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
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" />
</>
)
} 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 (
<>
<ShapeFill theme={theme} d={innerPath} fill={fill} color={color} />
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
<g
strokeWidth={strokeWidth}
stroke={theme[color].solid}
@ -304,14 +323,9 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
</>
)
} else if (dash === 'draw') {
const polygonPoints = getRoundedPolygonPoints(
id,
outline,
strokeWidth / 3,
strokeWidth * 2,
2
let d = getRoundedInkyPolygonPath(
getRoundedPolygonPoints(id, outline, strokeWidth / 3, strokeWidth * 2, 2)
)
let d = getRoundedInkyPolygonPath(polygonPoints)
if (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(innerPolygonPoints)
const innerPathData = getRoundedInkyPolygonPath(
getRoundedPolygonPoints(id, outline, 0, strokeWidth * 2, 1)
)
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" />
</>
)

View file

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

View file

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

View file

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

View file

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

View file

@ -95,6 +95,9 @@ export class Pointing extends StateNode {
type: 'line',
x: currentPagePoint.x,
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 {
Box,
Editor,
Group2d,
IndexKey,
@ -24,7 +26,6 @@ import { useCallback } from 'react'
import { useCurrentTranslation } from '../../ui/hooks/useTranslation/useTranslation'
import { isRightToLeftLanguage } from '../../utils/text/text'
import { HyperlinkButton } from '../shared/HyperlinkButton'
import { useDefaultColorTheme } from '../shared/ShapeFill'
import { SvgTextLabel } from '../shared/SvgTextLabel'
import { TextLabel } from '../shared/TextLabel'
import {
@ -35,8 +36,8 @@ import {
} from '../shared/default-shape-constants'
import { getFontDefForExport } from '../shared/defaultStyleDefs'
import { useDefaultColorTheme } from '../../..'
import { startEditingShapeWithLabel } from '../../tools/SelectTool/selectHelpers'
import { useForceSolid } from '../shared/useForceSolid'
import {
CLONE_HANDLE_MARGIN,
NOTE_CENTER_OFFSET,
@ -65,31 +66,37 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
growY: 0,
fontSizeAdjustment: 0,
url: '',
scale: 1,
}
}
getGeometry(shape: TLNoteShape) {
const noteHeight = getNoteHeight(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({
children: [
new Rectangle2d({ width: NOTE_SIZE, height: noteHeight, isFilled: true }),
new Rectangle2d({ width: nw, height: nh, isFilled: true }),
new Rectangle2d({
x:
shape.props.align === 'start'
? 0
: shape.props.align === 'end'
? NOTE_SIZE - labelWidth
: (NOTE_SIZE - labelWidth) / 2,
? nw - lw
: (nw - lw) / 2,
y:
shape.props.verticalAlign === 'start'
? 0
: shape.props.verticalAlign === 'end'
? noteHeight - labelHeight
: (noteHeight - labelHeight) / 2,
width: labelWidth,
height: labelHeight,
? nh - lh
: (nh - lh) / 2,
width: lw,
height: lh,
isFilled: true,
isLabel: true,
}),
@ -98,21 +105,25 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
}
override getHandles(shape: TLNoteShape): TLHandle[] {
const zoom = this.editor.getZoomLevel()
const offset = CLONE_HANDLE_MARGIN / zoom
const noteHeight = getNoteHeight(shape)
const { scale } = shape.props
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 [
{
id: 'bottom',
index: 'a3' as IndexKey,
type: 'clone',
x: NOTE_SIZE / 2,
y: noteHeight + offset,
x: nw / 2,
y: nh + offset,
},
]
}
@ -122,29 +133,29 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
id: 'top',
index: 'a1' as IndexKey,
type: 'clone',
x: NOTE_SIZE / 2,
x: nw / 2,
y: -offset,
},
{
id: 'right',
index: 'a2' as IndexKey,
type: 'clone',
x: NOTE_SIZE + offset,
y: noteHeight / 2,
x: nw + offset,
y: nh / 2,
},
{
id: 'bottom',
index: 'a3' as IndexKey,
type: 'clone',
x: NOTE_SIZE / 2,
y: noteHeight + offset,
x: nw / 2,
y: nh + offset,
},
{
id: 'left',
index: 'a4' as IndexKey,
type: 'clone',
x: -offset,
y: noteHeight / 2,
y: nh / 2,
},
]
}
@ -153,17 +164,15 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
const {
id,
type,
props: { color, font, size, align, text, verticalAlign, fontSizeAdjustment },
props: { scale, color, font, size, align, text, verticalAlign, fontSizeAdjustment },
} = shape
// eslint-disable-next-line react-hooks/rules-of-hooks
const handleKeyDown = useNoteKeydownHandler(id)
// eslint-disable-next-line react-hooks/rules-of-hooks
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(
'shape rotation',
() => 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
// 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()
@ -182,18 +194,18 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
id={id}
className="tl-note__container"
style={{
width: NOTE_SIZE,
height: noteHeight,
width: nw,
height: nh,
backgroundColor: theme[color].note.fill,
borderBottom: hideShadows ? `3px solid rgb(15, 23, 31, .2)` : `none`,
boxShadow: hideShadows ? 'none' : getNoteShadow(shape.id, rotation),
borderBottom: hideShadows ? `${3 * scale}px solid rgb(15, 23, 31, .2)` : `none`,
boxShadow: hideShadows ? 'none' : getNoteShadow(shape.id, rotation, scale),
}}
>
<TextLabel
id={id}
type={type}
font={font}
fontSize={fontSizeAdjustment || LABEL_FONT_SIZES[size]}
fontSize={(fontSizeAdjustment || LABEL_FONT_SIZES[size]) * scale}
lineHeight={TEXT_PROPS.lineHeight}
align={align}
verticalAlign={verticalAlign}
@ -202,6 +214,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
isSelected={isSelected}
labelColor={theme[color].note.text}
wrap
padding={16 * scale}
onKeyDown={handleKeyDown}
/>
</div>
@ -213,10 +226,11 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
}
indicator(shape: TLNoteShape) {
const { scale } = shape.props
return (
<rect
rx="1"
width={toDomPrecision(NOTE_SIZE)}
rx={scale}
width={toDomPrecision(NOTE_SIZE * scale)}
height={toDomPrecision(getNoteHeight(shape))}
/>
)
@ -226,7 +240,8 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
ctx.addExportDef(getFontDefForExport(shape.props.font))
if (shape.props.text) ctx.addExportDef(getFontDefForExport(shape.props.font))
const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode })
const bounds = this.editor.getShapeGeometry(shape).bounds
const bounds = getBoundsForSVG(shape)
return (
<>
<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.
*/
function getNoteLabelSize(editor: Editor, shape: TLNoteShape) {
const text = shape.props.text
const { text } = shape.props
if (!text) {
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)
return {
labelHeight,
labelWidth,
fontSizeAdjustment,
labelHeight: labelHeight,
labelWidth: labelWidth,
fontSizeAdjustment: fontSizeAdjustment,
}
}
@ -400,17 +415,18 @@ function useNoteKeydownHandler(id: TLShapeId) {
const isRTL = !!(translation.dir === 'rtl' || isRightToLeftLanguage(shape.props.text))
const offsetLength =
NOTE_SIZE +
editor.options.adjacentShapeMargin +
// If we're growing down, we need to account for the current shape's growY
(isCmdEnter && !e.shiftKey ? shape.props.growY : 0)
(NOTE_SIZE +
editor.options.adjacentShapeMargin +
// If we're growing down, we need to account for the current shape's growY
(isCmdEnter && !e.shiftKey ? shape.props.growY : 0)) *
shape.props.scale
const adjacentCenter = new Vec(
isTab ? (e.shiftKey != isRTL ? -1 : 1) : 0,
isCmdEnter ? (e.shiftKey ? -1 : 1) : 0
)
.mul(offsetLength)
.add(NOTE_CENTER_OFFSET)
.add(NOTE_CENTER_OFFSET.clone().mul(shape.props.scale))
.rot(pageRotation)
.add(pageTransform.point())
@ -427,14 +443,23 @@ function useNoteKeydownHandler(id: TLShapeId) {
}
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 lift = Math.abs(random()) + 0.5 // 0 to 1.5
const oy = Math.cos(rotation)
return `0px ${5 - lift}px 5px -5px rgba(15, 23, 31, .6),
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)}),
0px 48px 10px -10px inset rgba(15, 23, 44, ${((0.022 + random() * 0.005) * ((1 + oy) / 2)).toFixed(2)})`
const a = 5 * scale
const b = 4 * scale
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 */
export const NOTE_SIZE = 200
/** @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 */
export const NOTE_PIT_RADIUS = 10
export const NOTE_ADJACENT_POSITION_SNAP_RADIUS = 10
const DEFAULT_PITS = {
['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
['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
const BASE_NOTE_POSITIONS = [
[['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
[['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
] 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,
pageRotation: number,
growY: number,
extraHeight: number
extraHeight: number,
scale: number
): Record<IndexKey, Vec> {
return Object.fromEntries(
Object.entries(DEFAULT_PITS).map(([id, v], i) => {
getBaseAdjacentNotePositions(scale).map(([id, v], i) => {
const point = v.clone()
if (i === 0 && extraHeight) {
// apply top margin (the growY of the moving note shape)
@ -60,6 +73,7 @@ export function getNoteAdjacentPositions(
export function getAvailableNoteAdjacentPositions(
editor: Editor,
rotation: number,
scale: number,
extraHeight: number
) {
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
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
}
@ -84,7 +102,7 @@ export function getAvailableNoteAdjacentPositions(
// And push its position to the positions array
positions.push(
...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
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--) {
const otherNote = allShapesOnPage[i]
@ -158,7 +176,7 @@ export function getNoteShapeForAdjacentPosition(
const id = createShapeId()
// 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({
id,
type: 'note',
@ -179,13 +197,16 @@ export function getNoteShapeForAdjacentPosition(
// 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
// space as the newly created shape (i.e its parent's space)
// We need to put the page point in the same coordinate space as the newly created shape (i.e its parent's space)
const topLeft = editor.getPointInParentSpace(
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({

View file

@ -9,7 +9,10 @@ import {
Vec,
createShapeId,
} from '@tldraw/editor'
import { NOTE_PIT_RADIUS, getAvailableNoteAdjacentPositions } from '../noteHelpers'
import {
NOTE_ADJACENT_POSITION_SNAP_RADIUS,
getAvailableNoteAdjacentPositions,
} from '../noteHelpers'
export class Pointing extends StateNode {
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
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) {
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) {
const id = createShapeId()
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) {
center.sub(offset)
}
this.shape = createSticky(this.editor, id, center)
this.shape = createNoteShape(this.editor, id, center)
}
this.editor.setCurrentTool('select.translating', {
@ -107,10 +118,10 @@ export class Pointing extends StateNode {
}
}
export function getNotePitOffset(editor: Editor, center: Vec) {
let min = NOTE_PIT_RADIUS / editor.getZoomLevel() // in screen space
export function getNoteShapeAdjacentPositionOffset(editor: Editor, center: Vec, scale: number) {
let min = NOTE_ADJACENT_POSITION_SNAP_RADIUS / editor.getZoomLevel() // in screen space
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
const deltaToPit = Vec.Sub(center, pit)
const dist = deltaToPit.len()
@ -122,13 +133,16 @@ export function getNotePitOffset(editor: Editor, center: Vec) {
return offset
}
export function createSticky(editor: Editor, id: TLShapeId, center: Vec) {
export function createNoteShape(editor: Editor, id: TLShapeId, center: Vec) {
editor
.createShape({
id,
type: 'note',
x: center.x,
y: center.y,
props: {
scale: editor.user.getIsDynamicResizeMode() ? 1 / editor.getZoomLevel() : 1,
},
})
.select(id)

View file

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

View file

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

View file

@ -15,7 +15,7 @@ import {
useValue,
} from '@tldraw/editor'
import { useEffect, useRef, useState } from 'react'
import { useDefaultColorTheme } from './ShapeFill'
import { useDefaultColorTheme } from './useDefaultColorTheme'
/** @public */
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 { 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', () => {
editor.setCurrentTool('text')
editor.setStyleForNextShapes(DefaultTextAlignStyle, 'middle')
const x = 0
const y = 0
editor.pointerDown(x, y)
editor.pointerUp()
const bounds = editor.getShapePageBounds(editor.getCurrentPageShapes()[0])!
expect(editor.getCurrentPageShapes()[0]).toMatchObject({
const shape = editor.getLastCreatedShape()
const bounds = editor.getShapePageBounds(shape)!
expect(shape).toMatchObject({
x: x - bounds.width / 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', () => {
@ -151,7 +184,7 @@ describe('When resizing', () => {
editor.pointerMove(x + 100, y + 100)
expect(editor.getCurrentPageShapes()[0]).toMatchObject({
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,
} from '@tldraw/editor'
import { useCallback } from 'react'
import { useDefaultColorTheme } from '../shared/ShapeFill'
import { SvgTextLabel } from '../shared/SvgTextLabel'
import { TextHelpers } from '../shared/TextHelpers'
import { TextLabel } from '../shared/TextLabel'
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
import { getFontDefForExport } from '../shared/defaultStyleDefs'
import { resizeScaled } from '../shared/resizeScaled'
import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
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) => {
const {
id,
@ -267,29 +249,31 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
}
}
override onDoubleClickEdge = (shape: TLTextShape) => {
// If the shape has a fixed width, set it to autoSize.
if (!shape.props.autoSize) {
return {
id: shape.id,
type: shape.type,
props: {
autoSize: true,
},
}
}
// todo: The edge doubleclicking feels like a mistake more often than
// not, especially on multiline text. Removed June 16 2024
// 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,
},
}
}
}
// override onDoubleClickEdge = (shape: TLTextShape) => {
// // If the shape has a fixed width, set it to autoSize.
// if (!shape.props.autoSize) {
// return {
// id: shape.id,
// type: shape.type,
// 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']) {
@ -310,9 +294,9 @@ function getTextSize(editor: Editor, props: TLTextShape['props']) {
maxWidth: cw,
})
// // If we're autosizing the measureText will essentially `Math.floor`
// // the numbers so `19` rather than `19.3`, this means we must +1 to
// // whatever we get to avoid wrapping.
// If we're autosizing the measureText will essentially `Math.floor`
// the numbers so `19` rather than `19.3`, this means we must +1 to
// whatever we get to avoid wrapping.
if (autoSize) {
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 {
static override id = 'pointing'
@ -22,27 +30,17 @@ export class Pointing extends StateNode {
this.markId = `creating:${id}`
this.editor.mark(this.markId)
this.editor.createShapes<TLTextShape>([
{
id,
type: 'text',
x: originPagePoint.x,
y: originPagePoint.y,
props: {
text: '',
autoSize: false,
w: 20,
},
},
])
const shape = this.createTextShape(id, originPagePoint, false)
if (!shape) {
this.cancel()
return
}
// Now save the fresh reference
this.shape = this.editor.getShape(shape)
this.editor.select(id)
this.shape = this.editor.getShape(id)
if (!this.shape) return
const { shape } = this
this.editor.setCurrentTool('select.resizing', {
...info,
target: 'selection',
@ -77,22 +75,11 @@ export class Pointing extends StateNode {
private complete() {
this.editor.mark('creating text shape')
const id = createShapeId()
const { x, y } = this.editor.inputs.currentPagePoint
this.editor
.createShapes([
{
id,
type: 'text',
x,
y,
props: {
text: '',
autoSize: true,
},
},
])
.select(id)
const { currentPagePoint } = this.editor.inputs
const shape = this.createTextShape(id, currentPagePoint, true)
if (!shape) return
this.editor.select(id)
this.editor.setEditingShape(id)
this.editor.setCurrentTool('select')
this.editor.root.getCurrent()?.transition('editing_shape')
@ -102,4 +89,63 @@ export class Pointing extends StateNode {
this.parent.transition('idle')
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: 'solid', icon: 'fill-solid' },
{ value: 'pattern', icon: 'fill-pattern' },
// { value: 'fill', icon: 'fill-fill' },
],
dash: [
{ value: 'draw', icon: 'dash-draw' },

View file

@ -78,7 +78,7 @@ export class PointingHandle extends StateNode {
// Center the shape on the current pointer
const centeredOnPointer = editor
.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 })
// 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 pagePoint = pageTransform.point()
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]
if (pit) {
return getNoteShapeForAdjacentPosition(editor, shape, pit, pageRotation, forceNew)

View file

@ -16,8 +16,8 @@ import {
moveCameraWhenCloseToEdge,
} from '@tldraw/editor'
import {
NOTE_PIT_RADIUS,
NOTE_SIZE,
NOTE_ADJACENT_POSITION_SNAP_RADIUS,
NOTE_CENTER_OFFSET,
getAvailableNoteAdjacentPositions,
} from '../../../shapes/note/noteHelpers'
import { DragAndDropManager } from '../DragAndDropManager'
@ -353,7 +353,7 @@ function getTranslatingSnapshot(editor: Editor) {
}
let noteAdjacentPositions: Vec[] | undefined
let noteSnapshot: MovingShapeSnapshot | undefined
let noteSnapshot: (MovingShapeSnapshot & { shape: TLNoteShape }) | undefined
const { originPagePoint } = editor.inputs
@ -361,7 +361,7 @@ function getTranslatingSnapshot(editor: Editor) {
(s) =>
editor.isShapeOfType<TLNoteShape>(s.shape, 'note') &&
editor.isPointInShape(s.shape, originPagePoint)
)
) as (MovingShapeSnapshot & { shape: TLNoteShape })[]
if (allHoveredNotes.length === 0) {
// noop
@ -383,7 +383,8 @@ function getTranslatingSnapshot(editor: Editor) {
noteAdjacentPositions = getAvailableNoteAdjacentPositions(
editor,
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 {
// for sticky notes, snap to grid position next to other notes
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)
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) {
// We've already filtered pits with the same page rotation
const deltaToPit = Vec.Sub(pageCenter, pit)

View file

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

View file

@ -607,6 +607,23 @@ export function ToggleDebugModeItem() {
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 --------------------- */
/** @public @react */
export function PrintItem() {

View file

@ -1130,6 +1130,21 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
},
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',
label: {

View file

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

View file

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

View file

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

View file

@ -97,6 +97,8 @@ export const DEFAULT_TRANSLATION = {
'action.toggle-debug-mode': 'Toggle debug mode',
'action.toggle-focus-mode.menu': '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': 'Toggle grid',
'action.toggle-lock': 'Toggle locked',

View file

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

View file

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

View file

@ -329,42 +329,6 @@ describe('When pressing enter on a selected shape', () => {
// })
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', () => {
const id = createShapeId()
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' })
expect(editor.getEditingShapeId()).toBe(null)
editor.expectShapeToMatch({ id, props: { scale: 1, autoSize: true } })
editor.doubleClick(100, 100, { target: 'selection', handle: 'left' })
// Update:
// Previously, double clicking text edges would reset the scale and prevent editing. This is no longer the case.
//
// 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)
})

View file

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

View file

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

View file

@ -19,6 +19,7 @@ exports[`editor.packShapes packs rotated shapes: packed shapes 1`] = `
"growY": 0,
"h": 100,
"labelColor": "black",
"scale": 1,
"size": "m",
"text": "",
"url": "",
@ -48,6 +49,7 @@ exports[`editor.packShapes packs rotated shapes: packed shapes 1`] = `
"growY": 0,
"h": 100,
"labelColor": "black",
"scale": 1,
"size": "m",
"text": "",
"url": "",
@ -77,6 +79,7 @@ exports[`editor.packShapes packs rotated shapes: packed shapes 1`] = `
"growY": 0,
"h": 100,
"labelColor": "black",
"scale": 1,
"size": "m",
"text": "",
"url": "",
@ -111,6 +114,7 @@ exports[`editor.packShapes packs shapes: packed shapes 1`] = `
"growY": 0,
"h": 100,
"labelColor": "black",
"scale": 1,
"size": "m",
"text": "",
"url": "",
@ -140,6 +144,7 @@ exports[`editor.packShapes packs shapes: packed shapes 1`] = `
"growY": 0,
"h": 100,
"labelColor": "black",
"scale": 1,
"size": "m",
"text": "",
"url": "",
@ -169,6 +174,7 @@ exports[`editor.packShapes packs shapes: packed shapes 1`] = `
"growY": 0,
"h": 100,
"labelColor": "black",
"scale": 1,
"size": "m",
"text": "",
"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">;
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
end: T.Validator<VecModel>;
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
fill: EnumStyleProp<"fill" | "none" | "pattern" | "semi" | "solid">;
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">;
labelPosition: T.Validator<number>;
scale: T.Validator<number>;
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
start: T.Validator<VecModel>;
text: T.Validator<string>;
@ -195,7 +196,7 @@ export const DefaultColorThemePalette: {
export const DefaultDashStyle: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
// @public (undocumented)
export const DefaultFillStyle: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
export const DefaultFillStyle: EnumStyleProp<"fill" | "none" | "pattern" | "semi" | "solid">;
// @public (undocumented)
export const DefaultFontFamilies: {
@ -235,10 +236,11 @@ export const drawShapeMigrations: TLPropsMigrations;
export const drawShapeProps: {
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">;
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
fill: EnumStyleProp<"fill" | "none" | "pattern" | "semi" | "solid">;
isClosed: T.Validator<boolean>;
isComplete: T.Validator<boolean>;
isPen: T.Validator<boolean>;
scale: T.Validator<number>;
segments: T.ArrayOfValidator<TLDrawShapeSegment>;
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
};
@ -535,12 +537,13 @@ export const geoShapeProps: {
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">;
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">;
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>;
h: T.Validator<number>;
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">;
text: 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">;
isComplete: T.Validator<boolean>;
isPen: T.Validator<boolean>;
scale: T.Validator<number>;
segments: T.ArrayOfValidator<TLDrawShapeSegment>;
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
};
@ -750,6 +754,7 @@ export const lineShapeProps: {
x: number;
y: number;
} & {}>;
scale: T.Validator<number>;
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
spline: EnumStyleProp<"cubic" | "line">;
};
@ -767,6 +772,7 @@ export const noteShapeProps: {
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
fontSizeAdjustment: T.Validator<number>;
growY: T.Validator<number>;
scale: T.Validator<number>;
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
text: T.Validator<string>;
url: T.Validator<string>;
@ -1061,6 +1067,8 @@ export type TLDefaultColorTheme = Expand<{
// @public (undocumented)
export interface TLDefaultColorThemeColor {
// (undocumented)
fill: string;
// (undocumented)
highlight: {
p3: string;

View file

@ -17,6 +17,7 @@ import { bookmarkShapeVersions } from './shapes/TLBookmarkShape'
import { drawShapeVersions } from './shapes/TLDrawShape'
import { embedShapeVersions } from './shapes/TLEmbedShape'
import { geoShapeVersions } from './shapes/TLGeoShape'
import { highlightShapeVersions } from './shapes/TLHighlightShape'
import { imageShapeVersions } from './shapes/TLImageShape'
import { lineShapeVersions } from './shapes/TLLineShape'
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 --- */
// check that all migrator fns were called at least once

View file

@ -55,6 +55,7 @@ export const arrowShapeProps = {
bend: T.number,
text: T.string,
labelPosition: T.number,
scale: T.nonZeroNumber,
}
/** @public */
@ -68,6 +69,7 @@ export const arrowShapeVersions = createShapePropsMigrationIds('arrow', {
AddIsPrecise: 2,
AddLabelPosition: 3,
ExtractBindings: 4,
AddScale: 5,
})
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,
isClosed: T.boolean,
isPen: T.boolean,
scale: T.nonZeroNumber,
}
/** @public */
@ -39,6 +40,7 @@ export type TLDrawShape = TLBaseShape<'draw', TLDrawShapeProps>
const Versions = createShapePropsMigrationIds('draw', {
AddInPen: 1,
AddScale: 2,
})
export { Versions as drawShapeVersions }
@ -71,5 +73,14 @@ export const drawShapeMigrations = createShapePropsMigrationSequence({
},
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,
growY: T.positiveNumber,
text: T.string,
scale: T.nonZeroNumber,
}
/** @public */
@ -77,6 +78,7 @@ const geoShapeVersions = createShapePropsMigrationIds('geo', {
MigrateLegacyAlign: 6,
AddCloud: 7,
MakeUrlsValid: 8,
AddScale: 9,
})
export { geoShapeVersions as geoShapeVersions }
@ -158,5 +160,14 @@ export const geoShapeMigrations = createShapePropsMigrationSequence({
// 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 { createShapePropsMigrationSequence } from '../records/TLShape'
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
import { RecordPropsType } from '../recordsWithProps'
import { DefaultColorStyle } from '../styles/TLColorStyle'
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
@ -13,8 +13,15 @@ export const highlightShapeProps = {
segments: T.arrayOf(DrawShapeSegment),
isComplete: T.boolean,
isPen: T.boolean,
scale: T.nonZeroNumber,
}
const Versions = createShapePropsMigrationIds('highlight', {
AddScale: 1,
})
export { Versions as highlightShapeVersions }
/** @public */
export type TLHighlightShapeProps = RecordPropsType<typeof highlightShapeProps>
@ -22,4 +29,16 @@ export type TLHighlightShapeProps = RecordPropsType<typeof highlightShapeProps>
export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>
/** @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,
spline: LineShapeSplineStyle,
points: T.dict(T.string, lineShapePointValidator),
scale: T.nonZeroNumber,
}
/** @public */
@ -45,6 +46,7 @@ export const lineShapeVersions = createShapePropsMigrationIds('line', {
RemoveExtraHandleProps: 2,
HandlesToPoints: 3,
PointIndexIds: 4,
AddScale: 5,
})
/** @public */
@ -155,5 +157,14 @@ export const lineShapeMigrations = createShapePropsMigrationSequence({
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,
url: T.linkUrl,
text: T.string,
scale: T.nonZeroNumber,
}
/** @public */
@ -34,6 +35,7 @@ const Versions = createShapePropsMigrationIds('note', {
AddVerticalAlign: 4,
MakeUrlsValid: 5,
AddFontSizeAdjustment: 6,
AddScale: 7,
})
export { Versions as noteShapeVersions }
@ -101,5 +103,14 @@ export const noteShapeMigrations = createShapePropsMigrationSequence({
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
semi: string
pattern: string
fill: string // same as solid
note: {
fill: string
text: string
@ -56,6 +57,7 @@ export const DefaultColorThemePalette: {
solid: '#fcfffe',
black: {
solid: '#1d1d1d',
fill: '#1d1d1d',
note: {
fill: '#FCE19C',
text: '#000000',
@ -69,6 +71,7 @@ export const DefaultColorThemePalette: {
},
blue: {
solid: '#4465e9',
fill: '#4465e9',
note: {
fill: '#8AA3FF',
text: '#000000',
@ -82,6 +85,7 @@ export const DefaultColorThemePalette: {
},
green: {
solid: '#099268',
fill: '#099268',
note: {
fill: '#6FC896',
text: '#000000',
@ -95,6 +99,7 @@ export const DefaultColorThemePalette: {
},
grey: {
solid: '#9fa8b2',
fill: '#9fa8b2',
note: {
fill: '#C0CAD3',
text: '#000000',
@ -108,6 +113,7 @@ export const DefaultColorThemePalette: {
},
'light-blue': {
solid: '#4ba1f1',
fill: '#4ba1f1',
note: {
fill: '#9BC4FD',
text: '#000000',
@ -121,6 +127,7 @@ export const DefaultColorThemePalette: {
},
'light-green': {
solid: '#4cb05e',
fill: '#4cb05e',
note: {
fill: '#98D08A',
text: '#000000',
@ -134,6 +141,7 @@ export const DefaultColorThemePalette: {
},
'light-red': {
solid: '#f87777',
fill: '#f87777',
note: {
fill: '#F7A5A1',
text: '#000000',
@ -147,6 +155,7 @@ export const DefaultColorThemePalette: {
},
'light-violet': {
solid: '#e085f4',
fill: '#e085f4',
note: {
fill: '#DFB0F9',
text: '#000000',
@ -160,6 +169,7 @@ export const DefaultColorThemePalette: {
},
orange: {
solid: '#e16919',
fill: '#e16919',
note: {
fill: '#FAA475',
text: '#000000',
@ -173,6 +183,7 @@ export const DefaultColorThemePalette: {
},
red: {
solid: '#e03131',
fill: '#e03131',
note: {
fill: '#FC8282',
text: '#000000',
@ -186,6 +197,7 @@ export const DefaultColorThemePalette: {
},
violet: {
solid: '#ae3ec9',
fill: '#ae3ec9',
note: {
fill: '#DB91FD',
text: '#000000',
@ -199,6 +211,7 @@ export const DefaultColorThemePalette: {
},
yellow: {
solid: '#f1ac4b',
fill: '#f1ac4b',
note: {
fill: '#FED49A',
text: '#000000',
@ -212,6 +225,7 @@ export const DefaultColorThemePalette: {
},
white: {
solid: '#FFFFFF',
fill: '#FFFFFF',
semi: '#f5f5f5',
pattern: '#f9f9f9',
note: {
@ -232,6 +246,7 @@ export const DefaultColorThemePalette: {
black: {
solid: '#f2f2f2',
fill: '#f2f2f2',
note: {
fill: '#2c2c2c',
text: '#f2f2f2',
@ -245,6 +260,7 @@ export const DefaultColorThemePalette: {
},
blue: {
solid: '#4f72fc', // 3c60f0
fill: '#4f72fc',
note: {
fill: '#2A3F98',
text: '#f2f2f2',
@ -258,6 +274,7 @@ export const DefaultColorThemePalette: {
},
green: {
solid: '#099268',
fill: '#099268',
note: {
fill: '#014429',
text: '#f2f2f2',
@ -271,6 +288,7 @@ export const DefaultColorThemePalette: {
},
grey: {
solid: '#9398b0',
fill: '#9398b0',
note: {
fill: '#56595F',
text: '#f2f2f2',
@ -284,6 +302,7 @@ export const DefaultColorThemePalette: {
},
'light-blue': {
solid: '#4dabf7',
fill: '#4dabf7',
note: {
fill: '#1F5495',
text: '#f2f2f2',
@ -297,6 +316,7 @@ export const DefaultColorThemePalette: {
},
'light-green': {
solid: '#40c057',
fill: '#40c057',
note: {
fill: '#21581D',
text: '#f2f2f2',
@ -310,6 +330,7 @@ export const DefaultColorThemePalette: {
},
'light-red': {
solid: '#ff8787',
fill: '#ff8787',
note: {
fill: '#923632',
text: '#f2f2f2',
@ -323,6 +344,7 @@ export const DefaultColorThemePalette: {
},
'light-violet': {
solid: '#e599f7',
fill: '#e599f7',
note: {
fill: '#762F8E',
text: '#f2f2f2',
@ -336,6 +358,7 @@ export const DefaultColorThemePalette: {
},
orange: {
solid: '#f76707',
fill: '#f76707',
note: {
fill: '#843906',
text: '#f2f2f2',
@ -349,6 +372,7 @@ export const DefaultColorThemePalette: {
},
red: {
solid: '#e03131',
fill: '#e03131',
note: {
fill: '#89231A',
text: '#f2f2f2',
@ -362,6 +386,7 @@ export const DefaultColorThemePalette: {
},
violet: {
solid: '#ae3ec9',
fill: '#ae3ec9',
note: {
fill: '#681683',
text: '#f2f2f2',
@ -375,6 +400,7 @@ export const DefaultColorThemePalette: {
},
yellow: {
solid: '#ffc034',
fill: '#ffc034',
note: {
fill: '#98571B',
text: '#f2f2f2',
@ -388,6 +414,7 @@ export const DefaultColorThemePalette: {
},
white: {
solid: '#f3f3f3',
fill: '#f3f3f3',
semi: '#f5f5f5',
pattern: '#f9f9f9',
note: {

View file

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

View file

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