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>
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 4.8 KiB |
4
assets/icons/icon/fill-fill.svg
Normal 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 |
|
@ -93,6 +93,8 @@
|
||||||
"action.toggle-debug-mode": "Toggle debug mode",
|
"action.toggle-debug-mode": "Toggle debug mode",
|
||||||
"action.toggle-focus-mode.menu": "Focus mode",
|
"action.toggle-focus-mode.menu": "Focus mode",
|
||||||
"action.toggle-focus-mode": "Toggle focus mode",
|
"action.toggle-focus-mode": "Toggle focus mode",
|
||||||
|
"action.toggle-dynamic-size-mode.menu": "Dynamic size",
|
||||||
|
"action.toggle-dynamic-size-mode": "Toggle dynamic size",
|
||||||
"action.toggle-grid.menu": "Show grid",
|
"action.toggle-grid.menu": "Show grid",
|
||||||
"action.toggle-grid": "Toggle grid",
|
"action.toggle-grid": "Toggle grid",
|
||||||
"action.toggle-lock": "Toggle locked",
|
"action.toggle-lock": "Toggle locked",
|
||||||
|
|
|
@ -74,6 +74,7 @@ import iconsDragHandleDots from './icons/icon/drag-handle-dots.svg'
|
||||||
import iconsDuplicate from './icons/icon/duplicate.svg'
|
import iconsDuplicate from './icons/icon/duplicate.svg'
|
||||||
import iconsEdit from './icons/icon/edit.svg'
|
import iconsEdit from './icons/icon/edit.svg'
|
||||||
import iconsExternalLink from './icons/icon/external-link.svg'
|
import iconsExternalLink from './icons/icon/external-link.svg'
|
||||||
|
import iconsFillFill from './icons/icon/fill-fill.svg'
|
||||||
import iconsFillNone from './icons/icon/fill-none.svg'
|
import iconsFillNone from './icons/icon/fill-none.svg'
|
||||||
import iconsFillPattern from './icons/icon/fill-pattern.svg'
|
import iconsFillPattern from './icons/icon/fill-pattern.svg'
|
||||||
import iconsFillSemi from './icons/icon/fill-semi.svg'
|
import iconsFillSemi from './icons/icon/fill-semi.svg'
|
||||||
|
@ -266,6 +267,7 @@ export function getAssetUrlsByImport(opts) {
|
||||||
duplicate: formatAssetUrl(iconsDuplicate, opts),
|
duplicate: formatAssetUrl(iconsDuplicate, opts),
|
||||||
edit: formatAssetUrl(iconsEdit, opts),
|
edit: formatAssetUrl(iconsEdit, opts),
|
||||||
'external-link': formatAssetUrl(iconsExternalLink, opts),
|
'external-link': formatAssetUrl(iconsExternalLink, opts),
|
||||||
|
'fill-fill': formatAssetUrl(iconsFillFill, opts),
|
||||||
'fill-none': formatAssetUrl(iconsFillNone, opts),
|
'fill-none': formatAssetUrl(iconsFillNone, opts),
|
||||||
'fill-pattern': formatAssetUrl(iconsFillPattern, opts),
|
'fill-pattern': formatAssetUrl(iconsFillPattern, opts),
|
||||||
'fill-semi': formatAssetUrl(iconsFillSemi, opts),
|
'fill-semi': formatAssetUrl(iconsFillSemi, opts),
|
||||||
|
|
|
@ -74,6 +74,7 @@ import iconsDragHandleDots from './icons/icon/drag-handle-dots.svg?url'
|
||||||
import iconsDuplicate from './icons/icon/duplicate.svg?url'
|
import iconsDuplicate from './icons/icon/duplicate.svg?url'
|
||||||
import iconsEdit from './icons/icon/edit.svg?url'
|
import iconsEdit from './icons/icon/edit.svg?url'
|
||||||
import iconsExternalLink from './icons/icon/external-link.svg?url'
|
import iconsExternalLink from './icons/icon/external-link.svg?url'
|
||||||
|
import iconsFillFill from './icons/icon/fill-fill.svg?url'
|
||||||
import iconsFillNone from './icons/icon/fill-none.svg?url'
|
import iconsFillNone from './icons/icon/fill-none.svg?url'
|
||||||
import iconsFillPattern from './icons/icon/fill-pattern.svg?url'
|
import iconsFillPattern from './icons/icon/fill-pattern.svg?url'
|
||||||
import iconsFillSemi from './icons/icon/fill-semi.svg?url'
|
import iconsFillSemi from './icons/icon/fill-semi.svg?url'
|
||||||
|
@ -266,6 +267,7 @@ export function getAssetUrlsByImport(opts) {
|
||||||
duplicate: formatAssetUrl(iconsDuplicate, opts),
|
duplicate: formatAssetUrl(iconsDuplicate, opts),
|
||||||
edit: formatAssetUrl(iconsEdit, opts),
|
edit: formatAssetUrl(iconsEdit, opts),
|
||||||
'external-link': formatAssetUrl(iconsExternalLink, opts),
|
'external-link': formatAssetUrl(iconsExternalLink, opts),
|
||||||
|
'fill-fill': formatAssetUrl(iconsFillFill, opts),
|
||||||
'fill-none': formatAssetUrl(iconsFillNone, opts),
|
'fill-none': formatAssetUrl(iconsFillNone, opts),
|
||||||
'fill-pattern': formatAssetUrl(iconsFillPattern, opts),
|
'fill-pattern': formatAssetUrl(iconsFillPattern, opts),
|
||||||
'fill-semi': formatAssetUrl(iconsFillSemi, opts),
|
'fill-semi': formatAssetUrl(iconsFillSemi, opts),
|
||||||
|
|
|
@ -68,6 +68,7 @@ export function getAssetUrls(opts) {
|
||||||
duplicate: formatAssetUrl('./icons/icon/duplicate.svg', opts),
|
duplicate: formatAssetUrl('./icons/icon/duplicate.svg', opts),
|
||||||
edit: formatAssetUrl('./icons/icon/edit.svg', opts),
|
edit: formatAssetUrl('./icons/icon/edit.svg', opts),
|
||||||
'external-link': formatAssetUrl('./icons/icon/external-link.svg', opts),
|
'external-link': formatAssetUrl('./icons/icon/external-link.svg', opts),
|
||||||
|
'fill-fill': formatAssetUrl('./icons/icon/fill-fill.svg', opts),
|
||||||
'fill-none': formatAssetUrl('./icons/icon/fill-none.svg', opts),
|
'fill-none': formatAssetUrl('./icons/icon/fill-none.svg', opts),
|
||||||
'fill-pattern': formatAssetUrl('./icons/icon/fill-pattern.svg', opts),
|
'fill-pattern': formatAssetUrl('./icons/icon/fill-pattern.svg', opts),
|
||||||
'fill-semi': formatAssetUrl('./icons/icon/fill-semi.svg', opts),
|
'fill-semi': formatAssetUrl('./icons/icon/fill-semi.svg', opts),
|
||||||
|
|
1
packages/assets/types.d.ts
vendored
|
@ -58,6 +58,7 @@ export type AssetUrls = {
|
||||||
duplicate: string
|
duplicate: string
|
||||||
edit: string
|
edit: string
|
||||||
'external-link': string
|
'external-link': string
|
||||||
|
'fill-fill': string
|
||||||
'fill-none': string
|
'fill-none': string
|
||||||
'fill-pattern': string
|
'fill-pattern': string
|
||||||
'fill-semi': string
|
'fill-semi': string
|
||||||
|
|
|
@ -191,6 +191,10 @@ export function getAssetUrlsByMetaUrl(opts) {
|
||||||
new URL('./icons/icon/external-link.svg', import.meta.url).href,
|
new URL('./icons/icon/external-link.svg', import.meta.url).href,
|
||||||
opts
|
opts
|
||||||
),
|
),
|
||||||
|
'fill-fill': formatAssetUrl(
|
||||||
|
new URL('./icons/icon/fill-fill.svg', import.meta.url).href,
|
||||||
|
opts
|
||||||
|
),
|
||||||
'fill-none': formatAssetUrl(
|
'fill-none': formatAssetUrl(
|
||||||
new URL('./icons/icon/fill-none.svg', import.meta.url).href,
|
new URL('./icons/icon/fill-none.svg', import.meta.url).href,
|
||||||
opts
|
opts
|
||||||
|
|
|
@ -717,6 +717,7 @@ export const defaultUserPreferences: Readonly<{
|
||||||
color: "#02B1CC" | "#11B3A3" | "#39B178" | "#55B467" | "#7B66DC" | "#9D5BD2" | "#BD54C6" | "#E34BA9" | "#EC5E41" | "#F04F88" | "#F2555A" | "#FF802B";
|
color: "#02B1CC" | "#11B3A3" | "#39B178" | "#55B467" | "#7B66DC" | "#9D5BD2" | "#BD54C6" | "#E34BA9" | "#EC5E41" | "#F04F88" | "#F2555A" | "#FF802B";
|
||||||
edgeScrollSpeed: 1;
|
edgeScrollSpeed: 1;
|
||||||
isDarkMode: false;
|
isDarkMode: false;
|
||||||
|
isDynamicSizeMode: false;
|
||||||
isSnapMode: false;
|
isSnapMode: false;
|
||||||
isWrapMode: false;
|
isWrapMode: false;
|
||||||
locale: "ar" | "ca" | "cs" | "da" | "de" | "en" | "es" | "fa" | "fi" | "fr" | "gl" | "he" | "hi-in" | "hr" | "hu" | "id" | "it" | "ja" | "ko-kr" | "ku" | "my" | "ne" | "no" | "pl" | "pt-br" | "pt-pt" | "ro" | "ru" | "sl" | "sv" | "te" | "th" | "tr" | "uk" | "vi" | "zh-cn" | "zh-tw";
|
locale: "ar" | "ca" | "cs" | "da" | "de" | "en" | "es" | "fa" | "fi" | "fr" | "gl" | "he" | "hi-in" | "hr" | "hu" | "id" | "it" | "ja" | "ko-kr" | "ku" | "my" | "ne" | "no" | "pl" | "pt-br" | "pt-pt" | "ro" | "ru" | "sl" | "sv" | "te" | "th" | "tr" | "uk" | "vi" | "zh-cn" | "zh-tw";
|
||||||
|
@ -3336,6 +3337,8 @@ export interface TLUserPreferences {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isDarkMode?: boolean | null;
|
isDarkMode?: boolean | null;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
isDynamicSizeMode?: boolean | null;
|
||||||
|
// (undocumented)
|
||||||
isSnapMode?: boolean | null;
|
isSnapMode?: boolean | null;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
isWrapMode?: boolean | null;
|
isWrapMode?: boolean | null;
|
||||||
|
@ -3442,6 +3445,8 @@ export class UserPreferencesManager {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getIsDarkMode(): boolean;
|
getIsDarkMode(): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
getIsDynamicResizeMode(): boolean;
|
||||||
|
// (undocumented)
|
||||||
getIsSnapMode(): boolean;
|
getIsSnapMode(): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getIsWrapMode(): boolean;
|
getIsWrapMode(): boolean;
|
||||||
|
@ -3455,6 +3460,7 @@ export class UserPreferencesManager {
|
||||||
color: string;
|
color: string;
|
||||||
id: string;
|
id: string;
|
||||||
isDarkMode: boolean;
|
isDarkMode: boolean;
|
||||||
|
isDynamicResizeMode: boolean;
|
||||||
isSnapMode: boolean;
|
isSnapMode: boolean;
|
||||||
isWrapMode: boolean;
|
isWrapMode: boolean;
|
||||||
locale: string;
|
locale: string;
|
||||||
|
|
|
@ -1138,7 +1138,7 @@ input,
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
padding: 16px;
|
padding: inherit;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
border-radius: var(--radius-1);
|
border-radius: var(--radius-1);
|
||||||
|
@ -1150,7 +1150,7 @@ input,
|
||||||
inset: 0px;
|
inset: 0px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 16px;
|
padding: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-text-wrapper[data-isselected='true'] .tl-text-input {
|
.tl-text-wrapper[data-isselected='true'] .tl-text-input {
|
||||||
|
@ -1236,12 +1236,12 @@ input,
|
||||||
.tl-arrow-label .tl-arrow {
|
.tl-arrow-label .tl-arrow {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: max-content;
|
height: max-content;
|
||||||
padding: 4px;
|
padding: inherit;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tl-arrow-label textarea {
|
.tl-arrow-label textarea {
|
||||||
padding: 4px;
|
padding: inherit;
|
||||||
/* Don't allow textarea to be zero width */
|
/* Don't allow textarea to be zero width */
|
||||||
min-width: 4px;
|
min-width: 4px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,17 +71,26 @@ export const GeometryDebuggingView = track(function GeometryDebuggingView({
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
>
|
>
|
||||||
{showStroke && <GeometryStroke geometry={geometry} />}
|
{showStroke && (
|
||||||
|
<g
|
||||||
|
stroke={geometry.debugColor ?? 'red'}
|
||||||
|
opacity="1"
|
||||||
|
strokeWidth={2 / zoomLevel}
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<GeometryStroke geometry={geometry} />
|
||||||
|
</g>
|
||||||
|
)}
|
||||||
{showVertices &&
|
{showVertices &&
|
||||||
vertices.map((v, i) => (
|
vertices.map((v, i) => (
|
||||||
<circle
|
<circle
|
||||||
key={`v${i}`}
|
key={`v${i}`}
|
||||||
cx={v.x}
|
cx={v.x}
|
||||||
cy={v.y}
|
cy={v.y}
|
||||||
r="2"
|
r={2 / zoomLevel}
|
||||||
fill={`hsl(${modulate(i, [0, vertices.length - 1], [120, 200])}, 100%, 50%)`}
|
fill={`hsl(${modulate(i, [0, vertices.length - 1], [120, 200])}, 100%, 50%)`}
|
||||||
stroke="black"
|
stroke="black"
|
||||||
strokeWidth="1"
|
strokeWidth={1 / zoomLevel}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{showClosestPointOnOutline && dist < 150 && (
|
{showClosestPointOnOutline && dist < 150 && (
|
||||||
|
@ -92,7 +101,7 @@ export const GeometryDebuggingView = track(function GeometryDebuggingView({
|
||||||
y2={pointInShapeSpace.y}
|
y2={pointInShapeSpace.y}
|
||||||
opacity={1 - dist / 150}
|
opacity={1 - dist / 150}
|
||||||
stroke={hitInside ? 'goldenrod' : 'dodgerblue'}
|
stroke={hitInside ? 'goldenrod' : 'dodgerblue'}
|
||||||
strokeWidth="2"
|
strokeWidth={2 / zoomLevel}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</g>
|
</g>
|
||||||
|
@ -113,13 +122,5 @@ function GeometryStroke({ geometry }: { geometry: Geometry2d }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <path d={geometry.toSimpleSvgPath()} />
|
||||||
<path
|
|
||||||
stroke={geometry.debugColor ?? 'red'}
|
|
||||||
strokeWidth="2"
|
|
||||||
fill="none"
|
|
||||||
opacity="1"
|
|
||||||
d={geometry.toSimpleSvgPath()}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,8 @@ export function DefaultHandle({ handle, isCoarse, className, zoom }: TLHandlePro
|
||||||
|
|
||||||
if (handle.type === 'clone') {
|
if (handle.type === 'clone') {
|
||||||
// bouba
|
// bouba
|
||||||
const fr = 3 / Math.max(zoom, 0.35)
|
const fr = 3 / zoom
|
||||||
const path = `M0,${-fr} A${fr},${fr} 0 0,1 0,${fr}`
|
const path = `M0,${-fr} A${fr},${fr} 0 0,1 0,${fr}`
|
||||||
// kiki
|
|
||||||
// const fr = 4 / Math.max(zoom, 0.35)
|
|
||||||
// const path = `M0,${-fr} L${fr},0 L0,${fr} Z`
|
|
||||||
|
|
||||||
const index = SIDES.indexOf(handle.id as (typeof SIDES)[number])
|
const index = SIDES.indexOf(handle.id as (typeof SIDES)[number])
|
||||||
return (
|
return (
|
||||||
|
@ -35,7 +32,7 @@ export function DefaultHandle({ handle, isCoarse, className, zoom }: TLHandlePro
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = (handle.type === 'create' && isCoarse ? 3 : 4) / Math.max(zoom, 0.35)
|
const fr = (handle.type === 'create' && isCoarse ? 3 : 4) / Math.max(zoom, 0.25)
|
||||||
return (
|
return (
|
||||||
<g className={classNames(`tl-handle tl-handle__${handle.type}`, className)}>
|
<g className={classNames(`tl-handle tl-handle__${handle.type}`, className)}>
|
||||||
<circle className="tl-handle__bg" r={br} />
|
<circle className="tl-handle__bg" r={br} />
|
||||||
|
|
|
@ -21,6 +21,7 @@ export interface TLUserPreferences {
|
||||||
isDarkMode?: boolean | null
|
isDarkMode?: boolean | null
|
||||||
isSnapMode?: boolean | null
|
isSnapMode?: boolean | null
|
||||||
isWrapMode?: boolean | null
|
isWrapMode?: boolean | null
|
||||||
|
isDynamicSizeMode?: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserDataSnapshot {
|
interface UserDataSnapshot {
|
||||||
|
@ -39,11 +40,12 @@ const userTypeValidator: T.Validator<TLUserPreferences> = T.object<TLUserPrefere
|
||||||
name: T.string.nullable().optional(),
|
name: T.string.nullable().optional(),
|
||||||
locale: T.string.nullable().optional(),
|
locale: T.string.nullable().optional(),
|
||||||
color: T.string.nullable().optional(),
|
color: T.string.nullable().optional(),
|
||||||
isDarkMode: T.boolean.nullable().optional(),
|
|
||||||
animationSpeed: T.number.nullable().optional(),
|
animationSpeed: T.number.nullable().optional(),
|
||||||
edgeScrollSpeed: T.number.nullable().optional(),
|
edgeScrollSpeed: T.number.nullable().optional(),
|
||||||
|
isDarkMode: T.boolean.nullable().optional(),
|
||||||
isSnapMode: T.boolean.nullable().optional(),
|
isSnapMode: T.boolean.nullable().optional(),
|
||||||
isWrapMode: T.boolean.nullable().optional(),
|
isWrapMode: T.boolean.nullable().optional(),
|
||||||
|
isDynamicSizeMode: T.boolean.nullable().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const Versions = {
|
const Versions = {
|
||||||
|
@ -52,6 +54,7 @@ const Versions = {
|
||||||
MakeFieldsNullable: 3,
|
MakeFieldsNullable: 3,
|
||||||
AddEdgeScrollSpeed: 4,
|
AddEdgeScrollSpeed: 4,
|
||||||
AddExcalidrawSelectMode: 5,
|
AddExcalidrawSelectMode: 5,
|
||||||
|
AddDynamicSizeMode: 6,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const CURRENT_VERSION = Math.max(...Object.values(Versions))
|
const CURRENT_VERSION = Math.max(...Object.values(Versions))
|
||||||
|
@ -73,6 +76,10 @@ function migrateSnapshot(data: { version: number; user: any }) {
|
||||||
data.user.isWrapMode = false
|
data.user.isWrapMode = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.version < Versions.AddDynamicSizeMode) {
|
||||||
|
data.user.isDynamicSizeMode = false
|
||||||
|
}
|
||||||
|
|
||||||
// finally
|
// finally
|
||||||
data.version = CURRENT_VERSION
|
data.version = CURRENT_VERSION
|
||||||
}
|
}
|
||||||
|
@ -123,6 +130,7 @@ export const defaultUserPreferences = Object.freeze({
|
||||||
animationSpeed: userPrefersReducedMotion() ? 0 : 1,
|
animationSpeed: userPrefersReducedMotion() ? 0 : 1,
|
||||||
isSnapMode: false,
|
isSnapMode: false,
|
||||||
isWrapMode: false,
|
isWrapMode: false,
|
||||||
|
isDynamicSizeMode: false,
|
||||||
}) satisfies Readonly<Omit<TLUserPreferences, 'id'>>
|
}) satisfies Readonly<Omit<TLUserPreferences, 'id'>>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
|
|
@ -29,6 +29,7 @@ export class UserPreferencesManager {
|
||||||
isSnapMode: this.getIsSnapMode(),
|
isSnapMode: this.getIsSnapMode(),
|
||||||
isDarkMode: this.getIsDarkMode(),
|
isDarkMode: this.getIsDarkMode(),
|
||||||
isWrapMode: this.getIsWrapMode(),
|
isWrapMode: this.getIsWrapMode(),
|
||||||
|
isDynamicResizeMode: this.getIsDynamicResizeMode(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@computed getIsDarkMode() {
|
@computed getIsDarkMode() {
|
||||||
|
@ -72,4 +73,10 @@ export class UserPreferencesManager {
|
||||||
@computed getIsWrapMode() {
|
@computed getIsWrapMode() {
|
||||||
return this.user.userPreferences.get().isWrapMode ?? defaultUserPreferences.isWrapMode
|
return this.user.userPreferences.get().isWrapMode ?? defaultUserPreferences.isWrapMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed getIsDynamicResizeMode() {
|
||||||
|
return (
|
||||||
|
this.user.userPreferences.get().isDynamicSizeMode ?? defaultUserPreferences.isDynamicSizeMode
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { createShapeId } from '@tldraw/tlschema'
|
import { createShapeId } from '@tldraw/tlschema'
|
||||||
|
import { structuredClone } from '@tldraw/utils'
|
||||||
import { Vec } from '../../../../primitives/Vec'
|
import { Vec } from '../../../../primitives/Vec'
|
||||||
import { TLBaseBoxShape } from '../../../shapes/BaseBoxShapeUtil'
|
import { TLBaseBoxShape } from '../../../shapes/BaseBoxShapeUtil'
|
||||||
import { TLEventHandlers } from '../../../types/event-types'
|
import { TLEventHandlers } from '../../../types/event-types'
|
||||||
|
@ -85,6 +86,8 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
this.editor.mark(this.markId)
|
this.editor.mark(this.markId)
|
||||||
|
|
||||||
|
// todo: add scale here when dynamic size is enabled
|
||||||
|
|
||||||
this.editor.createShapes<TLBaseBoxShape>([
|
this.editor.createShapes<TLBaseBoxShape>([
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
|
@ -95,20 +98,35 @@ export class Pointing extends StateNode {
|
||||||
])
|
])
|
||||||
|
|
||||||
const shape = this.editor.getShape<TLBaseBoxShape>(id)!
|
const shape = this.editor.getShape<TLBaseBoxShape>(id)!
|
||||||
const { w, h } = this.editor.getShapeUtil(shape).getDefaultProps() as TLBaseBoxShape['props']
|
if (!shape) {
|
||||||
const delta = new Vec(w / 2, h / 2)
|
this.cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let { w, h } = shape.props
|
||||||
|
const delta = new Vec(w / 2, h / 2)
|
||||||
const parentTransform = this.editor.getShapeParentTransform(shape)
|
const parentTransform = this.editor.getShapeParentTransform(shape)
|
||||||
if (parentTransform) delta.rot(-parentTransform.rotation())
|
if (parentTransform) delta.rot(-parentTransform.rotation())
|
||||||
|
let scale = 1
|
||||||
|
|
||||||
this.editor.updateShapes<TLBaseBoxShape>([
|
if (this.editor.user.getIsDynamicResizeMode()) {
|
||||||
{
|
scale = 1 / this.editor.getZoomLevel()
|
||||||
id,
|
w *= scale
|
||||||
type: shapeType,
|
h *= scale
|
||||||
x: shape.x - delta.x,
|
delta.mul(scale)
|
||||||
y: shape.y - delta.y,
|
}
|
||||||
},
|
|
||||||
])
|
const next = structuredClone(shape)
|
||||||
|
next.x = shape.x - delta.x
|
||||||
|
next.y = shape.y - delta.y
|
||||||
|
next.props.w = w
|
||||||
|
next.props.h = h
|
||||||
|
|
||||||
|
if ('scale' in shape.props) {
|
||||||
|
;(next as TLBaseBoxShape & { props: { scale: number } }).props.scale = scale
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor.updateShape<TLBaseBoxShape>(next)
|
||||||
|
|
||||||
this.editor.setSelectedShapes([id])
|
this.editor.setSelectedShapes([id])
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,6 @@ export { LineShapeTool } from './lib/shapes/line/LineShapeTool'
|
||||||
export { LineShapeUtil } from './lib/shapes/line/LineShapeUtil'
|
export { LineShapeUtil } from './lib/shapes/line/LineShapeUtil'
|
||||||
export { NoteShapeTool } from './lib/shapes/note/NoteShapeTool'
|
export { NoteShapeTool } from './lib/shapes/note/NoteShapeTool'
|
||||||
export { NoteShapeUtil } from './lib/shapes/note/NoteShapeUtil'
|
export { NoteShapeUtil } from './lib/shapes/note/NoteShapeUtil'
|
||||||
export { useDefaultColorTheme } from './lib/shapes/shared/ShapeFill'
|
|
||||||
export { TextLabel, type TextLabelProps } from './lib/shapes/shared/TextLabel'
|
export { TextLabel, type TextLabelProps } from './lib/shapes/shared/TextLabel'
|
||||||
export {
|
export {
|
||||||
FONT_FAMILIES,
|
FONT_FAMILIES,
|
||||||
|
@ -46,6 +45,7 @@ export {
|
||||||
TEXT_PROPS,
|
TEXT_PROPS,
|
||||||
} from './lib/shapes/shared/default-shape-constants'
|
} from './lib/shapes/shared/default-shape-constants'
|
||||||
export { getPerfectDashProps } from './lib/shapes/shared/getPerfectDashProps'
|
export { getPerfectDashProps } from './lib/shapes/shared/getPerfectDashProps'
|
||||||
|
export { useDefaultColorTheme } from './lib/shapes/shared/useDefaultColorTheme'
|
||||||
export { useEditableText } from './lib/shapes/shared/useEditableText'
|
export { useEditableText } from './lib/shapes/shared/useEditableText'
|
||||||
export { TextShapeTool } from './lib/shapes/text/TextShapeTool'
|
export { TextShapeTool } from './lib/shapes/text/TextShapeTool'
|
||||||
export { TextShapeUtil } from './lib/shapes/text/TextShapeUtil'
|
export { TextShapeUtil } from './lib/shapes/text/TextShapeUtil'
|
||||||
|
|
|
@ -35,16 +35,18 @@ import {
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { updateArrowTerminal } from '../../bindings/arrow/ArrowBindingUtil'
|
import { updateArrowTerminal } from '../../bindings/arrow/ArrowBindingUtil'
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../shared/ShapeFill'
|
import { ShapeFill } from '../shared/ShapeFill'
|
||||||
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
||||||
import { ARROW_LABEL_FONT_SIZES, STROKE_SIZES } from '../shared/default-shape-constants'
|
import { TextLabel } from '../shared/TextLabel'
|
||||||
|
import { STROKE_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
|
||||||
import {
|
import {
|
||||||
getFillDefForCanvas,
|
getFillDefForCanvas,
|
||||||
getFillDefForExport,
|
getFillDefForExport,
|
||||||
getFontDefForExport,
|
getFontDefForExport,
|
||||||
} from '../shared/defaultStyleDefs'
|
} from '../shared/defaultStyleDefs'
|
||||||
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
|
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
|
||||||
import { getArrowLabelPosition } from './arrowLabel'
|
import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
|
||||||
|
import { getArrowLabelFontSize, getArrowLabelPosition } from './arrowLabel'
|
||||||
import { getArrowheadPathForType } from './arrowheads'
|
import { getArrowheadPathForType } from './arrowheads'
|
||||||
import {
|
import {
|
||||||
getCurvedArrowHandlePath,
|
getCurvedArrowHandlePath,
|
||||||
|
@ -52,7 +54,6 @@ import {
|
||||||
getSolidStraightArrowPath,
|
getSolidStraightArrowPath,
|
||||||
getStraightArrowHandlePath,
|
getStraightArrowHandlePath,
|
||||||
} from './arrowpaths'
|
} from './arrowpaths'
|
||||||
import { ArrowTextLabel } from './components/ArrowTextLabel'
|
|
||||||
import {
|
import {
|
||||||
TLArrowBindings,
|
TLArrowBindings,
|
||||||
createOrUpdateArrowBinding,
|
createOrUpdateArrowBinding,
|
||||||
|
@ -107,6 +108,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
text: '',
|
text: '',
|
||||||
labelPosition: 0.5,
|
labelPosition: 0.5,
|
||||||
font: 'draw',
|
font: 'draw',
|
||||||
|
scale: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -567,6 +569,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
component(shape: TLArrowShape) {
|
component(shape: TLArrowShape) {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const theme = useDefaultColorTheme()
|
||||||
const onlySelectedShape = this.editor.getOnlySelectedShape()
|
const onlySelectedShape = this.editor.getOnlySelectedShape()
|
||||||
const shouldDisplayHandles =
|
const shouldDisplayHandles =
|
||||||
this.editor.isInAny(
|
this.editor.isInAny(
|
||||||
|
@ -594,15 +598,23 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
/>
|
/>
|
||||||
</SVGContainer>
|
</SVGContainer>
|
||||||
{showArrowLabel && (
|
{showArrowLabel && (
|
||||||
<ArrowTextLabel
|
<TextLabel
|
||||||
id={shape.id}
|
id={shape.id}
|
||||||
text={shape.props.text}
|
classNamePrefix="tl-arrow"
|
||||||
|
type="arrow"
|
||||||
font={shape.props.font}
|
font={shape.props.font}
|
||||||
size={shape.props.size}
|
fontSize={getArrowLabelFontSize(shape)}
|
||||||
position={labelPosition.box.center}
|
lineHeight={TEXT_PROPS.lineHeight}
|
||||||
width={labelPosition.box.w}
|
align="middle"
|
||||||
|
verticalAlign="middle"
|
||||||
|
text={shape.props.text}
|
||||||
|
labelColor={theme[shape.props.labelColor].solid}
|
||||||
|
textWidth={labelPosition.box.w}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
labelColor={shape.props.labelColor}
|
padding={0}
|
||||||
|
style={{
|
||||||
|
transform: `translate(${labelPosition.box.center.x}px, ${labelPosition.box.center.y}px)`,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -624,7 +636,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
|
|
||||||
if (Vec.Equals(start, end)) return null
|
if (Vec.Equals(start, end)) return null
|
||||||
|
|
||||||
const strokeWidth = STROKE_SIZES[shape.props.size]
|
const strokeWidth = STROKE_SIZES[shape.props.size] * shape.props.scale
|
||||||
|
|
||||||
const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth)
|
const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth)
|
||||||
const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth)
|
const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth)
|
||||||
|
@ -645,8 +657,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
y={toDomPrecision(labelGeometry.y)}
|
y={toDomPrecision(labelGeometry.y)}
|
||||||
width={labelGeometry.w}
|
width={labelGeometry.w}
|
||||||
height={labelGeometry.h}
|
height={labelGeometry.h}
|
||||||
rx={3.5}
|
rx={3.5 * shape.props.scale}
|
||||||
ry={3.5}
|
ry={3.5 * shape.props.scale}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -670,8 +682,8 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
width={labelGeometry.w}
|
width={labelGeometry.w}
|
||||||
height={labelGeometry.h}
|
height={labelGeometry.h}
|
||||||
fill="black"
|
fill="black"
|
||||||
rx={3.5}
|
rx={3.5 * shape.props.scale}
|
||||||
ry={3.5}
|
ry={3.5 * shape.props.scale}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{as && (
|
{as && (
|
||||||
|
@ -746,21 +758,22 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
ctx.addExportDef(getFillDefForExport(shape.props.fill))
|
ctx.addExportDef(getFillDefForExport(shape.props.fill))
|
||||||
if (shape.props.text) ctx.addExportDef(getFontDefForExport(shape.props.font))
|
if (shape.props.text) ctx.addExportDef(getFontDefForExport(shape.props.font))
|
||||||
const theme = getDefaultColorTheme(ctx)
|
const theme = getDefaultColorTheme(ctx)
|
||||||
|
const scaleFactor = 1 / shape.props.scale
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<g transform={`scale(${scaleFactor})`}>
|
||||||
<ArrowSvg shape={shape} shouldDisplayHandles={false} />
|
<ArrowSvg shape={shape} shouldDisplayHandles={false} />
|
||||||
<SvgTextLabel
|
<SvgTextLabel
|
||||||
fontSize={ARROW_LABEL_FONT_SIZES[shape.props.size]}
|
fontSize={getArrowLabelFontSize(shape)}
|
||||||
font={shape.props.font}
|
font={shape.props.font}
|
||||||
align="middle"
|
align="middle"
|
||||||
verticalAlign="middle"
|
verticalAlign="middle"
|
||||||
text={shape.props.text}
|
text={shape.props.text}
|
||||||
labelColor={theme[shape.props.labelColor].solid}
|
labelColor={theme[shape.props.labelColor].solid}
|
||||||
bounds={getArrowLabelPosition(this.editor, shape).box}
|
bounds={getArrowLabelPosition(this.editor, shape).box}
|
||||||
padding={4}
|
padding={4 * shape.props.scale}
|
||||||
/>
|
/>
|
||||||
</>
|
</g>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -807,7 +820,7 @@ const ArrowSvg = track(function ArrowSvg({
|
||||||
|
|
||||||
if (!info?.isValid) return null
|
if (!info?.isValid) return null
|
||||||
|
|
||||||
const strokeWidth = STROKE_SIZES[shape.props.size]
|
const strokeWidth = STROKE_SIZES[shape.props.size] * shape.props.scale
|
||||||
|
|
||||||
const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth)
|
const as = info.start.arrowhead && getArrowheadPathForType(info, 'start', strokeWidth)
|
||||||
const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth)
|
const ae = info.end.arrowhead && getArrowheadPathForType(info, 'end', strokeWidth)
|
||||||
|
@ -817,7 +830,7 @@ const ArrowSvg = track(function ArrowSvg({
|
||||||
let handlePath: null | React.JSX.Element = null
|
let handlePath: null | React.JSX.Element = null
|
||||||
|
|
||||||
if (shouldDisplayHandles) {
|
if (shouldDisplayHandles) {
|
||||||
const sw = 2
|
const sw = 2 / editor.getZoomLevel()
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
getLength(editor, shape),
|
getLength(editor, shape),
|
||||||
sw,
|
sw,
|
||||||
|
@ -928,10 +941,22 @@ const ArrowSvg = track(function ArrowSvg({
|
||||||
<path d={path} strokeDasharray={strokeDasharray} strokeDashoffset={strokeDashoffset} />
|
<path d={path} strokeDasharray={strokeDasharray} strokeDashoffset={strokeDashoffset} />
|
||||||
</g>
|
</g>
|
||||||
{as && maskStartArrowhead && shape.props.fill !== 'none' && (
|
{as && maskStartArrowhead && shape.props.fill !== 'none' && (
|
||||||
<ShapeFill theme={theme} d={as} color={shape.props.color} fill={shape.props.fill} />
|
<ShapeFill
|
||||||
|
theme={theme}
|
||||||
|
d={as}
|
||||||
|
color={shape.props.color}
|
||||||
|
fill={shape.props.fill}
|
||||||
|
scale={shape.props.scale}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{ae && maskEndArrowhead && shape.props.fill !== 'none' && (
|
{ae && maskEndArrowhead && shape.props.fill !== 'none' && (
|
||||||
<ShapeFill theme={theme} d={ae} color={shape.props.color} fill={shape.props.fill} />
|
<ShapeFill
|
||||||
|
theme={theme}
|
||||||
|
d={ae}
|
||||||
|
color={shape.props.color}
|
||||||
|
fill={shape.props.fill}
|
||||||
|
scale={shape.props.scale}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{as && <path d={as} />}
|
{as && <path d={as} />}
|
||||||
{ae && <path d={ae} />}
|
{ae && <path d={ae} />}
|
||||||
|
|
|
@ -52,10 +52,12 @@ function getArrowLabelSize(editor: Editor, shape: TLArrowShape) {
|
||||||
if (shape.props.text.trim()) {
|
if (shape.props.text.trim()) {
|
||||||
const bodyBounds = bodyGeom.bounds
|
const bodyBounds = bodyGeom.bounds
|
||||||
|
|
||||||
|
const fontSize = getArrowLabelFontSize(shape)
|
||||||
|
|
||||||
const { w, h } = editor.textMeasure.measureText(shape.props.text, {
|
const { w, h } = editor.textMeasure.measureText(shape.props.text, {
|
||||||
...TEXT_PROPS,
|
...TEXT_PROPS,
|
||||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||||
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
|
fontSize,
|
||||||
maxWidth: null,
|
maxWidth: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -70,7 +72,7 @@ function getArrowLabelSize(editor: Editor, shape: TLArrowShape) {
|
||||||
{
|
{
|
||||||
...TEXT_PROPS,
|
...TEXT_PROPS,
|
||||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||||
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
|
fontSize,
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -79,15 +81,15 @@ function getArrowLabelSize(editor: Editor, shape: TLArrowShape) {
|
||||||
height = squishedHeight
|
height = squishedHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
if (width > 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]) {
|
if (width > 16 * fontSize) {
|
||||||
width = 16 * ARROW_LABEL_FONT_SIZES[shape.props.size]
|
width = 16 * fontSize
|
||||||
|
|
||||||
const { w: squishedWidth, h: squishedHeight } = editor.textMeasure.measureText(
|
const { w: squishedWidth, h: squishedHeight } = editor.textMeasure.measureText(
|
||||||
shape.props.text,
|
shape.props.text,
|
||||||
{
|
{
|
||||||
...TEXT_PROPS,
|
...TEXT_PROPS,
|
||||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||||
fontSize: ARROW_LABEL_FONT_SIZES[shape.props.size],
|
fontSize,
|
||||||
maxWidth: width,
|
maxWidth: width,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -97,17 +99,18 @@ function getArrowLabelSize(editor: Editor, shape: TLArrowShape) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const size = new Vec(width, height).addScalar(ARROW_LABEL_PADDING * 2)
|
const size = new Vec(width, height).addScalar(ARROW_LABEL_PADDING * 2 * shape.props.scale)
|
||||||
labelSizeCache.set(shape, size)
|
labelSizeCache.set(shape, size)
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLabelToArrowPadding(editor: Editor, shape: TLArrowShape) {
|
function getLabelToArrowPadding(shape: TLArrowShape) {
|
||||||
const strokeWidth = STROKE_SIZES[shape.props.size]
|
const strokeWidth = STROKE_SIZES[shape.props.size]
|
||||||
const labelToArrowPadding =
|
const labelToArrowPadding =
|
||||||
LABEL_TO_ARROW_PADDING +
|
(LABEL_TO_ARROW_PADDING +
|
||||||
(strokeWidth - STROKE_SIZES.s) * 2 +
|
(strokeWidth - STROKE_SIZES.s) * 2 +
|
||||||
(strokeWidth === STROKE_SIZES.xl ? 20 : 0)
|
(strokeWidth === STROKE_SIZES.xl ? 20 : 0)) *
|
||||||
|
shape.props.scale
|
||||||
|
|
||||||
return labelToArrowPadding
|
return labelToArrowPadding
|
||||||
}
|
}
|
||||||
|
@ -122,7 +125,7 @@ function getStraightArrowLabelRange(
|
||||||
info: Extract<TLArrowInfo, { isStraight: true }>
|
info: Extract<TLArrowInfo, { isStraight: true }>
|
||||||
): { start: number; end: number } {
|
): { start: number; end: number } {
|
||||||
const labelSize = getArrowLabelSize(editor, shape)
|
const labelSize = getArrowLabelSize(editor, shape)
|
||||||
const labelToArrowPadding = getLabelToArrowPadding(editor, shape)
|
const labelToArrowPadding = getLabelToArrowPadding(shape)
|
||||||
|
|
||||||
// take the start and end points of the arrow, and nudge them in a bit to give some spare space:
|
// take the start and end points of the arrow, and nudge them in a bit to give some spare space:
|
||||||
const startOffset = Vec.Nudge(info.start.point, info.end.point, labelToArrowPadding)
|
const startOffset = Vec.Nudge(info.start.point, info.end.point, labelToArrowPadding)
|
||||||
|
@ -165,7 +168,7 @@ function getCurvedArrowLabelRange(
|
||||||
info: Extract<TLArrowInfo, { isStraight: false }>
|
info: Extract<TLArrowInfo, { isStraight: false }>
|
||||||
): { start: number; end: number; dbg?: Geometry2d[] } {
|
): { start: number; end: number; dbg?: Geometry2d[] } {
|
||||||
const labelSize = getArrowLabelSize(editor, shape)
|
const labelSize = getArrowLabelSize(editor, shape)
|
||||||
const labelToArrowPadding = getLabelToArrowPadding(editor, shape)
|
const labelToArrowPadding = getLabelToArrowPadding(shape)
|
||||||
const direction = Math.sign(shape.props.bend)
|
const direction = Math.sign(shape.props.bend)
|
||||||
|
|
||||||
// take the start and end points of the arrow, and nudge them in a bit to give some spare space:
|
// take the start and end points of the arrow, and nudge them in a bit to give some spare space:
|
||||||
|
@ -351,3 +354,7 @@ function interpolateArcAngles(angleStart: number, angleEnd: number, direction: n
|
||||||
const dist = angleDistance(angleStart, angleEnd, direction)
|
const dist = angleDistance(angleStart, angleEnd, direction)
|
||||||
return angleStart + dist * t * direction * -1
|
return angleStart + dist * t * direction * -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getArrowLabelFontSize(shape: TLArrowShape) {
|
||||||
|
return ARROW_LABEL_FONT_SIZES[shape.props.size] * shape.props.scale
|
||||||
|
}
|
||||||
|
|
|
@ -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)`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
|
@ -34,7 +34,10 @@ export function getCurvedArrowInfo(
|
||||||
const { arrowheadEnd, arrowheadStart } = shape.props
|
const { arrowheadEnd, arrowheadStart } = shape.props
|
||||||
const bend = shape.props.bend
|
const bend = shape.props.bend
|
||||||
|
|
||||||
if (Math.abs(bend) > Math.abs(shape.props.bend * WAY_TOO_BIG_ARROW_BEND_FACTOR)) {
|
if (
|
||||||
|
Math.abs(bend) >
|
||||||
|
Math.abs(shape.props.bend * (WAY_TOO_BIG_ARROW_BEND_FACTOR * shape.props.scale))
|
||||||
|
) {
|
||||||
return getStraightArrowInfo(editor, shape, bindings)
|
return getStraightArrowInfo(editor, shape, bindings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +104,7 @@ export function getCurvedArrowInfo(
|
||||||
let offsetA = 0
|
let offsetA = 0
|
||||||
let offsetB = 0
|
let offsetB = 0
|
||||||
|
|
||||||
let minLength = MIN_ARROW_LENGTH
|
let minLength = MIN_ARROW_LENGTH * shape.props.scale
|
||||||
|
|
||||||
if (startShapeInfo && !startShapeInfo.isExact) {
|
if (startShapeInfo && !startShapeInfo.isExact) {
|
||||||
const startInPageSpace = Mat.applyToPoint(arrowPageTransform, tempA)
|
const startInPageSpace = Mat.applyToPoint(arrowPageTransform, tempA)
|
||||||
|
@ -165,8 +168,8 @@ export function getCurvedArrowInfo(
|
||||||
('size' in startShapeInfo.shape.props
|
('size' in startShapeInfo.shape.props
|
||||||
? STROKE_SIZES[startShapeInfo.shape.props.size] / 2
|
? STROKE_SIZES[startShapeInfo.shape.props.size] / 2
|
||||||
: 0)
|
: 0)
|
||||||
offsetA = BOUND_ARROW_OFFSET + strokeOffset
|
offsetA = (BOUND_ARROW_OFFSET + strokeOffset) * shape.props.scale
|
||||||
minLength += strokeOffset
|
minLength += strokeOffset * shape.props.scale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -237,8 +240,8 @@ export function getCurvedArrowInfo(
|
||||||
const strokeOffset =
|
const strokeOffset =
|
||||||
STROKE_SIZES[shape.props.size] / 2 +
|
STROKE_SIZES[shape.props.size] / 2 +
|
||||||
('size' in endShapeInfo.shape.props ? STROKE_SIZES[endShapeInfo.shape.props.size] / 2 : 0)
|
('size' in endShapeInfo.shape.props ? STROKE_SIZES[endShapeInfo.shape.props.size] / 2 : 0)
|
||||||
offsetB = BOUND_ARROW_OFFSET + strokeOffset
|
offsetB = (BOUND_ARROW_OFFSET + strokeOffset) * shape.props.scale
|
||||||
minLength += strokeOffset
|
minLength += strokeOffset * shape.props.scale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -257,15 +260,15 @@ export function getCurvedArrowInfo(
|
||||||
const tB = tempB.clone()
|
const tB = tempB.clone()
|
||||||
|
|
||||||
if (offsetA !== 0) {
|
if (offsetA !== 0) {
|
||||||
const n = (offsetA / lAB) * (isClockwise ? 1 : -1)
|
tA.setTo(handleArc.center).add(
|
||||||
const u = Vec.FromAngle(aCA + dAB * n)
|
Vec.FromAngle(aCA + dAB * ((offsetA / lAB) * (isClockwise ? 1 : -1))).mul(handleArc.radius)
|
||||||
tA.setTo(handleArc.center).add(u.mul(handleArc.radius))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offsetB !== 0) {
|
if (offsetB !== 0) {
|
||||||
const n = (offsetB / lAB) * (isClockwise ? -1 : 1)
|
tB.setTo(handleArc.center).add(
|
||||||
const u = Vec.FromAngle(aCB + dAB * n)
|
Vec.FromAngle(aCB + dAB * ((offsetB / lAB) * (isClockwise ? -1 : 1))).mul(handleArc.radius)
|
||||||
tB.setTo(handleArc.center).add(u.mul(handleArc.radius))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Vec.DistMin(tA, tB, minLength)) {
|
if (Vec.DistMin(tA, tB, minLength)) {
|
||||||
|
@ -282,15 +285,19 @@ export function getCurvedArrowInfo(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offsetA !== 0) {
|
if (offsetA !== 0) {
|
||||||
const n = (offsetA / lAB) * (isClockwise ? 1 : -1)
|
tempA
|
||||||
const u = Vec.FromAngle(aCA + dAB * n)
|
.setTo(handleArc.center)
|
||||||
tempA.setTo(handleArc.center).add(u.mul(handleArc.radius))
|
.add(
|
||||||
|
Vec.FromAngle(aCA + dAB * ((offsetA / lAB) * (isClockwise ? 1 : -1))).mul(handleArc.radius)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offsetB !== 0) {
|
if (offsetB !== 0) {
|
||||||
const n = (offsetB / lAB) * (isClockwise ? -1 : 1)
|
tempB
|
||||||
const u = Vec.FromAngle(aCB + dAB * n)
|
.setTo(handleArc.center)
|
||||||
tempB.setTo(handleArc.center).add(u.mul(handleArc.radius))
|
.add(
|
||||||
|
Vec.FromAngle(aCB + dAB * ((offsetB / lAB) * (isClockwise ? -1 : 1))).mul(handleArc.radius)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Did we miss intersections? This happens when we have overlapping shapes.
|
// Did we miss intersections? This happens when we have overlapping shapes.
|
||||||
|
@ -318,9 +325,16 @@ export function getCurvedArrowInfo(
|
||||||
(endShapeInfo && !endShapeInfo.didIntersect) ||
|
(endShapeInfo && !endShapeInfo.didIntersect) ||
|
||||||
distFn(handle_aCA, aCA) > distFn(handle_aCA, aCB)
|
distFn(handle_aCA, aCA) > distFn(handle_aCA, aCB)
|
||||||
) {
|
) {
|
||||||
const n = Math.min(0.9, MIN_ARROW_LENGTH / lAB) * (isClockwise ? 1 : -1)
|
tempB
|
||||||
const u = Vec.FromAngle(aCA + dAB * n)
|
.setTo(handleArc.center)
|
||||||
tempB.setTo(handleArc.center).add(u.mul(handleArc.radius))
|
.add(
|
||||||
|
Vec.FromAngle(
|
||||||
|
aCA +
|
||||||
|
dAB *
|
||||||
|
(Math.min(0.9, (MIN_ARROW_LENGTH * shape.props.scale) / lAB) *
|
||||||
|
(isClockwise ? 1 : -1))
|
||||||
|
).mul(handleArc.radius)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -421,9 +435,7 @@ function placeCenterHandle(
|
||||||
let dAB = clockwiseAngleDist(aCA, aCB) // angle distance between a and b
|
let dAB = clockwiseAngleDist(aCA, aCB) // angle distance between a and b
|
||||||
if (!isClockwise) dAB = PI2 - dAB
|
if (!isClockwise) dAB = PI2 - dAB
|
||||||
|
|
||||||
const n = 0.5 * (isClockwise ? 1 : -1)
|
tempC.setTo(center).add(Vec.FromAngle(aCA + dAB * (0.5 * (isClockwise ? 1 : -1))).mul(radius))
|
||||||
const u = Vec.FromAngle(aCA + dAB * n)
|
|
||||||
tempC.setTo(center).add(u.mul(radius))
|
|
||||||
|
|
||||||
if (dAB > originalArcLength) {
|
if (dAB > originalArcLength) {
|
||||||
tempC.rotWith(center, PI)
|
tempC.rotWith(center, PI)
|
||||||
|
|
|
@ -13,8 +13,10 @@ import { createComputedCache } from '@tldraw/store'
|
||||||
import { getCurvedArrowInfo } from './curved-arrow'
|
import { getCurvedArrowInfo } from './curved-arrow'
|
||||||
import { getStraightArrowInfo } from './straight-arrow'
|
import { getStraightArrowInfo } from './straight-arrow'
|
||||||
|
|
||||||
|
const MIN_ARROW_BEND = 8
|
||||||
|
|
||||||
export function getIsArrowStraight(shape: TLArrowShape) {
|
export function getIsArrowStraight(shape: TLArrowShape) {
|
||||||
return Math.abs(shape.props.bend) < 8 // snap to +-8px
|
return Math.abs(shape.props.bend) < MIN_ARROW_BEND * shape.props.scale // snap to +-8px
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BoundShapeInfo<T extends TLShape = TLShape> {
|
export interface BoundShapeInfo<T extends TLShape = TLShape> {
|
||||||
|
|
|
@ -82,7 +82,7 @@ export function getStraightArrowInfo(
|
||||||
let offsetB = 0
|
let offsetB = 0
|
||||||
let strokeOffsetA = 0
|
let strokeOffsetA = 0
|
||||||
let strokeOffsetB = 0
|
let strokeOffsetB = 0
|
||||||
let minLength = MIN_ARROW_LENGTH
|
let minLength = MIN_ARROW_LENGTH * shape.props.scale
|
||||||
|
|
||||||
const isSelfIntersection =
|
const isSelfIntersection =
|
||||||
startShapeInfo && endShapeInfo && startShapeInfo.shape === endShapeInfo.shape
|
startShapeInfo && endShapeInfo && startShapeInfo.shape === endShapeInfo.shape
|
||||||
|
@ -105,14 +105,14 @@ export function getStraightArrowInfo(
|
||||||
// a short arrow ending at the end shape intersection.
|
// a short arrow ending at the end shape intersection.
|
||||||
|
|
||||||
if (startShapeInfo.isClosed) {
|
if (startShapeInfo.isClosed) {
|
||||||
a.setTo(b.clone().add(uAB.clone().mul(MIN_ARROW_LENGTH)))
|
a.setTo(b.clone().add(uAB.clone().mul(MIN_ARROW_LENGTH * shape.props.scale)))
|
||||||
}
|
}
|
||||||
} else if (!endShapeInfo.didIntersect) {
|
} else if (!endShapeInfo.didIntersect) {
|
||||||
// ...and if only the end shape intersected, or if neither
|
// ...and if only the end shape intersected, or if neither
|
||||||
// shape intersected, then make it a short arrow starting
|
// shape intersected, then make it a short arrow starting
|
||||||
// at the start shape intersection.
|
// at the start shape intersection.
|
||||||
if (endShapeInfo.isClosed) {
|
if (endShapeInfo.isClosed) {
|
||||||
b.setTo(a.clone().sub(uAB.clone().mul(MIN_ARROW_LENGTH)))
|
b.setTo(a.clone().sub(uAB.clone().mul(MIN_ARROW_LENGTH * shape.props.scale)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,8 +136,8 @@ export function getStraightArrowInfo(
|
||||||
('size' in startShapeInfo.shape.props
|
('size' in startShapeInfo.shape.props
|
||||||
? STROKE_SIZES[startShapeInfo.shape.props.size] / 2
|
? STROKE_SIZES[startShapeInfo.shape.props.size] / 2
|
||||||
: 0)
|
: 0)
|
||||||
offsetA = BOUND_ARROW_OFFSET + strokeOffsetA
|
offsetA = (BOUND_ARROW_OFFSET + strokeOffsetA) * shape.props.scale
|
||||||
minLength += strokeOffsetA
|
minLength += strokeOffsetA * shape.props.scale
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the arrow is bound non-exact to an end shape and the
|
// If the arrow is bound non-exact to an end shape and the
|
||||||
|
@ -151,8 +151,8 @@ export function getStraightArrowInfo(
|
||||||
strokeOffsetB =
|
strokeOffsetB =
|
||||||
STROKE_SIZES[shape.props.size] / 2 +
|
STROKE_SIZES[shape.props.size] / 2 +
|
||||||
('size' in endShapeInfo.shape.props ? STROKE_SIZES[endShapeInfo.shape.props.size] / 2 : 0)
|
('size' in endShapeInfo.shape.props ? STROKE_SIZES[endShapeInfo.shape.props.size] / 2 : 0)
|
||||||
offsetB = BOUND_ARROW_OFFSET + strokeOffsetB
|
offsetB = (BOUND_ARROW_OFFSET + strokeOffsetB) * shape.props.scale
|
||||||
minLength += strokeOffsetB
|
minLength += strokeOffsetB * shape.props.scale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +187,7 @@ export function getStraightArrowInfo(
|
||||||
if (startShapeInfo && endShapeInfo) {
|
if (startShapeInfo && endShapeInfo) {
|
||||||
// If we have two bound shapes...then make the arrow a short arrow from
|
// If we have two bound shapes...then make the arrow a short arrow from
|
||||||
// the start point towards where the end point should be.
|
// the start point towards where the end point should be.
|
||||||
b.setTo(Vec.Add(a, u.clone().mul(-MIN_ARROW_LENGTH)))
|
b.setTo(Vec.Add(a, u.clone().mul(-MIN_ARROW_LENGTH * shape.props.scale)))
|
||||||
}
|
}
|
||||||
c.setTo(Vec.Med(terminalsInArrowSpace.start, terminalsInArrowSpace.end))
|
c.setTo(Vec.Med(terminalsInArrowSpace.start, terminalsInArrowSpace.end))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -90,14 +90,15 @@ export class Pointing extends StateNode {
|
||||||
this.markId = `creating:${id}`
|
this.markId = `creating:${id}`
|
||||||
this.editor.mark(this.markId)
|
this.editor.mark(this.markId)
|
||||||
|
|
||||||
this.editor.createShapes<TLArrowShape>([
|
this.editor.createShape<TLArrowShape>({
|
||||||
{
|
id,
|
||||||
id,
|
type: 'arrow',
|
||||||
type: 'arrow',
|
x: originPagePoint.x,
|
||||||
x: originPagePoint.x,
|
y: originPagePoint.y,
|
||||||
y: originPagePoint.y,
|
props: {
|
||||||
|
scale: this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1,
|
||||||
},
|
},
|
||||||
])
|
})
|
||||||
|
|
||||||
const shape = this.editor.getShape<TLArrowShape>(id)
|
const shape = this.editor.getShape<TLArrowShape>(id)
|
||||||
if (!shape) throw Error(`expected shape`)
|
if (!shape) throw Error(`expected shape`)
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Circle2d,
|
Circle2d,
|
||||||
|
@ -18,13 +17,14 @@ import {
|
||||||
rng,
|
rng,
|
||||||
toFixed,
|
toFixed,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../shared/ShapeFill'
|
|
||||||
|
import { ShapeFill } from '../shared/ShapeFill'
|
||||||
import { STROKE_SIZES } from '../shared/default-shape-constants'
|
import { STROKE_SIZES } from '../shared/default-shape-constants'
|
||||||
import { getFillDefForCanvas, getFillDefForExport } from '../shared/defaultStyleDefs'
|
import { getFillDefForCanvas, getFillDefForExport } from '../shared/defaultStyleDefs'
|
||||||
import { getStrokePoints } from '../shared/freehand/getStrokePoints'
|
import { getStrokePoints } from '../shared/freehand/getStrokePoints'
|
||||||
import { getSvgPathFromStrokePoints } from '../shared/freehand/svg'
|
import { getSvgPathFromStrokePoints } from '../shared/freehand/svg'
|
||||||
import { svgInk } from '../shared/freehand/svgInk'
|
import { svgInk } from '../shared/freehand/svgInk'
|
||||||
import { useForceSolid } from '../shared/useForceSolid'
|
import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
|
||||||
import { getDrawShapeStrokeDashArray, getFreehandOptions, getPointsFromSegments } from './getPath'
|
import { getDrawShapeStrokeDashArray, getFreehandOptions, getPointsFromSegments } from './getPath'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -47,21 +47,23 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
||||||
isComplete: false,
|
isComplete: false,
|
||||||
isClosed: false,
|
isClosed: false,
|
||||||
isPen: false,
|
isPen: false,
|
||||||
|
scale: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getGeometry(shape: TLDrawShape) {
|
getGeometry(shape: TLDrawShape) {
|
||||||
const points = getPointsFromSegments(shape.props.segments)
|
const points = getPointsFromSegments(shape.props.segments)
|
||||||
const strokeWidth = STROKE_SIZES[shape.props.size]
|
|
||||||
|
const sw = (STROKE_SIZES[shape.props.size] + 1) * shape.props.scale
|
||||||
|
|
||||||
// A dot
|
// A dot
|
||||||
if (shape.props.segments.length === 1) {
|
if (shape.props.segments.length === 1) {
|
||||||
const box = Box.FromPoints(points)
|
const box = Box.FromPoints(points)
|
||||||
if (box.width < strokeWidth * 2 && box.height < strokeWidth * 2) {
|
if (box.width < sw * 2 && box.height < sw * 2) {
|
||||||
return new Circle2d({
|
return new Circle2d({
|
||||||
x: -strokeWidth,
|
x: -sw,
|
||||||
y: -strokeWidth,
|
y: -sw,
|
||||||
radius: strokeWidth,
|
radius: sw,
|
||||||
isFilled: true,
|
isFilled: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -69,7 +71,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
||||||
|
|
||||||
const strokePoints = getStrokePoints(
|
const strokePoints = getStrokePoints(
|
||||||
points,
|
points,
|
||||||
getFreehandOptions(shape.props, strokeWidth, true, true)
|
getFreehandOptions(shape.props, sw, shape.props.isPen, true)
|
||||||
).map((p) => p.point)
|
).map((p) => p.point)
|
||||||
|
|
||||||
// A closed draw stroke
|
// A closed draw stroke
|
||||||
|
@ -89,24 +91,25 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
||||||
component(shape: TLDrawShape) {
|
component(shape: TLDrawShape) {
|
||||||
return (
|
return (
|
||||||
<SVGContainer id={shape.id}>
|
<SVGContainer id={shape.id}>
|
||||||
<DrawShapeSvg shape={shape} forceSolid={useForceSolid()} />
|
<DrawShapeSvg shape={shape} zoomLevel={this.editor.getZoomLevel()} />
|
||||||
</SVGContainer>
|
</SVGContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
indicator(shape: TLDrawShape) {
|
indicator(shape: TLDrawShape) {
|
||||||
const forceSolid = useForceSolid()
|
|
||||||
const strokeWidth = STROKE_SIZES[shape.props.size]
|
|
||||||
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
||||||
|
|
||||||
let sw = strokeWidth
|
let sw = (STROKE_SIZES[shape.props.size] + 1) * shape.props.scale
|
||||||
|
const zoomLevel = this.editor.getZoomLevel()
|
||||||
|
const forceSolid = zoomLevel < 0.5 && zoomLevel < 1.5 / sw
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!forceSolid &&
|
!forceSolid &&
|
||||||
!shape.props.isPen &&
|
!shape.props.isPen &&
|
||||||
shape.props.dash === 'draw' &&
|
shape.props.dash === 'draw' &&
|
||||||
allPointsFromSegments.length === 1
|
allPointsFromSegments.length === 1
|
||||||
) {
|
) {
|
||||||
sw += rng(shape.id)() * (strokeWidth / 6)
|
sw += rng(shape.id)() * (sw / 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
|
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
|
||||||
|
@ -122,7 +125,12 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
||||||
|
|
||||||
override toSvg(shape: TLDrawShape, ctx: SvgExportContext) {
|
override toSvg(shape: TLDrawShape, ctx: SvgExportContext) {
|
||||||
ctx.addExportDef(getFillDefForExport(shape.props.fill))
|
ctx.addExportDef(getFillDefForExport(shape.props.fill))
|
||||||
return <DrawShapeSvg shape={shape} forceSolid={false} />
|
const scaleFactor = 1 / shape.props.scale
|
||||||
|
return (
|
||||||
|
<g transform={`scale(${scaleFactor})`}>
|
||||||
|
<DrawShapeSvg shape={shape} zoomLevel={1} />
|
||||||
|
</g>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] {
|
override getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] {
|
||||||
|
@ -156,7 +164,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
|
||||||
|
|
||||||
override expandSelectionOutlinePx(shape: TLDrawShape): number {
|
override expandSelectionOutlinePx(shape: TLDrawShape): number {
|
||||||
const multiplier = shape.props.dash === 'draw' ? 1.6 : 1
|
const multiplier = shape.props.dash === 'draw' ? 1.6 : 1
|
||||||
return (STROKE_SIZES[shape.props.size] * multiplier) / 2
|
return ((STROKE_SIZES[shape.props.size] * multiplier) / 2) * shape.props.scale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,21 +179,23 @@ function getIsDot(shape: TLDrawShape) {
|
||||||
return shape.props.segments.length === 1 && shape.props.segments[0].points.length < 2
|
return shape.props.segments.length === 1 && shape.props.segments[0].points.length < 2
|
||||||
}
|
}
|
||||||
|
|
||||||
function DrawShapeSvg({ shape, forceSolid }: { shape: TLDrawShape; forceSolid: boolean }) {
|
function DrawShapeSvg({ shape, zoomLevel }: { shape: TLDrawShape; zoomLevel: number }) {
|
||||||
const theme = useDefaultColorTheme()
|
const theme = useDefaultColorTheme()
|
||||||
const strokeWidth = STROKE_SIZES[shape.props.size]
|
|
||||||
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
||||||
|
|
||||||
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
|
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
|
||||||
|
|
||||||
let sw = strokeWidth
|
let sw = (STROKE_SIZES[shape.props.size] + 1) * shape.props.scale
|
||||||
|
const forceSolid = zoomLevel < 0.5 && zoomLevel < 1.5 / sw
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!forceSolid &&
|
!forceSolid &&
|
||||||
!shape.props.isPen &&
|
!shape.props.isPen &&
|
||||||
shape.props.dash === 'draw' &&
|
shape.props.dash === 'draw' &&
|
||||||
allPointsFromSegments.length === 1
|
allPointsFromSegments.length === 1
|
||||||
) {
|
) {
|
||||||
sw += rng(shape.id)() * (strokeWidth / 6)
|
sw += rng(shape.id)() * (sw / 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = getFreehandOptions(shape.props, sw, showAsComplete, forceSolid)
|
const options = getFreehandOptions(shape.props, sw, showAsComplete, forceSolid)
|
||||||
|
@ -195,13 +205,14 @@ function DrawShapeSvg({ shape, forceSolid }: { shape: TLDrawShape; forceSolid: b
|
||||||
<>
|
<>
|
||||||
{shape.props.isClosed && shape.props.fill && allPointsFromSegments.length > 1 ? (
|
{shape.props.isClosed && shape.props.fill && allPointsFromSegments.length > 1 ? (
|
||||||
<ShapeFill
|
<ShapeFill
|
||||||
theme={theme}
|
|
||||||
fill={shape.props.isClosed ? shape.props.fill : 'none'}
|
|
||||||
color={shape.props.color}
|
|
||||||
d={getSvgPathFromStrokePoints(
|
d={getSvgPathFromStrokePoints(
|
||||||
getStrokePoints(allPointsFromSegments, options),
|
getStrokePoints(allPointsFromSegments, options),
|
||||||
shape.props.isClosed
|
shape.props.isClosed
|
||||||
)}
|
)}
|
||||||
|
theme={theme}
|
||||||
|
color={shape.props.color}
|
||||||
|
fill={shape.props.isClosed ? shape.props.fill : 'none'}
|
||||||
|
scale={shape.props.scale}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<path
|
<path
|
||||||
|
@ -222,18 +233,19 @@ function DrawShapeSvg({ shape, forceSolid }: { shape: TLDrawShape; forceSolid: b
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShapeFill
|
<ShapeFill
|
||||||
|
d={solidStrokePath}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
color={shape.props.color}
|
color={shape.props.color}
|
||||||
fill={isDot || shape.props.isClosed ? shape.props.fill : 'none'}
|
fill={isDot || shape.props.isClosed ? shape.props.fill : 'none'}
|
||||||
d={solidStrokePath}
|
scale={shape.props.scale}
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d={solidStrokePath}
|
d={solidStrokePath}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
fill={isDot ? theme[shape.props.color].solid : 'none'}
|
fill={isDot ? theme[shape.props.color].solid : 'none'}
|
||||||
stroke={theme[shape.props.color].solid}
|
stroke={theme[shape.props.color].solid}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={sw}
|
||||||
strokeDasharray={isDot ? 'none' : getDrawShapeStrokeDashArray(shape, strokeWidth)}
|
strokeDasharray={isDot ? 'none' : getDrawShapeStrokeDashArray(shape, sw)}
|
||||||
strokeDashoffset="0"
|
strokeDashoffset="0"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
TLDrawShape,
|
TLDrawShape,
|
||||||
TLDrawShapeSegment,
|
TLDrawShapeSegment,
|
||||||
Vec,
|
Vec,
|
||||||
|
modulate,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { StrokeOptions } from '../shared/freehand/types'
|
import { StrokeOptions } from '../shared/freehand/types'
|
||||||
|
|
||||||
|
@ -13,9 +14,9 @@ const PEN_EASING = (t: number) => t * 0.65 + SIN((t * PI) / 2) * 0.35
|
||||||
|
|
||||||
const simulatePressureSettings = (strokeWidth: number): StrokeOptions => {
|
const simulatePressureSettings = (strokeWidth: number): StrokeOptions => {
|
||||||
return {
|
return {
|
||||||
size: 1 + strokeWidth,
|
size: strokeWidth,
|
||||||
thinning: 0.5,
|
thinning: 0.5,
|
||||||
streamline: 0.62 + ((1 + strokeWidth) / 8) * 0.06,
|
streamline: modulate(strokeWidth, [9, 16], [0.64, 0.74], true), // 0.62 + ((1 + strokeWidth) / 8) * 0.06,
|
||||||
smoothing: 0.62,
|
smoothing: 0.62,
|
||||||
easing: EASINGS.easeOutSine,
|
easing: EASINGS.easeOutSine,
|
||||||
simulatePressure: true,
|
simulatePressure: true,
|
||||||
|
@ -35,9 +36,9 @@ const realPressureSettings = (strokeWidth: number): StrokeOptions => {
|
||||||
|
|
||||||
const solidSettings = (strokeWidth: number): StrokeOptions => {
|
const solidSettings = (strokeWidth: number): StrokeOptions => {
|
||||||
return {
|
return {
|
||||||
size: 1 + strokeWidth,
|
size: strokeWidth,
|
||||||
thinning: 0,
|
thinning: 0,
|
||||||
streamline: 0.62 + ((1 + strokeWidth) / 8) * 0.06,
|
streamline: modulate(strokeWidth, [9, 16], [0.68, 0.74], true), // 0.62 + ((1 + strokeWidth) / 8) * 0.06,
|
||||||
smoothing: 0.62,
|
smoothing: 0.62,
|
||||||
simulatePressure: false,
|
simulatePressure: false,
|
||||||
easing: EASINGS.linear,
|
easing: EASINGS.linear,
|
||||||
|
|
|
@ -268,6 +268,7 @@ export class Drawing extends StateNode {
|
||||||
y: originPagePoint.y,
|
y: originPagePoint.y,
|
||||||
props: {
|
props: {
|
||||||
isPen: this.isPenOrStylus,
|
isPen: this.isPenOrStylus,
|
||||||
|
scale: this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1,
|
||||||
segments: [
|
segments: [
|
||||||
{
|
{
|
||||||
type: this.segmentMode,
|
type: this.segmentMode,
|
||||||
|
@ -415,7 +416,13 @@ export class Drawing extends StateNode {
|
||||||
// ended and where the pointer is now
|
// ended and where the pointer is now
|
||||||
const newFreeSegment: TLDrawShapeSegment = {
|
const newFreeSegment: TLDrawShapeSegment = {
|
||||||
type: 'free',
|
type: 'free',
|
||||||
points: [...Vec.PointsBetween(prevPoint, newPoint, 6).map((p) => p.toFixed().toJson())],
|
points: [
|
||||||
|
...Vec.PointsBetween(prevPoint, newPoint, 6).map((p) => ({
|
||||||
|
x: toFixed(p.x),
|
||||||
|
y: toFixed(p.y),
|
||||||
|
z: toFixed(p.z),
|
||||||
|
})),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalSegments = [...newSegments, newFreeSegment]
|
const finalSegments = [...newSegments, newFreeSegment]
|
||||||
|
|
|
@ -18,8 +18,8 @@ import {
|
||||||
useValue,
|
useValue,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
|
||||||
import { createTextJsxFromSpans } from '../shared/createTextJsxFromSpans'
|
import { createTextJsxFromSpans } from '../shared/createTextJsxFromSpans'
|
||||||
|
import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
|
||||||
import { FrameHeading } from './components/FrameHeading'
|
import { FrameHeading } from './components/FrameHeading'
|
||||||
|
|
||||||
export function defaultEmptyAs(str: string, dflt: string) {
|
export function defaultEmptyAs(str: string, dflt: string) {
|
||||||
|
|
|
@ -51,6 +51,55 @@ describe(GeoShapeTool, () => {
|
||||||
|
|
||||||
expect(editor.getCurrentPageShapes().length).toBe(1)
|
expect(editor.getCurrentPageShapes().length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Creates geo shapes when scaled', () => {
|
||||||
|
editor.setCurrentTool('geo')
|
||||||
|
editor.pointerMove(50, 50)
|
||||||
|
editor.pointerDown()
|
||||||
|
editor.pointerMove(100, 100)
|
||||||
|
editor.pointerUp()
|
||||||
|
|
||||||
|
expect(editor.getLastCreatedShape()).toMatchObject({
|
||||||
|
props: {
|
||||||
|
w: 50,
|
||||||
|
h: 50,
|
||||||
|
scale: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.user.updateUserPreferences({ isDynamicSizeMode: true })
|
||||||
|
editor.zoomIn() // 1 -> 2
|
||||||
|
|
||||||
|
editor.setCurrentTool('geo')
|
||||||
|
editor.pointerMove(50, 50)
|
||||||
|
editor.pointerDown()
|
||||||
|
editor.pointerMove(100, 100)
|
||||||
|
editor.pointerUp()
|
||||||
|
|
||||||
|
expect(editor.getLastCreatedShape()).toMatchObject({
|
||||||
|
props: {
|
||||||
|
w: 25,
|
||||||
|
h: 25,
|
||||||
|
scale: 0.5,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.zoomOut().zoomOut() // 2 -> 1 -> .5
|
||||||
|
|
||||||
|
editor.setCurrentTool('geo')
|
||||||
|
editor.pointerMove(50, 50)
|
||||||
|
editor.pointerDown()
|
||||||
|
editor.pointerMove(100, 100)
|
||||||
|
editor.pointerUp()
|
||||||
|
|
||||||
|
expect(editor.getLastCreatedShape()).toMatchObject({
|
||||||
|
props: {
|
||||||
|
w: 100,
|
||||||
|
h: 100,
|
||||||
|
scale: 2,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When selecting the tool', () => {
|
describe('When selecting the tool', () => {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import {
|
import {
|
||||||
BaseBoxShapeUtil,
|
BaseBoxShapeUtil,
|
||||||
|
Box,
|
||||||
Editor,
|
Editor,
|
||||||
Ellipse2d,
|
Ellipse2d,
|
||||||
Geometry2d,
|
Geometry2d,
|
||||||
|
@ -28,7 +29,6 @@ import {
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
|
||||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||||
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
|
||||||
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
||||||
import { TextLabel } from '../shared/TextLabel'
|
import { TextLabel } from '../shared/TextLabel'
|
||||||
import {
|
import {
|
||||||
|
@ -43,6 +43,7 @@ import {
|
||||||
getFillDefForExport,
|
getFillDefForExport,
|
||||||
getFontDefForExport,
|
getFontDefForExport,
|
||||||
} from '../shared/defaultStyleDefs'
|
} from '../shared/defaultStyleDefs'
|
||||||
|
import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
|
||||||
import { GeoShapeBody } from './components/GeoShapeBody'
|
import { GeoShapeBody } from './components/GeoShapeBody'
|
||||||
import {
|
import {
|
||||||
cloudOutline,
|
cloudOutline,
|
||||||
|
@ -81,6 +82,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
growY: 0,
|
growY: 0,
|
||||||
url: '',
|
url: '',
|
||||||
|
scale: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +92,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
const cx = w / 2
|
const cx = w / 2
|
||||||
const cy = h / 2
|
const cy = h / 2
|
||||||
|
|
||||||
const strokeWidth = STROKE_SIZES[shape.props.size]
|
const strokeWidth = STROKE_SIZES[shape.props.size] * shape.props.scale
|
||||||
const isFilled = shape.props.fill !== 'none' // || shape.props.text.trim().length > 0
|
const isFilled = shape.props.fill !== 'none' // || shape.props.text.trim().length > 0
|
||||||
|
|
||||||
let body: Geometry2d
|
let body: Geometry2d
|
||||||
|
@ -318,12 +320,16 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
|
|
||||||
const labelSize = getLabelSize(this.editor, shape)
|
const labelSize = getLabelSize(this.editor, shape)
|
||||||
const minWidth = Math.min(100, w / 2)
|
const minWidth = Math.min(100, w / 2)
|
||||||
const labelWidth = Math.min(w, Math.max(labelSize.w, Math.min(minWidth, Math.max(1, w - 8))))
|
|
||||||
const minHeight = Math.min(
|
const minHeight = Math.min(
|
||||||
LABEL_FONT_SIZES[shape.props.size] * TEXT_PROPS.lineHeight + LABEL_PADDING * 2,
|
LABEL_FONT_SIZES[shape.props.size] * shape.props.scale * TEXT_PROPS.lineHeight +
|
||||||
|
LABEL_PADDING * 2,
|
||||||
h / 2
|
h / 2
|
||||||
)
|
)
|
||||||
const labelHeight = Math.min(h, Math.max(labelSize.h, Math.min(minHeight, Math.max(1, w - 8)))) // not sure if bug
|
|
||||||
|
const labelWidth = Math.min(w, Math.max(labelSize.w, Math.min(minWidth, Math.max(1, w - 8))))
|
||||||
|
const labelHeight = Math.min(h, Math.max(labelSize.h, Math.min(minHeight, Math.max(1, w - 8))))
|
||||||
|
|
||||||
|
// not sure if bug
|
||||||
|
|
||||||
const lines = getLines(shape.props, strokeWidth)
|
const lines = getLines(shape.props, strokeWidth)
|
||||||
const edges = lines ? lines.map((line) => new Polyline2d({ points: line })) : []
|
const edges = lines ? lines.map((line) => new Polyline2d({ points: line })) : []
|
||||||
|
@ -421,7 +427,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SVGContainer id={id}>
|
<SVGContainer id={id}>
|
||||||
<GeoShapeBody shape={shape} />
|
<GeoShapeBody shape={shape} shouldScale={true} />
|
||||||
</SVGContainer>
|
</SVGContainer>
|
||||||
{showHtmlContainer && (
|
{showHtmlContainer && (
|
||||||
<HTMLContainer
|
<HTMLContainer
|
||||||
|
@ -435,8 +441,9 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
id={id}
|
id={id}
|
||||||
type={type}
|
type={type}
|
||||||
font={font}
|
font={font}
|
||||||
fontSize={LABEL_FONT_SIZES[size]}
|
fontSize={LABEL_FONT_SIZES[size] * shape.props.scale}
|
||||||
lineHeight={TEXT_PROPS.lineHeight}
|
lineHeight={TEXT_PROPS.lineHeight}
|
||||||
|
padding={16 * shape.props.scale}
|
||||||
fill={fill}
|
fill={fill}
|
||||||
align={align}
|
align={align}
|
||||||
verticalAlign={verticalAlign}
|
verticalAlign={verticalAlign}
|
||||||
|
@ -488,7 +495,13 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
let path: string
|
let path: string
|
||||||
|
|
||||||
if (props.dash === 'draw') {
|
if (props.dash === 'draw') {
|
||||||
const polygonPoints = getRoundedPolygonPoints(id, outline, 0, strokeWidth * 2, 1)
|
const polygonPoints = getRoundedPolygonPoints(
|
||||||
|
id,
|
||||||
|
outline,
|
||||||
|
0,
|
||||||
|
strokeWidth * 2 * shape.props.scale,
|
||||||
|
1
|
||||||
|
)
|
||||||
path = getRoundedInkyPolygonPath(polygonPoints)
|
path = getRoundedInkyPolygonPath(polygonPoints)
|
||||||
} else {
|
} else {
|
||||||
path = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
|
path = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
|
||||||
|
@ -508,15 +521,24 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override toSvg(shape: TLGeoShape, ctx: SvgExportContext) {
|
override toSvg(shape: TLGeoShape, ctx: SvgExportContext) {
|
||||||
const { props } = shape
|
// We need to scale the shape to 1x for export
|
||||||
ctx.addExportDef(getFillDefForExport(shape.props.fill))
|
const newShape = {
|
||||||
|
...shape,
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
w: shape.props.w / shape.props.scale,
|
||||||
|
h: shape.props.h / shape.props.scale,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const props = newShape.props
|
||||||
|
ctx.addExportDef(getFillDefForExport(props.fill))
|
||||||
|
|
||||||
let textEl
|
let textEl
|
||||||
if (props.text) {
|
if (props.text) {
|
||||||
ctx.addExportDef(getFontDefForExport(shape.props.font))
|
ctx.addExportDef(getFontDefForExport(props.font))
|
||||||
const theme = getDefaultColorTheme(ctx)
|
const theme = getDefaultColorTheme(ctx)
|
||||||
|
|
||||||
const bounds = this.editor.getShapeGeometry(shape).bounds
|
const bounds = new Box(0, 0, props.w, props.h + props.growY)
|
||||||
textEl = (
|
textEl = (
|
||||||
<SvgTextLabel
|
<SvgTextLabel
|
||||||
fontSize={LABEL_FONT_SIZES[props.size]}
|
fontSize={LABEL_FONT_SIZES[props.size]}
|
||||||
|
@ -526,13 +548,14 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
||||||
text={props.text}
|
text={props.text}
|
||||||
labelColor={theme[props.labelColor].solid}
|
labelColor={theme[props.labelColor].solid}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
|
padding={16}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GeoShapeBody shape={shape} />
|
<GeoShapeBody shouldScale={false} shape={newShape} />
|
||||||
{textEl}
|
{textEl}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -782,8 +805,8 @@ function getLabelSize(editor: Editor, shape: TLGeoShape) {
|
||||||
const minSize = editor.textMeasure.measureText('w', {
|
const minSize = editor.textMeasure.measureText('w', {
|
||||||
...TEXT_PROPS,
|
...TEXT_PROPS,
|
||||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||||
fontSize: LABEL_FONT_SIZES[shape.props.size],
|
fontSize: LABEL_FONT_SIZES[shape.props.size] * shape.props.scale,
|
||||||
maxWidth: 100,
|
maxWidth: 100, // ?
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Can I get these from somewhere?
|
// TODO: Can I get these from somewhere?
|
||||||
|
@ -797,7 +820,7 @@ function getLabelSize(editor: Editor, shape: TLGeoShape) {
|
||||||
const size = editor.textMeasure.measureText(text, {
|
const size = editor.textMeasure.measureText(text, {
|
||||||
...TEXT_PROPS,
|
...TEXT_PROPS,
|
||||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||||
fontSize: LABEL_FONT_SIZES[shape.props.size],
|
fontSize: LABEL_FONT_SIZES[shape.props.size] * shape.props.scale,
|
||||||
minWidth: minSize.w,
|
minWidth: minSize.w,
|
||||||
maxWidth: Math.max(
|
maxWidth: Math.max(
|
||||||
// Guard because a DOM nodes can't be less 0
|
// Guard because a DOM nodes can't be less 0
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { Group2d, TLGeoShape, Vec, canonicalizeRotation, useEditor } from '@tldraw/editor'
|
import { Group2d, TLGeoShape, Vec, canonicalizeRotation, useEditor } from '@tldraw/editor'
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../../shared/ShapeFill'
|
import { ShapeFill } from '../../shared/ShapeFill'
|
||||||
import { STROKE_SIZES } from '../../shared/default-shape-constants'
|
import { STROKE_SIZES } from '../../shared/default-shape-constants'
|
||||||
import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
|
import { getPerfectDashProps } from '../../shared/getPerfectDashProps'
|
||||||
|
import { useDefaultColorTheme } from '../../shared/useDefaultColorTheme'
|
||||||
import {
|
import {
|
||||||
getCloudArcs,
|
getCloudArcs,
|
||||||
getCloudPath,
|
getCloudPath,
|
||||||
|
@ -14,12 +15,13 @@ import {
|
||||||
} from '../geo-shape-helpers'
|
} from '../geo-shape-helpers'
|
||||||
import { getLines } from '../getLines'
|
import { getLines } from '../getLines'
|
||||||
|
|
||||||
export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
export function GeoShapeBody({ shape, shouldScale }: { shape: TLGeoShape; shouldScale: boolean }) {
|
||||||
|
const scaleToUse = shouldScale ? shape.props.scale : 1
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const theme = useDefaultColorTheme()
|
const theme = useDefaultColorTheme()
|
||||||
const { id, props } = shape
|
const { id, props } = shape
|
||||||
const { w, color, fill, dash, growY, size } = props
|
const { w, color, fill, dash, growY, size } = props
|
||||||
const strokeWidth = STROKE_SIZES[size]
|
const strokeWidth = STROKE_SIZES[size] * scaleToUse
|
||||||
const h = props.h + growY
|
const h = props.h + growY
|
||||||
|
|
||||||
switch (props.geo) {
|
switch (props.geo) {
|
||||||
|
@ -28,7 +30,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
const d = getCloudPath(w, h, id, size)
|
const d = getCloudPath(w, h, id, size)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
|
||||||
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -36,17 +38,17 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
const d = inkyCloudSvgPath(w, h, id, size)
|
const d = inkyCloudSvgPath(w, h, id, size)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
|
||||||
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const innerPath = getCloudPath(w, h, id, size)
|
const d = getCloudPath(w, h, id, size)
|
||||||
const arcs = getCloudArcs(w, h, id, size)
|
const arcs = getCloudArcs(w, h, id, size)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShapeFill theme={theme} d={innerPath} color={color} fill={fill} />
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
|
||||||
<g
|
<g
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
stroke={theme[color].solid}
|
stroke={theme[color].solid}
|
||||||
|
@ -91,7 +93,11 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'ellipse': {
|
case 'ellipse': {
|
||||||
const geometry = editor.getShapeGeometry(shape)
|
const geometry = shouldScale
|
||||||
|
? // cached
|
||||||
|
editor.getShapeGeometry(shape)
|
||||||
|
: // not cached
|
||||||
|
editor.getShapeUtil(shape).getGeometry(shape)
|
||||||
const d = geometry.getSvgPathData(true)
|
const d = geometry.getSvgPathData(true)
|
||||||
|
|
||||||
if (dash === 'dashed' || dash === 'dotted') {
|
if (dash === 'dashed' || dash === 'dotted') {
|
||||||
|
@ -108,7 +114,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
|
||||||
<path
|
<path
|
||||||
d={d}
|
d={d}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
|
@ -120,18 +126,26 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const geometry = editor.getShapeGeometry(shape)
|
const geometry = shouldScale
|
||||||
|
? // cached
|
||||||
|
editor.getShapeGeometry(shape)
|
||||||
|
: // not cached
|
||||||
|
editor.getShapeUtil(shape).getGeometry(shape)
|
||||||
const d = geometry.getSvgPathData(true)
|
const d = geometry.getSvgPathData(true)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
|
||||||
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'oval': {
|
case 'oval': {
|
||||||
const geometry = editor.getShapeGeometry(shape)
|
const geometry = shouldScale
|
||||||
|
? // cached
|
||||||
|
editor.getShapeGeometry(shape)
|
||||||
|
: // not cached
|
||||||
|
editor.getShapeUtil(shape).getGeometry(shape)
|
||||||
const d = geometry.getSvgPathData(true)
|
const d = geometry.getSvgPathData(true)
|
||||||
if (dash === 'dashed' || dash === 'dotted') {
|
if (dash === 'dashed' || dash === 'dotted') {
|
||||||
const perimeter = geometry.getLength()
|
const perimeter = geometry.getLength()
|
||||||
|
@ -149,7 +163,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
|
||||||
<path
|
<path
|
||||||
d={d}
|
d={d}
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
|
@ -163,7 +177,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
|
||||||
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -176,7 +190,7 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
|
||||||
{curves.map((c, i) => {
|
{curves.map((c, i) => {
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
c.length,
|
c.length,
|
||||||
|
@ -208,14 +222,19 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
const d = getDrawHeartPath(w, h, strokeWidth, shape.id)
|
const d = getDrawHeartPath(w, h, strokeWidth, shape.id)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShapeFill d={d} color={color} fill={fill} theme={theme} />
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
|
||||||
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
const geometry = editor.getShapeGeometry(shape)
|
const geometry = shouldScale
|
||||||
|
? // cached
|
||||||
|
editor.getShapeGeometry(shape)
|
||||||
|
: // not cached
|
||||||
|
editor.getShapeUtil(shape).getGeometry(shape)
|
||||||
|
|
||||||
const outline =
|
const outline =
|
||||||
geometry instanceof Group2d ? geometry.children[0].vertices : geometry.vertices
|
geometry instanceof Group2d ? geometry.children[0].vertices : geometry.vertices
|
||||||
const lines = getLines(shape.props, strokeWidth)
|
const lines = getLines(shape.props, strokeWidth)
|
||||||
|
@ -231,16 +250,16 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShapeFill theme={theme} d={d} color={color} fill={fill} />
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
|
||||||
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
} else if (dash === 'dashed' || dash === 'dotted') {
|
} else if (dash === 'dashed' || dash === 'dotted') {
|
||||||
const innerPath = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
|
const d = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShapeFill theme={theme} d={innerPath} fill={fill} color={color} />
|
<ShapeFill theme={theme} d={d} color={color} fill={fill} scale={scaleToUse} />
|
||||||
<g
|
<g
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
stroke={theme[color].solid}
|
stroke={theme[color].solid}
|
||||||
|
@ -304,14 +323,9 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
} else if (dash === 'draw') {
|
} else if (dash === 'draw') {
|
||||||
const polygonPoints = getRoundedPolygonPoints(
|
let d = getRoundedInkyPolygonPath(
|
||||||
id,
|
getRoundedPolygonPoints(id, outline, strokeWidth / 3, strokeWidth * 2, 2)
|
||||||
outline,
|
|
||||||
strokeWidth / 3,
|
|
||||||
strokeWidth * 2,
|
|
||||||
2
|
|
||||||
)
|
)
|
||||||
let d = getRoundedInkyPolygonPath(polygonPoints)
|
|
||||||
|
|
||||||
if (lines) {
|
if (lines) {
|
||||||
for (const [A, B] of lines) {
|
for (const [A, B] of lines) {
|
||||||
|
@ -319,12 +333,19 @@ export function GeoShapeBody({ shape }: { shape: TLGeoShape }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const innerPolygonPoints = getRoundedPolygonPoints(id, outline, 0, strokeWidth * 2, 1)
|
const innerPathData = getRoundedInkyPolygonPath(
|
||||||
const innerPathData = getRoundedInkyPolygonPath(innerPolygonPoints)
|
getRoundedPolygonPoints(id, outline, 0, strokeWidth * 2, 1)
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShapeFill d={innerPathData} fill={fill} color={color} theme={theme} />
|
<ShapeFill
|
||||||
|
theme={theme}
|
||||||
|
d={innerPathData}
|
||||||
|
color={color}
|
||||||
|
fill={fill}
|
||||||
|
scale={scaleToUse}
|
||||||
|
/>
|
||||||
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
<path d={d} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
GeoShapeGeoStyle,
|
GeoShapeGeoStyle,
|
||||||
StateNode,
|
StateNode,
|
||||||
TLEventHandlers,
|
TLEventHandlers,
|
||||||
TLGeoShape,
|
TLGeoShape,
|
||||||
|
Vec,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ export class Pointing extends StateNode {
|
||||||
w: 1,
|
w: 1,
|
||||||
h: 1,
|
h: 1,
|
||||||
geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle),
|
geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle),
|
||||||
|
scale: this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
@ -73,6 +74,17 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
this.editor.mark(this.markId)
|
this.editor.mark(this.markId)
|
||||||
|
|
||||||
|
const scale = this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1
|
||||||
|
|
||||||
|
const geo = this.editor.getStyleForNextShape(GeoShapeGeoStyle)
|
||||||
|
|
||||||
|
const size =
|
||||||
|
geo === 'star'
|
||||||
|
? { w: 200, h: 190 }
|
||||||
|
: geo === 'cloud'
|
||||||
|
? { w: 300, h: 180 }
|
||||||
|
: { w: 200, h: 200 }
|
||||||
|
|
||||||
this.editor.createShapes<TLGeoShape>([
|
this.editor.createShapes<TLGeoShape>([
|
||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
|
@ -81,8 +93,8 @@ export class Pointing extends StateNode {
|
||||||
y: originPagePoint.y,
|
y: originPagePoint.y,
|
||||||
props: {
|
props: {
|
||||||
geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle),
|
geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle),
|
||||||
w: 1,
|
scale,
|
||||||
h: 1,
|
...size,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
@ -90,31 +102,24 @@ export class Pointing extends StateNode {
|
||||||
const shape = this.editor.getShape<TLGeoShape>(id)!
|
const shape = this.editor.getShape<TLGeoShape>(id)!
|
||||||
if (!shape) return
|
if (!shape) return
|
||||||
|
|
||||||
const bounds =
|
const { w, h } = shape.props
|
||||||
shape.props.geo === 'star'
|
|
||||||
? new Box(0, 0, 200, 190)
|
|
||||||
: shape.props.geo === 'cloud'
|
|
||||||
? new Box(0, 0, 300, 180)
|
|
||||||
: new Box(0, 0, 200, 200)
|
|
||||||
|
|
||||||
const delta = bounds.center
|
const delta = new Vec(w / 2, h / 2).mul(scale)
|
||||||
const parentTransform = this.editor.getShapeParentTransform(shape)
|
const parentTransform = this.editor.getShapeParentTransform(shape)
|
||||||
if (parentTransform) delta.rot(-parentTransform.rotation())
|
if (parentTransform) delta.rot(-parentTransform.rotation())
|
||||||
|
|
||||||
this.editor.select(id)
|
this.editor.select(id)
|
||||||
this.editor.updateShapes<TLGeoShape>([
|
this.editor.updateShape<TLGeoShape>({
|
||||||
{
|
id: shape.id,
|
||||||
id: shape.id,
|
type: 'geo',
|
||||||
type: 'geo',
|
x: shape.x - delta.x,
|
||||||
x: shape.x - delta.x,
|
y: shape.y - delta.y,
|
||||||
y: shape.y - delta.y,
|
props: {
|
||||||
props: {
|
geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle),
|
||||||
geo: this.editor.getStyleForNextShape(GeoShapeGeoStyle),
|
w: w * scale,
|
||||||
w: bounds.width,
|
h: h * scale,
|
||||||
h: bounds.height,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
])
|
})
|
||||||
|
|
||||||
if (this.editor.getInstanceState().isToolLocked) {
|
if (this.editor.getInstanceState().isToolLocked) {
|
||||||
this.parent.transition('idle')
|
this.parent.transition('idle')
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import {
|
import {
|
||||||
Circle2d,
|
Circle2d,
|
||||||
|
Editor,
|
||||||
Polygon2d,
|
Polygon2d,
|
||||||
SVGContainer,
|
SVGContainer,
|
||||||
ShapeUtil,
|
ShapeUtil,
|
||||||
|
@ -12,16 +13,16 @@ import {
|
||||||
highlightShapeProps,
|
highlightShapeProps,
|
||||||
last,
|
last,
|
||||||
rng,
|
rng,
|
||||||
|
useValue,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { getHighlightFreehandSettings, getPointsFromSegments } from '../draw/getPath'
|
import { getHighlightFreehandSettings, getPointsFromSegments } from '../draw/getPath'
|
||||||
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
|
||||||
import { FONT_SIZES } from '../shared/default-shape-constants'
|
import { FONT_SIZES } from '../shared/default-shape-constants'
|
||||||
import { getStrokeOutlinePoints } from '../shared/freehand/getStrokeOutlinePoints'
|
import { getStrokeOutlinePoints } from '../shared/freehand/getStrokeOutlinePoints'
|
||||||
import { getStrokePoints } from '../shared/freehand/getStrokePoints'
|
import { getStrokePoints } from '../shared/freehand/getStrokePoints'
|
||||||
import { setStrokePointRadii } from '../shared/freehand/setStrokePointRadii'
|
import { setStrokePointRadii } from '../shared/freehand/setStrokePointRadii'
|
||||||
import { getSvgPathFromStrokePoints } from '../shared/freehand/svg'
|
import { getSvgPathFromStrokePoints } from '../shared/freehand/svg'
|
||||||
import { useColorSpace } from '../shared/useColorSpace'
|
import { useColorSpace } from '../shared/useColorSpace'
|
||||||
import { useForceSolid } from '../shared/useForceSolid'
|
import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
|
||||||
|
|
||||||
const OVERLAY_OPACITY = 0.35
|
const OVERLAY_OPACITY = 0.35
|
||||||
const UNDERLAY_OPACITY = 0.82
|
const UNDERLAY_OPACITY = 0.82
|
||||||
|
@ -43,6 +44,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
||||||
size: 'm',
|
size: 'm',
|
||||||
isComplete: false,
|
isComplete: false,
|
||||||
isPen: false,
|
isPen: false,
|
||||||
|
scale: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,38 +70,43 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
component(shape: TLHighlightShape) {
|
component(shape: TLHighlightShape) {
|
||||||
|
const forceSolid = useHighlightForceSolid(this.editor, shape)
|
||||||
|
const strokeWidth = getStrokeWidth(shape)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SVGContainer id={shape.id} style={{ opacity: OVERLAY_OPACITY }}>
|
<SVGContainer id={shape.id}>
|
||||||
<HighlightRenderer strokeWidth={getStrokeWidth(shape)} shape={shape} />
|
<HighlightRenderer
|
||||||
|
shape={shape}
|
||||||
|
forceSolid={forceSolid}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
opacity={OVERLAY_OPACITY}
|
||||||
|
/>
|
||||||
</SVGContainer>
|
</SVGContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override backgroundComponent(shape: TLHighlightShape) {
|
override backgroundComponent(shape: TLHighlightShape) {
|
||||||
|
const forceSolid = useHighlightForceSolid(this.editor, shape)
|
||||||
|
const strokeWidth = getStrokeWidth(shape)
|
||||||
return (
|
return (
|
||||||
<SVGContainer id={shape.id} style={{ opacity: UNDERLAY_OPACITY }}>
|
<SVGContainer id={shape.id}>
|
||||||
<HighlightRenderer strokeWidth={getStrokeWidth(shape)} shape={shape} />
|
<HighlightRenderer
|
||||||
|
shape={shape}
|
||||||
|
forceSolid={forceSolid}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
opacity={UNDERLAY_OPACITY}
|
||||||
|
/>
|
||||||
</SVGContainer>
|
</SVGContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
indicator(shape: TLHighlightShape) {
|
indicator(shape: TLHighlightShape) {
|
||||||
const forceSolid = useForceSolid()
|
const forceSolid = useHighlightForceSolid(this.editor, shape)
|
||||||
const strokeWidth = getStrokeWidth(shape)
|
const strokeWidth = getStrokeWidth(shape)
|
||||||
|
|
||||||
|
const { strokePoints, sw } = getHighlightStrokePoints(shape, strokeWidth, forceSolid)
|
||||||
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
||||||
|
|
||||||
let sw = strokeWidth
|
|
||||||
if (!forceSolid && !shape.props.isPen && allPointsFromSegments.length === 1) {
|
|
||||||
sw += rng(shape.id)() * (strokeWidth / 6)
|
|
||||||
}
|
|
||||||
|
|
||||||
const showAsComplete = shape.props.isComplete || last(shape.props.segments)?.type === 'straight'
|
|
||||||
const options = getHighlightFreehandSettings({
|
|
||||||
strokeWidth,
|
|
||||||
showAsComplete,
|
|
||||||
})
|
|
||||||
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
|
||||||
|
|
||||||
let strokePath
|
let strokePath
|
||||||
if (strokePoints.length < 2) {
|
if (strokePoints.length < 2) {
|
||||||
strokePath = getIndicatorDot(allPointsFromSegments[0], sw)
|
strokePath = getIndicatorDot(allPointsFromSegments[0], sw)
|
||||||
|
@ -111,22 +118,34 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override toSvg(shape: TLHighlightShape) {
|
override toSvg(shape: TLHighlightShape) {
|
||||||
|
const strokeWidth = getStrokeWidth(shape)
|
||||||
|
const forceSolid = strokeWidth < 1.5
|
||||||
|
const scaleFactor = 1 / shape.props.scale
|
||||||
return (
|
return (
|
||||||
<HighlightRenderer
|
<g transform={`scale(${scaleFactor})`}>
|
||||||
strokeWidth={getStrokeWidth(shape)}
|
<HighlightRenderer
|
||||||
shape={shape}
|
forceSolid={forceSolid}
|
||||||
opacity={OVERLAY_OPACITY}
|
strokeWidth={strokeWidth}
|
||||||
/>
|
shape={shape}
|
||||||
|
opacity={OVERLAY_OPACITY}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override toBackgroundSvg(shape: TLHighlightShape) {
|
override toBackgroundSvg(shape: TLHighlightShape) {
|
||||||
|
const strokeWidth = getStrokeWidth(shape)
|
||||||
|
const forceSolid = strokeWidth < 1.5
|
||||||
|
const scaleFactor = 1 / shape.props.scale
|
||||||
return (
|
return (
|
||||||
<HighlightRenderer
|
<g transform={`scale(${scaleFactor})`}>
|
||||||
strokeWidth={getStrokeWidth(shape)}
|
<HighlightRenderer
|
||||||
shape={shape}
|
forceSolid={forceSolid}
|
||||||
opacity={UNDERLAY_OPACITY}
|
strokeWidth={strokeWidth}
|
||||||
/>
|
shape={shape}
|
||||||
|
opacity={UNDERLAY_OPACITY}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,34 +206,52 @@ function getHighlightStrokePoints(
|
||||||
strokeWidth: sw,
|
strokeWidth: sw,
|
||||||
showAsComplete,
|
showAsComplete,
|
||||||
})
|
})
|
||||||
|
|
||||||
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
||||||
|
|
||||||
return { strokePoints, sw }
|
return { strokePoints, sw }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHighlightSvgPath(shape: TLHighlightShape, strokeWidth: number, forceSolid: boolean) {
|
function getStrokeWidth(shape: TLHighlightShape) {
|
||||||
const { strokePoints, sw } = getHighlightStrokePoints(shape, strokeWidth, forceSolid)
|
return FONT_SIZES[shape.props.size] * 1.12 * shape.props.scale
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIsDot(shape: TLHighlightShape) {
|
||||||
|
return shape.props.segments.length === 1 && shape.props.segments[0].points.length < 2
|
||||||
|
}
|
||||||
|
|
||||||
|
function HighlightRenderer({
|
||||||
|
strokeWidth,
|
||||||
|
forceSolid,
|
||||||
|
shape,
|
||||||
|
opacity,
|
||||||
|
}: {
|
||||||
|
strokeWidth: number
|
||||||
|
forceSolid: boolean
|
||||||
|
shape: TLHighlightShape
|
||||||
|
opacity: number
|
||||||
|
}) {
|
||||||
|
const theme = useDefaultColorTheme()
|
||||||
|
|
||||||
|
const allPointsFromSegments = getPointsFromSegments(shape.props.segments)
|
||||||
|
|
||||||
|
let sw = strokeWidth
|
||||||
|
if (!forceSolid && !shape.props.isPen && allPointsFromSegments.length === 1) {
|
||||||
|
sw += rng(shape.id)() * (sw / 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = getHighlightFreehandSettings({
|
||||||
|
strokeWidth: sw,
|
||||||
|
showAsComplete: shape.props.isComplete || last(shape.props.segments)?.type === 'straight',
|
||||||
|
})
|
||||||
|
|
||||||
|
const strokePoints = getStrokePoints(allPointsFromSegments, options)
|
||||||
|
|
||||||
const solidStrokePath =
|
const solidStrokePath =
|
||||||
strokePoints.length > 1
|
strokePoints.length > 1
|
||||||
? getSvgPathFromStrokePoints(strokePoints, false)
|
? getSvgPathFromStrokePoints(strokePoints, false)
|
||||||
: getShapeDot(shape.props.segments[0].points[0])
|
: getShapeDot(shape.props.segments[0].points[0])
|
||||||
|
|
||||||
return { solidStrokePath, sw }
|
|
||||||
}
|
|
||||||
|
|
||||||
function HighlightRenderer({
|
|
||||||
strokeWidth,
|
|
||||||
shape,
|
|
||||||
opacity,
|
|
||||||
}: {
|
|
||||||
strokeWidth: number
|
|
||||||
shape: TLHighlightShape
|
|
||||||
opacity?: number
|
|
||||||
}) {
|
|
||||||
const theme = useDefaultColorTheme()
|
|
||||||
const forceSolid = useForceSolid()
|
|
||||||
const { solidStrokePath, sw } = getHighlightSvgPath(shape, strokeWidth, forceSolid)
|
|
||||||
const colorSpace = useColorSpace()
|
const colorSpace = useColorSpace()
|
||||||
const color = theme[shape.props.color].highlight[colorSpace]
|
const color = theme[shape.props.color].highlight[colorSpace]
|
||||||
|
|
||||||
|
@ -231,10 +268,17 @@ function HighlightRenderer({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStrokeWidth(shape: TLHighlightShape) {
|
function useHighlightForceSolid(editor: Editor, shape: TLHighlightShape) {
|
||||||
return FONT_SIZES[shape.props.size] * 1.12
|
return useValue(
|
||||||
}
|
'forceSolid',
|
||||||
|
() => {
|
||||||
function getIsDot(shape: TLHighlightShape) {
|
const sw = getStrokeWidth(shape)
|
||||||
return shape.props.segments.length === 1 && shape.props.segments[0].points.length < 2
|
const zoomLevel = editor.getZoomLevel()
|
||||||
|
if (sw / zoomLevel < 1.5) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
[editor]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,9 @@ import {
|
||||||
sortByIndex,
|
sortByIndex,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
|
||||||
import { ShapeFill, useDefaultColorTheme } from '../shared/ShapeFill'
|
|
||||||
import { STROKE_SIZES } from '../shared/default-shape-constants'
|
import { STROKE_SIZES } from '../shared/default-shape-constants'
|
||||||
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
|
import { getPerfectDashProps } from '../shared/getPerfectDashProps'
|
||||||
|
import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
|
||||||
import { getLineDrawPath, getLineIndicatorPath } from './components/getLinePath'
|
import { getLineDrawPath, getLineIndicatorPath } from './components/getLinePath'
|
||||||
import { getDrawLinePathData } from './line-helpers'
|
import { getDrawLinePathData } from './line-helpers'
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
[start]: { id: start, index: start, x: 0, y: 0 },
|
[start]: { id: start, index: start, x: 0, y: 0 },
|
||||||
[end]: { id: end, index: end, x: 0.1, y: 0.1 },
|
[end]: { id: end, index: end, x: 0.1, y: 0.1 },
|
||||||
},
|
},
|
||||||
|
scale: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +130,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
indicator(shape: TLLineShape) {
|
indicator(shape: TLLineShape) {
|
||||||
const strokeWidth = STROKE_SIZES[shape.props.size]
|
const strokeWidth = STROKE_SIZES[shape.props.size] * shape.props.scale
|
||||||
const spline = getGeometryForLineShape(shape)
|
const spline = getGeometryForLineShape(shape)
|
||||||
const { dash } = shape.props
|
const { dash } = shape.props
|
||||||
|
|
||||||
|
@ -151,7 +152,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override toSvg(shape: TLLineShape) {
|
override toSvg(shape: TLLineShape) {
|
||||||
return <LineShapeSvg shape={shape} />
|
return <LineShapeSvg shouldScale shape={shape} />
|
||||||
}
|
}
|
||||||
|
|
||||||
override getHandleSnapGeometry(shape: TLLineShape): HandleSnapGeometry {
|
override getHandleSnapGeometry(shape: TLLineShape): HandleSnapGeometry {
|
||||||
|
@ -204,12 +205,23 @@ export function getGeometryForLineShape(shape: TLLineShape): CubicSpline2d | Pol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function LineShapeSvg({ shape }: { shape: TLLineShape }) {
|
function LineShapeSvg({
|
||||||
|
shape,
|
||||||
|
shouldScale = false,
|
||||||
|
}: {
|
||||||
|
shape: TLLineShape
|
||||||
|
shouldScale?: boolean
|
||||||
|
}) {
|
||||||
const theme = useDefaultColorTheme()
|
const theme = useDefaultColorTheme()
|
||||||
const spline = getGeometryForLineShape(shape)
|
|
||||||
const strokeWidth = STROKE_SIZES[shape.props.size]
|
|
||||||
|
|
||||||
const { dash, color } = shape.props
|
const spline = getGeometryForLineShape(shape)
|
||||||
|
const { dash, color, size } = shape.props
|
||||||
|
|
||||||
|
const scaleFactor = 1 / shape.props.scale
|
||||||
|
|
||||||
|
const scale = shouldScale ? scaleFactor : 1
|
||||||
|
|
||||||
|
const strokeWidth = STROKE_SIZES[size] * shape.props.scale
|
||||||
|
|
||||||
// Line style lines
|
// Line style lines
|
||||||
if (shape.props.spline === 'line') {
|
if (shape.props.spline === 'line') {
|
||||||
|
@ -218,61 +230,56 @@ function LineShapeSvg({ shape }: { shape: TLLineShape }) {
|
||||||
const pathData = 'M' + outline[0] + 'L' + outline.slice(1)
|
const pathData = 'M' + outline[0] + 'L' + outline.slice(1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<path
|
||||||
<ShapeFill d={pathData} fill={'none'} color={color} theme={theme} />
|
d={pathData}
|
||||||
<path d={pathData} stroke={theme[color].solid} strokeWidth={strokeWidth} fill="none" />
|
stroke={theme[color].solid}
|
||||||
</>
|
strokeWidth={strokeWidth}
|
||||||
|
fill="none"
|
||||||
|
transform={`scale(${scale})`}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dash === 'dashed' || dash === 'dotted') {
|
if (dash === 'dashed' || dash === 'dotted') {
|
||||||
const outline = spline.points
|
|
||||||
const pathData = 'M' + outline[0] + 'L' + outline.slice(1)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<g stroke={theme[color].solid} strokeWidth={strokeWidth} transform={`scale(${scale})`}>
|
||||||
<ShapeFill d={pathData} fill={'none'} color={color} theme={theme} />
|
{spline.segments.map((segment, i) => {
|
||||||
<g stroke={theme[color].solid} strokeWidth={strokeWidth}>
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
{spline.segments.map((segment, i) => {
|
segment.length,
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
strokeWidth,
|
||||||
segment.length,
|
{
|
||||||
strokeWidth,
|
style: dash,
|
||||||
{
|
start: i > 0 ? 'outset' : 'none',
|
||||||
style: dash,
|
end: i < spline.segments.length - 1 ? 'outset' : 'none',
|
||||||
start: i > 0 ? 'outset' : 'none',
|
}
|
||||||
end: i < spline.segments.length - 1 ? 'outset' : 'none',
|
)
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<path
|
<path
|
||||||
key={i}
|
key={i}
|
||||||
strokeDasharray={strokeDasharray}
|
strokeDasharray={strokeDasharray}
|
||||||
strokeDashoffset={strokeDashoffset}
|
strokeDashoffset={strokeDashoffset}
|
||||||
d={segment.getSvgPathData(true)}
|
d={segment.getSvgPathData(true)}
|
||||||
fill="none"
|
fill="none"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</g>
|
</g>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dash === 'draw') {
|
if (dash === 'draw') {
|
||||||
const outline = spline.points
|
const outline = spline.points
|
||||||
const [innerPathData, outerPathData] = getDrawLinePathData(shape.id, outline, strokeWidth)
|
const [_, outerPathData] = getDrawLinePathData(shape.id, outline, strokeWidth)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<path
|
||||||
<ShapeFill d={innerPathData} fill={'none'} color={color} theme={theme} />
|
d={outerPathData}
|
||||||
<path
|
stroke={theme[color].solid}
|
||||||
d={outerPathData}
|
strokeWidth={strokeWidth}
|
||||||
stroke={theme[color].solid}
|
fill="none"
|
||||||
strokeWidth={strokeWidth}
|
transform={`scale(${scale})`}
|
||||||
fill="none"
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -281,55 +288,53 @@ function LineShapeSvg({ shape }: { shape: TLLineShape }) {
|
||||||
const splinePath = spline.getSvgPathData()
|
const splinePath = spline.getSvgPathData()
|
||||||
if (dash === 'solid') {
|
if (dash === 'solid') {
|
||||||
return (
|
return (
|
||||||
<>
|
<path
|
||||||
<ShapeFill d={splinePath} fill={'none'} color={color} theme={theme} />
|
strokeWidth={strokeWidth}
|
||||||
<path strokeWidth={strokeWidth} stroke={theme[color].solid} fill="none" d={splinePath} />
|
stroke={theme[color].solid}
|
||||||
</>
|
fill="none"
|
||||||
|
d={splinePath}
|
||||||
|
transform={`scale(${scale})`}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dash === 'dashed' || dash === 'dotted') {
|
if (dash === 'dashed' || dash === 'dotted') {
|
||||||
return (
|
return (
|
||||||
<>
|
<g stroke={theme[color].solid} strokeWidth={strokeWidth} transform={`scale(${scale})`}>
|
||||||
<ShapeFill d={splinePath} fill={'none'} color={color} theme={theme} />
|
{spline.segments.map((segment, i) => {
|
||||||
<g stroke={theme[color].solid} strokeWidth={strokeWidth}>
|
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
||||||
{spline.segments.map((segment, i) => {
|
segment.length,
|
||||||
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
|
strokeWidth,
|
||||||
segment.length,
|
{
|
||||||
strokeWidth,
|
style: dash,
|
||||||
{
|
start: i > 0 ? 'outset' : 'none',
|
||||||
style: dash,
|
end: i < spline.segments.length - 1 ? 'outset' : 'none',
|
||||||
start: i > 0 ? 'outset' : 'none',
|
}
|
||||||
end: i < spline.segments.length - 1 ? 'outset' : 'none',
|
)
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<path
|
<path
|
||||||
key={i}
|
key={i}
|
||||||
strokeDasharray={strokeDasharray}
|
strokeDasharray={strokeDasharray}
|
||||||
strokeDashoffset={strokeDashoffset}
|
strokeDashoffset={strokeDashoffset}
|
||||||
d={segment.getSvgPathData()}
|
d={segment.getSvgPathData()}
|
||||||
fill="none"
|
fill="none"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</g>
|
</g>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dash === 'draw') {
|
if (dash === 'draw') {
|
||||||
return (
|
return (
|
||||||
<>
|
<path
|
||||||
<ShapeFill d={splinePath} fill={'none'} color={color} theme={theme} />
|
d={getLineDrawPath(shape, spline, strokeWidth)}
|
||||||
<path
|
strokeWidth={1}
|
||||||
d={getLineDrawPath(shape, spline, strokeWidth)}
|
stroke={theme[color].solid}
|
||||||
strokeWidth={1}
|
fill={theme[color].solid}
|
||||||
stroke={theme[color].solid}
|
transform={`scale(${scale})`}
|
||||||
fill={theme[color].solid}
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ exports[`Misc resizes: line shape after resize 1`] = `
|
||||||
"y": 700,
|
"y": 700,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "line",
|
"spline": "line",
|
||||||
},
|
},
|
||||||
|
|
|
@ -95,6 +95,9 @@ export class Pointing extends StateNode {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
x: currentPagePoint.x,
|
x: currentPagePoint.x,
|
||||||
y: currentPagePoint.y,
|
y: currentPagePoint.y,
|
||||||
|
props: {
|
||||||
|
scale: this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
Editor,
|
Editor,
|
||||||
Group2d,
|
Group2d,
|
||||||
IndexKey,
|
IndexKey,
|
||||||
|
@ -24,7 +26,6 @@ import { useCallback } from 'react'
|
||||||
import { useCurrentTranslation } from '../../ui/hooks/useTranslation/useTranslation'
|
import { useCurrentTranslation } from '../../ui/hooks/useTranslation/useTranslation'
|
||||||
import { isRightToLeftLanguage } from '../../utils/text/text'
|
import { isRightToLeftLanguage } from '../../utils/text/text'
|
||||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||||
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
|
||||||
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
||||||
import { TextLabel } from '../shared/TextLabel'
|
import { TextLabel } from '../shared/TextLabel'
|
||||||
import {
|
import {
|
||||||
|
@ -35,8 +36,8 @@ import {
|
||||||
} from '../shared/default-shape-constants'
|
} from '../shared/default-shape-constants'
|
||||||
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
||||||
|
|
||||||
|
import { useDefaultColorTheme } from '../../..'
|
||||||
import { startEditingShapeWithLabel } from '../../tools/SelectTool/selectHelpers'
|
import { startEditingShapeWithLabel } from '../../tools/SelectTool/selectHelpers'
|
||||||
import { useForceSolid } from '../shared/useForceSolid'
|
|
||||||
import {
|
import {
|
||||||
CLONE_HANDLE_MARGIN,
|
CLONE_HANDLE_MARGIN,
|
||||||
NOTE_CENTER_OFFSET,
|
NOTE_CENTER_OFFSET,
|
||||||
|
@ -65,31 +66,37 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
growY: 0,
|
growY: 0,
|
||||||
fontSizeAdjustment: 0,
|
fontSizeAdjustment: 0,
|
||||||
url: '',
|
url: '',
|
||||||
|
scale: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getGeometry(shape: TLNoteShape) {
|
getGeometry(shape: TLNoteShape) {
|
||||||
const noteHeight = getNoteHeight(shape)
|
|
||||||
const { labelHeight, labelWidth } = getLabelSize(this.editor, shape)
|
const { labelHeight, labelWidth } = getLabelSize(this.editor, shape)
|
||||||
|
const { scale } = shape.props
|
||||||
|
|
||||||
|
const lh = labelHeight * scale
|
||||||
|
const lw = labelWidth * scale
|
||||||
|
const nw = NOTE_SIZE * scale
|
||||||
|
const nh = getNoteHeight(shape)
|
||||||
|
|
||||||
return new Group2d({
|
return new Group2d({
|
||||||
children: [
|
children: [
|
||||||
new Rectangle2d({ width: NOTE_SIZE, height: noteHeight, isFilled: true }),
|
new Rectangle2d({ width: nw, height: nh, isFilled: true }),
|
||||||
new Rectangle2d({
|
new Rectangle2d({
|
||||||
x:
|
x:
|
||||||
shape.props.align === 'start'
|
shape.props.align === 'start'
|
||||||
? 0
|
? 0
|
||||||
: shape.props.align === 'end'
|
: shape.props.align === 'end'
|
||||||
? NOTE_SIZE - labelWidth
|
? nw - lw
|
||||||
: (NOTE_SIZE - labelWidth) / 2,
|
: (nw - lw) / 2,
|
||||||
y:
|
y:
|
||||||
shape.props.verticalAlign === 'start'
|
shape.props.verticalAlign === 'start'
|
||||||
? 0
|
? 0
|
||||||
: shape.props.verticalAlign === 'end'
|
: shape.props.verticalAlign === 'end'
|
||||||
? noteHeight - labelHeight
|
? nh - lh
|
||||||
: (noteHeight - labelHeight) / 2,
|
: (nh - lh) / 2,
|
||||||
width: labelWidth,
|
width: lw,
|
||||||
height: labelHeight,
|
height: lh,
|
||||||
isFilled: true,
|
isFilled: true,
|
||||||
isLabel: true,
|
isLabel: true,
|
||||||
}),
|
}),
|
||||||
|
@ -98,21 +105,25 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override getHandles(shape: TLNoteShape): TLHandle[] {
|
override getHandles(shape: TLNoteShape): TLHandle[] {
|
||||||
const zoom = this.editor.getZoomLevel()
|
const { scale } = shape.props
|
||||||
const offset = CLONE_HANDLE_MARGIN / zoom
|
|
||||||
const noteHeight = getNoteHeight(shape)
|
|
||||||
const isCoarsePointer = this.editor.getInstanceState().isCoarsePointer
|
const isCoarsePointer = this.editor.getInstanceState().isCoarsePointer
|
||||||
|
if (isCoarsePointer) return []
|
||||||
|
|
||||||
if (zoom < 0.25 || isCoarsePointer) return []
|
const zoom = this.editor.getZoomLevel()
|
||||||
|
if (zoom * scale < 0.25) return []
|
||||||
|
|
||||||
if (zoom < 0.5) {
|
const nh = getNoteHeight(shape)
|
||||||
|
const nw = NOTE_SIZE * scale
|
||||||
|
const offset = (CLONE_HANDLE_MARGIN / zoom) * scale
|
||||||
|
|
||||||
|
if (zoom * scale < 0.5) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'bottom',
|
id: 'bottom',
|
||||||
index: 'a3' as IndexKey,
|
index: 'a3' as IndexKey,
|
||||||
type: 'clone',
|
type: 'clone',
|
||||||
x: NOTE_SIZE / 2,
|
x: nw / 2,
|
||||||
y: noteHeight + offset,
|
y: nh + offset,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -122,29 +133,29 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
id: 'top',
|
id: 'top',
|
||||||
index: 'a1' as IndexKey,
|
index: 'a1' as IndexKey,
|
||||||
type: 'clone',
|
type: 'clone',
|
||||||
x: NOTE_SIZE / 2,
|
x: nw / 2,
|
||||||
y: -offset,
|
y: -offset,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'right',
|
id: 'right',
|
||||||
index: 'a2' as IndexKey,
|
index: 'a2' as IndexKey,
|
||||||
type: 'clone',
|
type: 'clone',
|
||||||
x: NOTE_SIZE + offset,
|
x: nw + offset,
|
||||||
y: noteHeight / 2,
|
y: nh / 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bottom',
|
id: 'bottom',
|
||||||
index: 'a3' as IndexKey,
|
index: 'a3' as IndexKey,
|
||||||
type: 'clone',
|
type: 'clone',
|
||||||
x: NOTE_SIZE / 2,
|
x: nw / 2,
|
||||||
y: noteHeight + offset,
|
y: nh + offset,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'left',
|
id: 'left',
|
||||||
index: 'a4' as IndexKey,
|
index: 'a4' as IndexKey,
|
||||||
type: 'clone',
|
type: 'clone',
|
||||||
x: -offset,
|
x: -offset,
|
||||||
y: noteHeight / 2,
|
y: nh / 2,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -153,17 +164,15 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
props: { color, font, size, align, text, verticalAlign, fontSizeAdjustment },
|
props: { scale, color, font, size, align, text, verticalAlign, fontSizeAdjustment },
|
||||||
} = shape
|
} = shape
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
const handleKeyDown = useNoteKeydownHandler(id)
|
const handleKeyDown = useNoteKeydownHandler(id)
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
const theme = useDefaultColorTheme()
|
const theme = useDefaultColorTheme()
|
||||||
const noteHeight = getNoteHeight(shape)
|
const nw = NOTE_SIZE * scale
|
||||||
|
const nh = getNoteHeight(shape)
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
const rotation = useValue(
|
const rotation = useValue(
|
||||||
'shape rotation',
|
'shape rotation',
|
||||||
() => this.editor.getShapePageTransform(id)?.rotation() ?? 0,
|
() => this.editor.getShapePageTransform(id)?.rotation() ?? 0,
|
||||||
|
@ -171,8 +180,11 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
)
|
)
|
||||||
|
|
||||||
// todo: consider hiding shadows on dark mode if they're invisible anyway
|
// todo: consider hiding shadows on dark mode if they're invisible anyway
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
||||||
const hideShadows = useForceSolid()
|
const hideShadows = useValue('zoom', () => this.editor.getZoomLevel() < 0.35 / scale, [
|
||||||
|
scale,
|
||||||
|
this.editor,
|
||||||
|
])
|
||||||
|
|
||||||
const isSelected = shape.id === this.editor.getOnlySelectedShapeId()
|
const isSelected = shape.id === this.editor.getOnlySelectedShapeId()
|
||||||
|
|
||||||
|
@ -182,18 +194,18 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
id={id}
|
id={id}
|
||||||
className="tl-note__container"
|
className="tl-note__container"
|
||||||
style={{
|
style={{
|
||||||
width: NOTE_SIZE,
|
width: nw,
|
||||||
height: noteHeight,
|
height: nh,
|
||||||
backgroundColor: theme[color].note.fill,
|
backgroundColor: theme[color].note.fill,
|
||||||
borderBottom: hideShadows ? `3px solid rgb(15, 23, 31, .2)` : `none`,
|
borderBottom: hideShadows ? `${3 * scale}px solid rgb(15, 23, 31, .2)` : `none`,
|
||||||
boxShadow: hideShadows ? 'none' : getNoteShadow(shape.id, rotation),
|
boxShadow: hideShadows ? 'none' : getNoteShadow(shape.id, rotation, scale),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextLabel
|
<TextLabel
|
||||||
id={id}
|
id={id}
|
||||||
type={type}
|
type={type}
|
||||||
font={font}
|
font={font}
|
||||||
fontSize={fontSizeAdjustment || LABEL_FONT_SIZES[size]}
|
fontSize={(fontSizeAdjustment || LABEL_FONT_SIZES[size]) * scale}
|
||||||
lineHeight={TEXT_PROPS.lineHeight}
|
lineHeight={TEXT_PROPS.lineHeight}
|
||||||
align={align}
|
align={align}
|
||||||
verticalAlign={verticalAlign}
|
verticalAlign={verticalAlign}
|
||||||
|
@ -202,6 +214,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
labelColor={theme[color].note.text}
|
labelColor={theme[color].note.text}
|
||||||
wrap
|
wrap
|
||||||
|
padding={16 * scale}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -213,10 +226,11 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
indicator(shape: TLNoteShape) {
|
indicator(shape: TLNoteShape) {
|
||||||
|
const { scale } = shape.props
|
||||||
return (
|
return (
|
||||||
<rect
|
<rect
|
||||||
rx="1"
|
rx={scale}
|
||||||
width={toDomPrecision(NOTE_SIZE)}
|
width={toDomPrecision(NOTE_SIZE * scale)}
|
||||||
height={toDomPrecision(getNoteHeight(shape))}
|
height={toDomPrecision(getNoteHeight(shape))}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -226,7 +240,8 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||||
ctx.addExportDef(getFontDefForExport(shape.props.font))
|
ctx.addExportDef(getFontDefForExport(shape.props.font))
|
||||||
if (shape.props.text) ctx.addExportDef(getFontDefForExport(shape.props.font))
|
if (shape.props.text) ctx.addExportDef(getFontDefForExport(shape.props.font))
|
||||||
const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode })
|
const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode })
|
||||||
const bounds = this.editor.getShapeGeometry(shape).bounds
|
const bounds = getBoundsForSVG(shape)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<rect x={5} y={5} rx={1} width={NOTE_SIZE - 10} height={bounds.h} fill="rgba(0,0,0,.1)" />
|
<rect x={5} y={5} rx={1} width={NOTE_SIZE - 10} height={bounds.h} fill="rgba(0,0,0,.1)" />
|
||||||
|
@ -311,7 +326,7 @@ function getNoteSizeAdjustments(editor: Editor, shape: TLNoteShape) {
|
||||||
* Get the label size for a note.
|
* Get the label size for a note.
|
||||||
*/
|
*/
|
||||||
function getNoteLabelSize(editor: Editor, shape: TLNoteShape) {
|
function getNoteLabelSize(editor: Editor, shape: TLNoteShape) {
|
||||||
const text = shape.props.text
|
const { text } = shape.props
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
const minHeight = LABEL_FONT_SIZES[shape.props.size] * TEXT_PROPS.lineHeight + LABEL_PADDING * 2
|
const minHeight = LABEL_FONT_SIZES[shape.props.size] * TEXT_PROPS.lineHeight + LABEL_PADDING * 2
|
||||||
|
@ -365,9 +380,9 @@ function getNoteLabelSize(editor: Editor, shape: TLNoteShape) {
|
||||||
} while (iterations++ < 50)
|
} while (iterations++ < 50)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
labelHeight,
|
labelHeight: labelHeight,
|
||||||
labelWidth,
|
labelWidth: labelWidth,
|
||||||
fontSizeAdjustment,
|
fontSizeAdjustment: fontSizeAdjustment,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -400,17 +415,18 @@ function useNoteKeydownHandler(id: TLShapeId) {
|
||||||
const isRTL = !!(translation.dir === 'rtl' || isRightToLeftLanguage(shape.props.text))
|
const isRTL = !!(translation.dir === 'rtl' || isRightToLeftLanguage(shape.props.text))
|
||||||
|
|
||||||
const offsetLength =
|
const offsetLength =
|
||||||
NOTE_SIZE +
|
(NOTE_SIZE +
|
||||||
editor.options.adjacentShapeMargin +
|
editor.options.adjacentShapeMargin +
|
||||||
// If we're growing down, we need to account for the current shape's growY
|
// If we're growing down, we need to account for the current shape's growY
|
||||||
(isCmdEnter && !e.shiftKey ? shape.props.growY : 0)
|
(isCmdEnter && !e.shiftKey ? shape.props.growY : 0)) *
|
||||||
|
shape.props.scale
|
||||||
|
|
||||||
const adjacentCenter = new Vec(
|
const adjacentCenter = new Vec(
|
||||||
isTab ? (e.shiftKey != isRTL ? -1 : 1) : 0,
|
isTab ? (e.shiftKey != isRTL ? -1 : 1) : 0,
|
||||||
isCmdEnter ? (e.shiftKey ? -1 : 1) : 0
|
isCmdEnter ? (e.shiftKey ? -1 : 1) : 0
|
||||||
)
|
)
|
||||||
.mul(offsetLength)
|
.mul(offsetLength)
|
||||||
.add(NOTE_CENTER_OFFSET)
|
.add(NOTE_CENTER_OFFSET.clone().mul(shape.props.scale))
|
||||||
.rot(pageRotation)
|
.rot(pageRotation)
|
||||||
.add(pageTransform.point())
|
.add(pageTransform.point())
|
||||||
|
|
||||||
|
@ -427,14 +443,23 @@ function useNoteKeydownHandler(id: TLShapeId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNoteHeight(shape: TLNoteShape) {
|
function getNoteHeight(shape: TLNoteShape) {
|
||||||
return NOTE_SIZE + shape.props.growY
|
return (NOTE_SIZE + shape.props.growY) * shape.props.scale
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNoteShadow(id: string, rotation: number) {
|
function getNoteShadow(id: string, rotation: number, scale: number) {
|
||||||
const random = rng(id) // seeded based on id
|
const random = rng(id) // seeded based on id
|
||||||
const lift = Math.abs(random()) + 0.5 // 0 to 1.5
|
const lift = Math.abs(random()) + 0.5 // 0 to 1.5
|
||||||
const oy = Math.cos(rotation)
|
const oy = Math.cos(rotation)
|
||||||
return `0px ${5 - lift}px 5px -5px rgba(15, 23, 31, .6),
|
const a = 5 * scale
|
||||||
0px ${(4 + lift * 7) * Math.max(0, oy)}px ${6 + lift * 7}px -${4 + lift * 6}px rgba(15, 23, 31, ${(0.3 + lift * 0.1).toFixed(2)}),
|
const b = 4 * scale
|
||||||
0px 48px 10px -10px inset rgba(15, 23, 44, ${((0.022 + random() * 0.005) * ((1 + oy) / 2)).toFixed(2)})`
|
const c = 6 * scale
|
||||||
|
const d = 7 * scale
|
||||||
|
return `0px ${a - lift}px ${a}px -${a}px rgba(15, 23, 31, .6),
|
||||||
|
0px ${(b + lift * d) * Math.max(0, oy)}px ${c + lift * d}px -${b + lift * c}px rgba(15, 23, 31, ${(0.3 + lift * 0.1).toFixed(2)}),
|
||||||
|
0px ${48 * scale}px ${10 * scale}px -${10 * scale}px inset rgba(15, 23, 44, ${((0.022 + random() * 0.005) * ((1 + oy) / 2)).toFixed(2)})`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBoundsForSVG(shape: TLNoteShape) {
|
||||||
|
// When rendering the SVG we don't want to adjust for scale
|
||||||
|
return new Box(0, 0, NOTE_SIZE, NOTE_SIZE + shape.props.growY)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,15 +8,27 @@ export const CLONE_HANDLE_MARGIN = 0
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const NOTE_SIZE = 200
|
export const NOTE_SIZE = 200
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const NOTE_CENTER_OFFSET = { x: NOTE_SIZE / 2, y: NOTE_SIZE / 2 }
|
export const NOTE_CENTER_OFFSET = new Vec(NOTE_SIZE / 2, NOTE_SIZE / 2)
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const NOTE_PIT_RADIUS = 10
|
export const NOTE_ADJACENT_POSITION_SNAP_RADIUS = 10
|
||||||
|
|
||||||
const DEFAULT_PITS = {
|
const BASE_NOTE_POSITIONS = [
|
||||||
['a1' as IndexKey]: new Vec(NOTE_SIZE * 0.5, NOTE_SIZE * -0.5 - ADJACENT_NOTE_MARGIN), // t
|
[['a1' as IndexKey], new Vec(NOTE_SIZE * 0.5, NOTE_SIZE * -0.5 - ADJACENT_NOTE_MARGIN)], // t
|
||||||
['a2' as IndexKey]: new Vec(NOTE_SIZE * 1.5 + ADJACENT_NOTE_MARGIN, NOTE_SIZE * 0.5), // r
|
[['a2' as IndexKey], new Vec(NOTE_SIZE * 1.5 + ADJACENT_NOTE_MARGIN, NOTE_SIZE * 0.5)], // r
|
||||||
['a3' as IndexKey]: new Vec(NOTE_SIZE * 0.5, NOTE_SIZE * 1.5 + ADJACENT_NOTE_MARGIN), // b
|
[['a3' as IndexKey], new Vec(NOTE_SIZE * 0.5, NOTE_SIZE * 1.5 + ADJACENT_NOTE_MARGIN)], // b
|
||||||
['a4' as IndexKey]: new Vec(NOTE_SIZE * -0.5 - ADJACENT_NOTE_MARGIN, NOTE_SIZE * 0.5), // l
|
[['a4' as IndexKey], new Vec(NOTE_SIZE * -0.5 - ADJACENT_NOTE_MARGIN, NOTE_SIZE * 0.5)], // l
|
||||||
|
] as const
|
||||||
|
|
||||||
|
function getBaseAdjacentNotePositions(scale: number) {
|
||||||
|
if (scale === 1) return BASE_NOTE_POSITIONS
|
||||||
|
const s = NOTE_SIZE * scale
|
||||||
|
const m = ADJACENT_NOTE_MARGIN * scale
|
||||||
|
return [
|
||||||
|
[['a1' as IndexKey], new Vec(s * 0.5, s * -0.5 - m)], // t
|
||||||
|
[['a2' as IndexKey], new Vec(s * 1.5 + m, s * 0.5)], // r
|
||||||
|
[['a3' as IndexKey], new Vec(s * 0.5, s * 1.5 + m)], // b
|
||||||
|
[['a4' as IndexKey], new Vec(s * -0.5 - m, s * 0.5)], // l
|
||||||
|
] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,10 +44,11 @@ export function getNoteAdjacentPositions(
|
||||||
pagePoint: Vec,
|
pagePoint: Vec,
|
||||||
pageRotation: number,
|
pageRotation: number,
|
||||||
growY: number,
|
growY: number,
|
||||||
extraHeight: number
|
extraHeight: number,
|
||||||
|
scale: number
|
||||||
): Record<IndexKey, Vec> {
|
): Record<IndexKey, Vec> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(DEFAULT_PITS).map(([id, v], i) => {
|
getBaseAdjacentNotePositions(scale).map(([id, v], i) => {
|
||||||
const point = v.clone()
|
const point = v.clone()
|
||||||
if (i === 0 && extraHeight) {
|
if (i === 0 && extraHeight) {
|
||||||
// apply top margin (the growY of the moving note shape)
|
// apply top margin (the growY of the moving note shape)
|
||||||
|
@ -60,6 +73,7 @@ export function getNoteAdjacentPositions(
|
||||||
export function getAvailableNoteAdjacentPositions(
|
export function getAvailableNoteAdjacentPositions(
|
||||||
editor: Editor,
|
editor: Editor,
|
||||||
rotation: number,
|
rotation: number,
|
||||||
|
scale: number,
|
||||||
extraHeight: number
|
extraHeight: number
|
||||||
) {
|
) {
|
||||||
const selectedShapeIds = new Set(editor.getSelectedShapeIds())
|
const selectedShapeIds = new Set(editor.getSelectedShapeIds())
|
||||||
|
@ -69,7 +83,11 @@ export function getAvailableNoteAdjacentPositions(
|
||||||
|
|
||||||
// Get all the positions that are adjacent to the selected note shapes
|
// Get all the positions that are adjacent to the selected note shapes
|
||||||
for (const shape of editor.getCurrentPageShapes()) {
|
for (const shape of editor.getCurrentPageShapes()) {
|
||||||
if (!editor.isShapeOfType<TLNoteShape>(shape, 'note') || selectedShapeIds.has(shape.id)) {
|
if (
|
||||||
|
!editor.isShapeOfType<TLNoteShape>(shape, 'note') ||
|
||||||
|
scale !== shape.props.scale ||
|
||||||
|
selectedShapeIds.has(shape.id)
|
||||||
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +102,7 @@ export function getAvailableNoteAdjacentPositions(
|
||||||
// And push its position to the positions array
|
// And push its position to the positions array
|
||||||
positions.push(
|
positions.push(
|
||||||
...Object.values(
|
...Object.values(
|
||||||
getNoteAdjacentPositions(transform.point(), rotation, shape.props.growY, extraHeight)
|
getNoteAdjacentPositions(transform.point(), rotation, shape.props.growY, extraHeight, scale)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -133,7 +151,7 @@ export function getNoteShapeForAdjacentPosition(
|
||||||
// Start from the top of the stack, and work our way down
|
// Start from the top of the stack, and work our way down
|
||||||
const allShapesOnPage = editor.getCurrentPageShapesSorted()
|
const allShapesOnPage = editor.getCurrentPageShapesSorted()
|
||||||
|
|
||||||
const minDistance = NOTE_SIZE + ADJACENT_NOTE_MARGIN ** 2
|
const minDistance = (NOTE_SIZE + ADJACENT_NOTE_MARGIN ** 2) ** shape.props.scale
|
||||||
|
|
||||||
for (let i = allShapesOnPage.length - 1; i >= 0; i--) {
|
for (let i = allShapesOnPage.length - 1; i >= 0; i--) {
|
||||||
const otherNote = allShapesOnPage[i]
|
const otherNote = allShapesOnPage[i]
|
||||||
|
@ -158,7 +176,7 @@ export function getNoteShapeForAdjacentPosition(
|
||||||
const id = createShapeId()
|
const id = createShapeId()
|
||||||
|
|
||||||
// We create it at the center first, so that it becomes
|
// We create it at the center first, so that it becomes
|
||||||
// the child of whatever parent was at that center
|
// the child of whatever parent was at that center
|
||||||
editor.createShape({
|
editor.createShape({
|
||||||
id,
|
id,
|
||||||
type: 'note',
|
type: 'note',
|
||||||
|
@ -179,13 +197,16 @@ export function getNoteShapeForAdjacentPosition(
|
||||||
|
|
||||||
// Now we need to correct its location within its new parent
|
// Now we need to correct its location within its new parent
|
||||||
|
|
||||||
const createdShape = editor.getShape(id)!
|
const createdShape = editor.getShape<TLNoteShape>(id)!
|
||||||
|
if (!createdShape) return // may have hit max shapes
|
||||||
|
|
||||||
// We need to put the page point in the same coordinate
|
// We need to put the page point in the same coordinate space as the newly created shape (i.e its parent's space)
|
||||||
// space as the newly created shape (i.e its parent's space)
|
|
||||||
const topLeft = editor.getPointInParentSpace(
|
const topLeft = editor.getPointInParentSpace(
|
||||||
createdShape,
|
createdShape,
|
||||||
Vec.Sub(center, Vec.Rot(NOTE_CENTER_OFFSET, pageRotation))
|
Vec.Sub(
|
||||||
|
center,
|
||||||
|
Vec.Rot(NOTE_CENTER_OFFSET.clone().mul(createdShape.props.scale), pageRotation)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
editor.updateShape({
|
editor.updateShape({
|
||||||
|
|
|
@ -9,7 +9,10 @@ import {
|
||||||
Vec,
|
Vec,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { NOTE_PIT_RADIUS, getAvailableNoteAdjacentPositions } from '../noteHelpers'
|
import {
|
||||||
|
NOTE_ADJACENT_POSITION_SNAP_RADIUS,
|
||||||
|
getAvailableNoteAdjacentPositions,
|
||||||
|
} from '../noteHelpers'
|
||||||
|
|
||||||
export class Pointing extends StateNode {
|
export class Pointing extends StateNode {
|
||||||
static override id = 'pointing'
|
static override id = 'pointing'
|
||||||
|
@ -36,11 +39,15 @@ export class Pointing extends StateNode {
|
||||||
|
|
||||||
// Check for note pits; if the pointer is close to one, place the note centered on the pit
|
// Check for note pits; if the pointer is close to one, place the note centered on the pit
|
||||||
const center = this.editor.inputs.originPagePoint.clone()
|
const center = this.editor.inputs.originPagePoint.clone()
|
||||||
const offset = getNotePitOffset(this.editor, center)
|
const offset = getNoteShapeAdjacentPositionOffset(
|
||||||
|
this.editor,
|
||||||
|
center,
|
||||||
|
this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1
|
||||||
|
)
|
||||||
if (offset) {
|
if (offset) {
|
||||||
center.sub(offset)
|
center.sub(offset)
|
||||||
}
|
}
|
||||||
this.shape = createSticky(this.editor, id, center)
|
this.shape = createNoteShape(this.editor, id, center)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,11 +56,15 @@ export class Pointing extends StateNode {
|
||||||
if (!this.wasFocusedOnEnter) {
|
if (!this.wasFocusedOnEnter) {
|
||||||
const id = createShapeId()
|
const id = createShapeId()
|
||||||
const center = this.editor.inputs.originPagePoint.clone()
|
const center = this.editor.inputs.originPagePoint.clone()
|
||||||
const offset = getNotePitOffset(this.editor, center)
|
const offset = getNoteShapeAdjacentPositionOffset(
|
||||||
|
this.editor,
|
||||||
|
center,
|
||||||
|
this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1
|
||||||
|
)
|
||||||
if (offset) {
|
if (offset) {
|
||||||
center.sub(offset)
|
center.sub(offset)
|
||||||
}
|
}
|
||||||
this.shape = createSticky(this.editor, id, center)
|
this.shape = createNoteShape(this.editor, id, center)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editor.setCurrentTool('select.translating', {
|
this.editor.setCurrentTool('select.translating', {
|
||||||
|
@ -107,10 +118,10 @@ export class Pointing extends StateNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNotePitOffset(editor: Editor, center: Vec) {
|
export function getNoteShapeAdjacentPositionOffset(editor: Editor, center: Vec, scale: number) {
|
||||||
let min = NOTE_PIT_RADIUS / editor.getZoomLevel() // in screen space
|
let min = NOTE_ADJACENT_POSITION_SNAP_RADIUS / editor.getZoomLevel() // in screen space
|
||||||
let offset: Vec | undefined
|
let offset: Vec | undefined
|
||||||
for (const pit of getAvailableNoteAdjacentPositions(editor, 0, 0)) {
|
for (const pit of getAvailableNoteAdjacentPositions(editor, 0, scale, 0)) {
|
||||||
// only check page rotations of zero
|
// only check page rotations of zero
|
||||||
const deltaToPit = Vec.Sub(center, pit)
|
const deltaToPit = Vec.Sub(center, pit)
|
||||||
const dist = deltaToPit.len()
|
const dist = deltaToPit.len()
|
||||||
|
@ -122,13 +133,16 @@ export function getNotePitOffset(editor: Editor, center: Vec) {
|
||||||
return offset
|
return offset
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSticky(editor: Editor, id: TLShapeId, center: Vec) {
|
export function createNoteShape(editor: Editor, id: TLShapeId, center: Vec) {
|
||||||
editor
|
editor
|
||||||
.createShape({
|
.createShape({
|
||||||
id,
|
id,
|
||||||
type: 'note',
|
type: 'note',
|
||||||
x: center.x,
|
x: center.x,
|
||||||
y: center.y,
|
y: center.y,
|
||||||
|
props: {
|
||||||
|
scale: editor.user.getIsDynamicResizeMode() ? 1 / editor.getZoomLevel() : 1,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.select(id)
|
.select(id)
|
||||||
|
|
||||||
|
|
|
@ -2,28 +2,28 @@ import {
|
||||||
TLDefaultColorStyle,
|
TLDefaultColorStyle,
|
||||||
TLDefaultColorTheme,
|
TLDefaultColorTheme,
|
||||||
TLDefaultFillStyle,
|
TLDefaultFillStyle,
|
||||||
getDefaultColorTheme,
|
|
||||||
useEditor,
|
useEditor,
|
||||||
useIsDarkMode,
|
|
||||||
useSvgExportContext,
|
useSvgExportContext,
|
||||||
useValue,
|
useValue,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { getHashPatternZoomName } from './defaultStyleDefs'
|
import { getHashPatternZoomName } from './defaultStyleDefs'
|
||||||
|
|
||||||
export interface ShapeFillProps {
|
interface ShapeFillProps {
|
||||||
d: string
|
d: string
|
||||||
fill: TLDefaultFillStyle
|
fill: TLDefaultFillStyle
|
||||||
color: TLDefaultColorStyle
|
color: TLDefaultColorStyle
|
||||||
theme: TLDefaultColorTheme
|
theme: TLDefaultColorTheme
|
||||||
|
scale: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
export const ShapeFill = React.memo(function ShapeFill({
|
||||||
export function useDefaultColorTheme() {
|
theme,
|
||||||
return getDefaultColorTheme({ isDarkMode: useIsDarkMode() })
|
d,
|
||||||
}
|
color,
|
||||||
|
fill,
|
||||||
export const ShapeFill = React.memo(function ShapeFill({ theme, d, color, fill }: ShapeFillProps) {
|
scale,
|
||||||
|
}: ShapeFillProps) {
|
||||||
switch (fill) {
|
switch (fill) {
|
||||||
case 'none': {
|
case 'none': {
|
||||||
return null
|
return null
|
||||||
|
@ -34,8 +34,11 @@ export const ShapeFill = React.memo(function ShapeFill({ theme, d, color, fill }
|
||||||
case 'semi': {
|
case 'semi': {
|
||||||
return <path fill={theme.solid} d={d} />
|
return <path fill={theme.solid} d={d} />
|
||||||
}
|
}
|
||||||
|
case 'fill': {
|
||||||
|
return <path fill={theme[color].fill} d={d} />
|
||||||
|
}
|
||||||
case 'pattern': {
|
case 'pattern': {
|
||||||
return <PatternFill theme={theme} color={color} fill={fill} d={d} />
|
return <PatternFill theme={theme} color={color} fill={fill} d={d} scale={scale} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,10 +6,10 @@ import {
|
||||||
TLDefaultVerticalAlignStyle,
|
TLDefaultVerticalAlignStyle,
|
||||||
useEditor,
|
useEditor,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { useDefaultColorTheme } from './ShapeFill'
|
|
||||||
import { createTextJsxFromSpans } from './createTextJsxFromSpans'
|
import { createTextJsxFromSpans } from './createTextJsxFromSpans'
|
||||||
import { TEXT_PROPS } from './default-shape-constants'
|
import { TEXT_PROPS } from './default-shape-constants'
|
||||||
import { getLegacyOffsetX } from './legacyProps'
|
import { getLegacyOffsetX } from './legacyProps'
|
||||||
|
import { useDefaultColorTheme } from './useDefaultColorTheme'
|
||||||
|
|
||||||
export function SvgTextLabel({
|
export function SvgTextLabel({
|
||||||
fontSize,
|
fontSize,
|
||||||
|
|
|
@ -33,6 +33,7 @@ export interface TextLabelProps {
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
textWidth?: number
|
textWidth?: number
|
||||||
textHeight?: number
|
textHeight?: number
|
||||||
|
padding?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public @react */
|
/** @public @react */
|
||||||
|
@ -48,6 +49,7 @@ export const TextLabel = React.memo(function TextLabel({
|
||||||
verticalAlign,
|
verticalAlign,
|
||||||
wrap,
|
wrap,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
padding = 0,
|
||||||
onKeyDown: handleKeyDownCustom,
|
onKeyDown: handleKeyDownCustom,
|
||||||
classNamePrefix,
|
classNamePrefix,
|
||||||
style,
|
style,
|
||||||
|
@ -90,6 +92,7 @@ export const TextLabel = React.memo(function TextLabel({
|
||||||
style={{
|
style={{
|
||||||
justifyContent: align === 'middle' || legacyAlign ? 'center' : align,
|
justifyContent: align === 'middle' || legacyAlign ? 'center' : align,
|
||||||
alignItems: verticalAlign === 'middle' ? 'center' : verticalAlign,
|
alignItems: verticalAlign === 'middle' ? 'center' : verticalAlign,
|
||||||
|
padding,
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -98,7 +101,7 @@ export const TextLabel = React.memo(function TextLabel({
|
||||||
style={{
|
style={{
|
||||||
fontSize,
|
fontSize,
|
||||||
lineHeight: Math.floor(fontSize * lineHeight) + 'px',
|
lineHeight: Math.floor(fontSize * lineHeight) + 'px',
|
||||||
minHeight: lineHeight + 32,
|
minHeight: Math.floor(fontSize * lineHeight) + 'px',
|
||||||
minWidth: Math.ceil(textWidth || 0),
|
minWidth: Math.ceil(textWidth || 0),
|
||||||
color: labelColor,
|
color: labelColor,
|
||||||
width: textWidth ? Math.ceil(textWidth) : undefined,
|
width: textWidth ? Math.ceil(textWidth) : undefined,
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
useValue,
|
useValue,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useDefaultColorTheme } from './ShapeFill'
|
import { useDefaultColorTheme } from './useDefaultColorTheme'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef {
|
export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef {
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { getDefaultColorTheme, useIsDarkMode } from '@tldraw/editor'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export function useDefaultColorTheme() {
|
||||||
|
return getDefaultColorTheme({ isDarkMode: useIsDarkMode() })
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { DefaultTextAlignStyle } from '@tldraw/editor'
|
||||||
import { TestEditor } from '../../../test/TestEditor'
|
import { TestEditor } from '../../../test/TestEditor'
|
||||||
import { TextShapeTool } from './TextShapeTool'
|
import { TextShapeTool } from './TextShapeTool'
|
||||||
|
|
||||||
|
@ -111,16 +112,48 @@ describe('When in the pointing state', () => {
|
||||||
|
|
||||||
it('on pointer up, preserves the center when the text has a auto width', () => {
|
it('on pointer up, preserves the center when the text has a auto width', () => {
|
||||||
editor.setCurrentTool('text')
|
editor.setCurrentTool('text')
|
||||||
|
editor.setStyleForNextShapes(DefaultTextAlignStyle, 'middle')
|
||||||
const x = 0
|
const x = 0
|
||||||
const y = 0
|
const y = 0
|
||||||
editor.pointerDown(x, y)
|
editor.pointerDown(x, y)
|
||||||
editor.pointerUp()
|
editor.pointerUp()
|
||||||
const bounds = editor.getShapePageBounds(editor.getCurrentPageShapes()[0])!
|
const shape = editor.getLastCreatedShape()
|
||||||
expect(editor.getCurrentPageShapes()[0]).toMatchObject({
|
const bounds = editor.getShapePageBounds(shape)!
|
||||||
|
expect(shape).toMatchObject({
|
||||||
x: x - bounds.width / 2,
|
x: x - bounds.width / 2,
|
||||||
y: y - bounds.height / 2,
|
y: y - bounds.height / 2,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('on pointer up, preserves the center when the text has a auto width (left aligned)', () => {
|
||||||
|
editor.setCurrentTool('text')
|
||||||
|
editor.setStyleForNextShapes(DefaultTextAlignStyle, 'start')
|
||||||
|
const x = 0
|
||||||
|
const y = 0
|
||||||
|
editor.pointerDown(x, y)
|
||||||
|
editor.pointerUp()
|
||||||
|
const shape = editor.getLastCreatedShape()
|
||||||
|
const bounds = editor.getShapePageBounds(shape)!
|
||||||
|
expect(shape).toMatchObject({
|
||||||
|
x,
|
||||||
|
y: y - bounds.height / 2,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('on pointer up, preserves the center when the text has a auto width (right aligned)', () => {
|
||||||
|
editor.setCurrentTool('text')
|
||||||
|
editor.setStyleForNextShapes(DefaultTextAlignStyle, 'end')
|
||||||
|
const x = 0
|
||||||
|
const y = 0
|
||||||
|
editor.pointerDown(x, y)
|
||||||
|
editor.pointerUp()
|
||||||
|
const shape = editor.getLastCreatedShape()
|
||||||
|
const bounds = editor.getShapePageBounds(shape)!
|
||||||
|
expect(shape).toMatchObject({
|
||||||
|
x: x - bounds.width,
|
||||||
|
y: y - bounds.height / 2,
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When resizing', () => {
|
describe('When resizing', () => {
|
||||||
|
@ -151,7 +184,7 @@ describe('When resizing', () => {
|
||||||
editor.pointerMove(x + 100, y + 100)
|
editor.pointerMove(x + 100, y + 100)
|
||||||
expect(editor.getCurrentPageShapes()[0]).toMatchObject({
|
expect(editor.getCurrentPageShapes()[0]).toMatchObject({
|
||||||
x,
|
x,
|
||||||
y,
|
y: -12, // 24 is the height of the text, and it's centered at that point
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,13 +20,13 @@ import {
|
||||||
useEditor,
|
useEditor,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
|
||||||
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
||||||
import { TextHelpers } from '../shared/TextHelpers'
|
import { TextHelpers } from '../shared/TextHelpers'
|
||||||
import { TextLabel } from '../shared/TextLabel'
|
import { TextLabel } from '../shared/TextLabel'
|
||||||
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
|
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
|
||||||
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
||||||
import { resizeScaled } from '../shared/resizeScaled'
|
import { resizeScaled } from '../shared/resizeScaled'
|
||||||
|
import { useDefaultColorTheme } from '../shared/useDefaultColorTheme'
|
||||||
|
|
||||||
const sizeCache = new WeakCache<TLTextShape['props'], { height: number; width: number }>()
|
const sizeCache = new WeakCache<TLTextShape['props'], { height: number; width: number }>()
|
||||||
|
|
||||||
|
@ -162,24 +162,6 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override onBeforeCreate = (shape: TLTextShape) => {
|
|
||||||
// When a shape is created, center the text at the created point.
|
|
||||||
|
|
||||||
// Only center if the shape is set to autosize.
|
|
||||||
if (!shape.props.autoSize) return
|
|
||||||
|
|
||||||
// Only center if the shape is empty when created.
|
|
||||||
if (shape.props.text.trim()) return
|
|
||||||
|
|
||||||
const bounds = this.getMinDimensions(shape)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...shape,
|
|
||||||
x: shape.x - bounds.width / 2,
|
|
||||||
y: shape.y - bounds.height / 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override onEditEnd: TLOnEditEndHandler<TLTextShape> = (shape) => {
|
override onEditEnd: TLOnEditEndHandler<TLTextShape> = (shape) => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
|
@ -267,29 +249,31 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override onDoubleClickEdge = (shape: TLTextShape) => {
|
// todo: The edge doubleclicking feels like a mistake more often than
|
||||||
// If the shape has a fixed width, set it to autoSize.
|
// not, especially on multiline text. Removed June 16 2024
|
||||||
if (!shape.props.autoSize) {
|
|
||||||
return {
|
|
||||||
id: shape.id,
|
|
||||||
type: shape.type,
|
|
||||||
props: {
|
|
||||||
autoSize: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the shape is scaled, reset the scale to 1.
|
// override onDoubleClickEdge = (shape: TLTextShape) => {
|
||||||
if (shape.props.scale !== 1) {
|
// // If the shape has a fixed width, set it to autoSize.
|
||||||
return {
|
// if (!shape.props.autoSize) {
|
||||||
id: shape.id,
|
// return {
|
||||||
type: shape.type,
|
// id: shape.id,
|
||||||
props: {
|
// type: shape.type,
|
||||||
scale: 1,
|
// props: {
|
||||||
},
|
// autoSize: true,
|
||||||
}
|
// },
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
// // If the shape is scaled, reset the scale to 1.
|
||||||
|
// if (shape.props.scale !== 1) {
|
||||||
|
// return {
|
||||||
|
// id: shape.id,
|
||||||
|
// type: shape.type,
|
||||||
|
// props: {
|
||||||
|
// scale: 1,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTextSize(editor: Editor, props: TLTextShape['props']) {
|
function getTextSize(editor: Editor, props: TLTextShape['props']) {
|
||||||
|
@ -310,9 +294,9 @@ function getTextSize(editor: Editor, props: TLTextShape['props']) {
|
||||||
maxWidth: cw,
|
maxWidth: cw,
|
||||||
})
|
})
|
||||||
|
|
||||||
// // If we're autosizing the measureText will essentially `Math.floor`
|
// If we're autosizing the measureText will essentially `Math.floor`
|
||||||
// // the numbers so `19` rather than `19.3`, this means we must +1 to
|
// the numbers so `19` rather than `19.3`, this means we must +1 to
|
||||||
// // whatever we get to avoid wrapping.
|
// whatever we get to avoid wrapping.
|
||||||
if (autoSize) {
|
if (autoSize) {
|
||||||
result.w += 1
|
result.w += 1
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import { StateNode, TLEventHandlers, TLTextShape, createShapeId } from '@tldraw/editor'
|
import {
|
||||||
|
StateNode,
|
||||||
|
TLEventHandlers,
|
||||||
|
TLShapeId,
|
||||||
|
TLTextShape,
|
||||||
|
Vec,
|
||||||
|
createShapeId,
|
||||||
|
isShapeId,
|
||||||
|
} from '@tldraw/editor'
|
||||||
|
|
||||||
export class Pointing extends StateNode {
|
export class Pointing extends StateNode {
|
||||||
static override id = 'pointing'
|
static override id = 'pointing'
|
||||||
|
@ -22,27 +30,17 @@ export class Pointing extends StateNode {
|
||||||
this.markId = `creating:${id}`
|
this.markId = `creating:${id}`
|
||||||
this.editor.mark(this.markId)
|
this.editor.mark(this.markId)
|
||||||
|
|
||||||
this.editor.createShapes<TLTextShape>([
|
const shape = this.createTextShape(id, originPagePoint, false)
|
||||||
{
|
if (!shape) {
|
||||||
id,
|
this.cancel()
|
||||||
type: 'text',
|
return
|
||||||
x: originPagePoint.x,
|
}
|
||||||
y: originPagePoint.y,
|
|
||||||
props: {
|
// Now save the fresh reference
|
||||||
text: '',
|
this.shape = this.editor.getShape(shape)
|
||||||
autoSize: false,
|
|
||||||
w: 20,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
this.editor.select(id)
|
this.editor.select(id)
|
||||||
|
|
||||||
this.shape = this.editor.getShape(id)
|
|
||||||
if (!this.shape) return
|
|
||||||
|
|
||||||
const { shape } = this
|
|
||||||
|
|
||||||
this.editor.setCurrentTool('select.resizing', {
|
this.editor.setCurrentTool('select.resizing', {
|
||||||
...info,
|
...info,
|
||||||
target: 'selection',
|
target: 'selection',
|
||||||
|
@ -77,22 +75,11 @@ export class Pointing extends StateNode {
|
||||||
private complete() {
|
private complete() {
|
||||||
this.editor.mark('creating text shape')
|
this.editor.mark('creating text shape')
|
||||||
const id = createShapeId()
|
const id = createShapeId()
|
||||||
const { x, y } = this.editor.inputs.currentPagePoint
|
const { currentPagePoint } = this.editor.inputs
|
||||||
this.editor
|
const shape = this.createTextShape(id, currentPagePoint, true)
|
||||||
.createShapes([
|
if (!shape) return
|
||||||
{
|
|
||||||
id,
|
|
||||||
type: 'text',
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
props: {
|
|
||||||
text: '',
|
|
||||||
autoSize: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.select(id)
|
|
||||||
|
|
||||||
|
this.editor.select(id)
|
||||||
this.editor.setEditingShape(id)
|
this.editor.setEditingShape(id)
|
||||||
this.editor.setCurrentTool('select')
|
this.editor.setCurrentTool('select')
|
||||||
this.editor.root.getCurrent()?.transition('editing_shape')
|
this.editor.root.getCurrent()?.transition('editing_shape')
|
||||||
|
@ -102,4 +89,63 @@ export class Pointing extends StateNode {
|
||||||
this.parent.transition('idle')
|
this.parent.transition('idle')
|
||||||
this.editor.bailToMark(this.markId)
|
this.editor.bailToMark(this.markId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createTextShape(id: TLShapeId, point: Vec, autoSize: boolean) {
|
||||||
|
this.editor.createShape<TLTextShape>({
|
||||||
|
id,
|
||||||
|
type: 'text',
|
||||||
|
x: point.x,
|
||||||
|
y: point.y,
|
||||||
|
props: {
|
||||||
|
text: '',
|
||||||
|
autoSize,
|
||||||
|
w: 20,
|
||||||
|
scale: this.editor.user.getIsDynamicResizeMode() ? 1 / this.editor.getZoomLevel() : 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const shape = this.editor.getShape<TLTextShape>(id)
|
||||||
|
if (!shape) {
|
||||||
|
this.cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const bounds = this.editor.getShapePageBounds(shape)!
|
||||||
|
|
||||||
|
const delta = new Vec()
|
||||||
|
|
||||||
|
if (autoSize) {
|
||||||
|
switch (shape.props.textAlign) {
|
||||||
|
case 'start': {
|
||||||
|
delta.x = 0
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'middle': {
|
||||||
|
delta.x = -bounds.width / 2
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'end': {
|
||||||
|
delta.x = -bounds.width
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delta.x = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
delta.y = -bounds.height / 2
|
||||||
|
|
||||||
|
if (isShapeId(shape.parentId)) {
|
||||||
|
const transform = this.editor.getShapeParentTransform(shape)
|
||||||
|
delta.rot(-transform.rotation())
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor.updateShape({
|
||||||
|
...shape,
|
||||||
|
x: shape.x + delta.x,
|
||||||
|
y: shape.y + delta.y,
|
||||||
|
})
|
||||||
|
|
||||||
|
return shape
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ export const STYLES = {
|
||||||
{ value: 'semi', icon: 'fill-semi' },
|
{ value: 'semi', icon: 'fill-semi' },
|
||||||
{ value: 'solid', icon: 'fill-solid' },
|
{ value: 'solid', icon: 'fill-solid' },
|
||||||
{ value: 'pattern', icon: 'fill-pattern' },
|
{ value: 'pattern', icon: 'fill-pattern' },
|
||||||
|
// { value: 'fill', icon: 'fill-fill' },
|
||||||
],
|
],
|
||||||
dash: [
|
dash: [
|
||||||
{ value: 'draw', icon: 'dash-draw' },
|
{ value: 'draw', icon: 'dash-draw' },
|
||||||
|
|
|
@ -78,7 +78,7 @@ export class PointingHandle extends StateNode {
|
||||||
// Center the shape on the current pointer
|
// Center the shape on the current pointer
|
||||||
const centeredOnPointer = editor
|
const centeredOnPointer = editor
|
||||||
.getPointInParentSpace(nextNote, editor.inputs.originPagePoint)
|
.getPointInParentSpace(nextNote, editor.inputs.originPagePoint)
|
||||||
.sub(Vec.Rot(NOTE_CENTER_OFFSET, nextNote.rotation))
|
.sub(Vec.Rot(NOTE_CENTER_OFFSET.clone().mul(shape.props.scale), nextNote.rotation))
|
||||||
editor.updateShape({ ...nextNote, x: centeredOnPointer.x, y: centeredOnPointer.y })
|
editor.updateShape({ ...nextNote, x: centeredOnPointer.x, y: centeredOnPointer.y })
|
||||||
|
|
||||||
// Then select and begin translating the shape
|
// Then select and begin translating the shape
|
||||||
|
@ -124,7 +124,13 @@ function getNoteForPit(editor: Editor, shape: TLNoteShape, handle: TLHandle, for
|
||||||
const pageTransform = editor.getShapePageTransform(shape.id)!
|
const pageTransform = editor.getShapePageTransform(shape.id)!
|
||||||
const pagePoint = pageTransform.point()
|
const pagePoint = pageTransform.point()
|
||||||
const pageRotation = pageTransform.rotation()
|
const pageRotation = pageTransform.rotation()
|
||||||
const pits = getNoteAdjacentPositions(pagePoint, pageRotation, shape.props.growY, 0)
|
const pits = getNoteAdjacentPositions(
|
||||||
|
pagePoint,
|
||||||
|
pageRotation,
|
||||||
|
shape.props.growY,
|
||||||
|
0,
|
||||||
|
shape.props.scale
|
||||||
|
)
|
||||||
const pit = pits[handle.index]
|
const pit = pits[handle.index]
|
||||||
if (pit) {
|
if (pit) {
|
||||||
return getNoteShapeForAdjacentPosition(editor, shape, pit, pageRotation, forceNew)
|
return getNoteShapeForAdjacentPosition(editor, shape, pit, pageRotation, forceNew)
|
||||||
|
|
|
@ -16,8 +16,8 @@ import {
|
||||||
moveCameraWhenCloseToEdge,
|
moveCameraWhenCloseToEdge,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
import {
|
import {
|
||||||
NOTE_PIT_RADIUS,
|
NOTE_ADJACENT_POSITION_SNAP_RADIUS,
|
||||||
NOTE_SIZE,
|
NOTE_CENTER_OFFSET,
|
||||||
getAvailableNoteAdjacentPositions,
|
getAvailableNoteAdjacentPositions,
|
||||||
} from '../../../shapes/note/noteHelpers'
|
} from '../../../shapes/note/noteHelpers'
|
||||||
import { DragAndDropManager } from '../DragAndDropManager'
|
import { DragAndDropManager } from '../DragAndDropManager'
|
||||||
|
@ -353,7 +353,7 @@ function getTranslatingSnapshot(editor: Editor) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let noteAdjacentPositions: Vec[] | undefined
|
let noteAdjacentPositions: Vec[] | undefined
|
||||||
let noteSnapshot: MovingShapeSnapshot | undefined
|
let noteSnapshot: (MovingShapeSnapshot & { shape: TLNoteShape }) | undefined
|
||||||
|
|
||||||
const { originPagePoint } = editor.inputs
|
const { originPagePoint } = editor.inputs
|
||||||
|
|
||||||
|
@ -361,7 +361,7 @@ function getTranslatingSnapshot(editor: Editor) {
|
||||||
(s) =>
|
(s) =>
|
||||||
editor.isShapeOfType<TLNoteShape>(s.shape, 'note') &&
|
editor.isShapeOfType<TLNoteShape>(s.shape, 'note') &&
|
||||||
editor.isPointInShape(s.shape, originPagePoint)
|
editor.isPointInShape(s.shape, originPagePoint)
|
||||||
)
|
) as (MovingShapeSnapshot & { shape: TLNoteShape })[]
|
||||||
|
|
||||||
if (allHoveredNotes.length === 0) {
|
if (allHoveredNotes.length === 0) {
|
||||||
// noop
|
// noop
|
||||||
|
@ -383,7 +383,8 @@ function getTranslatingSnapshot(editor: Editor) {
|
||||||
noteAdjacentPositions = getAvailableNoteAdjacentPositions(
|
noteAdjacentPositions = getAvailableNoteAdjacentPositions(
|
||||||
editor,
|
editor,
|
||||||
noteSnapshot.pageRotation,
|
noteSnapshot.pageRotation,
|
||||||
(noteSnapshot.shape as TLNoteShape).props.growY ?? 0
|
noteSnapshot.shape.props.scale,
|
||||||
|
noteSnapshot.shape.props.growY ?? 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,14 +462,16 @@ export function moveShapesToPoint({
|
||||||
} else {
|
} else {
|
||||||
// for sticky notes, snap to grid position next to other notes
|
// for sticky notes, snap to grid position next to other notes
|
||||||
if (noteSnapshot && noteAdjacentPositions) {
|
if (noteSnapshot && noteAdjacentPositions) {
|
||||||
let min = NOTE_PIT_RADIUS / editor.getZoomLevel() // in screen space
|
const { scale } = noteSnapshot.shape.props
|
||||||
|
const pageCenter = noteSnapshot.pagePoint
|
||||||
|
.clone()
|
||||||
|
.add(delta)
|
||||||
|
// use the middle of the note, disregarding extra height
|
||||||
|
.add(NOTE_CENTER_OFFSET.clone().mul(scale).rot(noteSnapshot.pageRotation))
|
||||||
|
|
||||||
|
// Find the pit with the center closest to the put center
|
||||||
|
let min = NOTE_ADJACENT_POSITION_SNAP_RADIUS / editor.getZoomLevel() // in screen space
|
||||||
let offset = new Vec(0, 0)
|
let offset = new Vec(0, 0)
|
||||||
|
|
||||||
const pageCenter = Vec.Add(
|
|
||||||
Vec.Add(noteSnapshot.pagePoint, delta),
|
|
||||||
new Vec(NOTE_SIZE / 2, NOTE_SIZE / 2).rot(noteSnapshot.pageRotation)
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const pit of noteAdjacentPositions) {
|
for (const pit of noteAdjacentPositions) {
|
||||||
// We've already filtered pits with the same page rotation
|
// We've already filtered pits with the same page rotation
|
||||||
const deltaToPit = Vec.Sub(pageCenter, pit)
|
const deltaToPit = Vec.Sub(pageCenter, pit)
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
ToggleAutoSizeMenuItem,
|
ToggleAutoSizeMenuItem,
|
||||||
ToggleDarkModeItem,
|
ToggleDarkModeItem,
|
||||||
ToggleDebugModeItem,
|
ToggleDebugModeItem,
|
||||||
|
ToggleDynamicSizeModeItem,
|
||||||
ToggleEdgeScrollingItem,
|
ToggleEdgeScrollingItem,
|
||||||
ToggleFocusModeItem,
|
ToggleFocusModeItem,
|
||||||
ToggleGridItem,
|
ToggleGridItem,
|
||||||
|
@ -173,6 +174,7 @@ export function PreferencesGroup() {
|
||||||
<ToggleFocusModeItem />
|
<ToggleFocusModeItem />
|
||||||
<ToggleEdgeScrollingItem />
|
<ToggleEdgeScrollingItem />
|
||||||
<ToggleReduceMotionItem />
|
<ToggleReduceMotionItem />
|
||||||
|
<ToggleDynamicSizeModeItem />
|
||||||
<ToggleDebugModeItem />
|
<ToggleDebugModeItem />
|
||||||
</TldrawUiMenuGroup>
|
</TldrawUiMenuGroup>
|
||||||
<TldrawUiMenuGroup id="language">
|
<TldrawUiMenuGroup id="language">
|
||||||
|
|
|
@ -607,6 +607,23 @@ export function ToggleDebugModeItem() {
|
||||||
return <TldrawUiMenuCheckboxItem {...actions['toggle-debug-mode']} checked={isDebugMode} />
|
return <TldrawUiMenuCheckboxItem {...actions['toggle-debug-mode']} checked={isDebugMode} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @public @react */
|
||||||
|
export function ToggleDynamicSizeModeItem() {
|
||||||
|
const actions = useActions()
|
||||||
|
const editor = useEditor()
|
||||||
|
const isDynamicResizeMode = useValue(
|
||||||
|
'dynamic resize',
|
||||||
|
() => editor.user.getIsDynamicResizeMode(),
|
||||||
|
[editor]
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<TldrawUiMenuCheckboxItem
|
||||||
|
{...actions['toggle-dynamic-size-mode']}
|
||||||
|
checked={isDynamicResizeMode}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------------- Print --------------------- */
|
/* ---------------------- Print --------------------- */
|
||||||
/** @public @react */
|
/** @public @react */
|
||||||
export function PrintItem() {
|
export function PrintItem() {
|
||||||
|
|
|
@ -1130,6 +1130,21 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
},
|
},
|
||||||
checkbox: true,
|
checkbox: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'toggle-dynamic-size-mode',
|
||||||
|
label: {
|
||||||
|
default: 'action.toggle-dynamic-size-mode',
|
||||||
|
menu: 'action.toggle-dynamic-size-mode.menu',
|
||||||
|
},
|
||||||
|
readonlyOk: false,
|
||||||
|
onSelect(source) {
|
||||||
|
trackEvent('toggle-dynamic-size-mode', { source })
|
||||||
|
editor.user.updateUserPreferences({
|
||||||
|
isDynamicSizeMode: !editor.user.getIsDynamicResizeMode(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
checkbox: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'toggle-reduce-motion',
|
id: 'toggle-reduce-motion',
|
||||||
label: {
|
label: {
|
||||||
|
|
|
@ -88,6 +88,7 @@ export interface TLUiEventMap {
|
||||||
'toggle-wrap-mode': null
|
'toggle-wrap-mode': null
|
||||||
'toggle-focus-mode': null
|
'toggle-focus-mode': null
|
||||||
'toggle-debug-mode': null
|
'toggle-debug-mode': null
|
||||||
|
'toggle-dynamic-size-mode': null
|
||||||
'toggle-lock': null
|
'toggle-lock': null
|
||||||
'toggle-reduce-motion': null
|
'toggle-reduce-motion': null
|
||||||
'toggle-edge-scrolling': null
|
'toggle-edge-scrolling': null
|
||||||
|
|
|
@ -67,6 +67,7 @@ exports[`pasteExcalidrawContent test fixtures bound-arrows.json 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 129.7109375,
|
"h": 129.7109375,
|
||||||
"labelColor": "black",
|
"labelColor": "black",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -100,6 +101,7 @@ exports[`pasteExcalidrawContent test fixtures bound-arrows.json 1`] = `
|
||||||
"font": "draw",
|
"font": "draw",
|
||||||
"labelColor": "black",
|
"labelColor": "black",
|
||||||
"labelPosition": 0.5,
|
"labelPosition": 0.5,
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"start": {
|
"start": {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
|
@ -130,6 +132,7 @@ exports[`pasteExcalidrawContent test fixtures bound-arrows.json 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 116.80078125,
|
"h": 116.80078125,
|
||||||
"labelColor": "black",
|
"labelColor": "black",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -203,6 +206,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 1.1747704163190065,
|
"y": 1.1747704163190065,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -248,6 +252,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": -94.83601179012066,
|
"y": -94.83601179012066,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -311,6 +316,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 44.74807313069914,
|
"y": 44.74807313069914,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -356,6 +362,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 6.728230566191087,
|
"y": 6.728230566191087,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -395,6 +402,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 1.81555427976582,
|
"y": 1.81555427976582,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "line",
|
"spline": "line",
|
||||||
},
|
},
|
||||||
|
@ -428,6 +436,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": -40.796572639443866,
|
"y": -40.796572639443866,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "line",
|
"spline": "line",
|
||||||
},
|
},
|
||||||
|
@ -461,6 +470,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 37.69945063278442,
|
"y": 37.69945063278442,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "line",
|
"spline": "line",
|
||||||
},
|
},
|
||||||
|
@ -487,6 +497,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 10.466136436297347,
|
"h": 10.466136436297347,
|
||||||
"labelColor": "black",
|
"labelColor": "black",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -541,6 +552,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 53.02035135709548,
|
"y": 53.02035135709548,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "line",
|
"spline": "line",
|
||||||
},
|
},
|
||||||
|
@ -586,6 +598,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 12.388488026637333,
|
"y": 12.388488026637333,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -637,6 +650,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": -6.401707036148309,
|
"y": -6.401707036148309,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -660,6 +674,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"isClosed": false,
|
"isClosed": false,
|
||||||
"isComplete": false,
|
"isComplete": false,
|
||||||
"isPen": false,
|
"isPen": false,
|
||||||
|
"scale": 1,
|
||||||
"segments": [
|
"segments": [
|
||||||
{
|
{
|
||||||
"points": [
|
"points": [
|
||||||
|
@ -699,6 +714,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"isClosed": false,
|
"isClosed": false,
|
||||||
"isComplete": false,
|
"isComplete": false,
|
||||||
"isPen": false,
|
"isPen": false,
|
||||||
|
"scale": 1,
|
||||||
"segments": [
|
"segments": [
|
||||||
{
|
{
|
||||||
"points": [
|
"points": [
|
||||||
|
@ -748,6 +764,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 39.21438083934686,
|
"y": 39.21438083934686,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -787,6 +804,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 38.824834009817096,
|
"y": 38.824834009817096,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -832,6 +850,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 1.8178852044727591,
|
"y": 1.8178852044727591,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -883,6 +902,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 1.2616464425132108,
|
"y": 1.2616464425132108,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -934,6 +954,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 90.45732205656805,
|
"y": 90.45732205656805,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -985,6 +1006,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 1.1747704163190065,
|
"y": 1.1747704163190065,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -1030,6 +1052,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": -94.83601179012066,
|
"y": -94.83601179012066,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -1063,6 +1086,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 39.21438083934686,
|
"y": 39.21438083934686,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -1102,6 +1126,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 38.824834009817096,
|
"y": 38.824834009817096,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -1147,6 +1172,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 1.0989312447093198,
|
"y": 1.0989312447093198,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -1198,6 +1224,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 0.9092450946582176,
|
"y": 0.9092450946582176,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -1224,6 +1251,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 80.37089607781583,
|
"h": 80.37089607781583,
|
||||||
"labelColor": "black",
|
"labelColor": "black",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -1278,6 +1306,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": -6.401707036148309,
|
"y": -6.401707036148309,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -1301,6 +1330,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"isClosed": false,
|
"isClosed": false,
|
||||||
"isComplete": false,
|
"isComplete": false,
|
||||||
"isPen": false,
|
"isPen": false,
|
||||||
|
"scale": 1,
|
||||||
"segments": [
|
"segments": [
|
||||||
{
|
{
|
||||||
"points": [
|
"points": [
|
||||||
|
@ -1340,6 +1370,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"isClosed": false,
|
"isClosed": false,
|
||||||
"isComplete": false,
|
"isComplete": false,
|
||||||
"isPen": false,
|
"isPen": false,
|
||||||
|
"scale": 1,
|
||||||
"segments": [
|
"segments": [
|
||||||
{
|
{
|
||||||
"points": [
|
"points": [
|
||||||
|
@ -1413,6 +1444,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 25.20210208864819,
|
"y": 25.20210208864819,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -1458,6 +1490,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 8.260689017945879,
|
"y": 8.260689017945879,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
@ -1484,6 +1517,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 8.680724052756432,
|
"h": 8.680724052756432,
|
||||||
"labelColor": "black",
|
"labelColor": "black",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -1526,6 +1560,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": -42.45472883070397,
|
"y": -42.45472883070397,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "line",
|
"spline": "line",
|
||||||
},
|
},
|
||||||
|
@ -1566,6 +1601,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 80.37089607781583,
|
"h": 80.37089607781583,
|
||||||
"labelColor": "black",
|
"labelColor": "black",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -1620,6 +1656,7 @@ exports[`pasteExcalidrawContent test fixtures line-drawing.json 1`] = `
|
||||||
"y": 90.45732205656805,
|
"y": 90.45732205656805,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "cubic",
|
"spline": "cubic",
|
||||||
},
|
},
|
||||||
|
|
|
@ -97,6 +97,8 @@ export type TLUiTranslationKey =
|
||||||
| 'action.toggle-debug-mode'
|
| 'action.toggle-debug-mode'
|
||||||
| 'action.toggle-focus-mode.menu'
|
| 'action.toggle-focus-mode.menu'
|
||||||
| 'action.toggle-focus-mode'
|
| 'action.toggle-focus-mode'
|
||||||
|
| 'action.toggle-dynamic-size-mode.menu'
|
||||||
|
| 'action.toggle-dynamic-size-mode'
|
||||||
| 'action.toggle-grid.menu'
|
| 'action.toggle-grid.menu'
|
||||||
| 'action.toggle-grid'
|
| 'action.toggle-grid'
|
||||||
| 'action.toggle-lock'
|
| 'action.toggle-lock'
|
||||||
|
|
|
@ -97,6 +97,8 @@ export const DEFAULT_TRANSLATION = {
|
||||||
'action.toggle-debug-mode': 'Toggle debug mode',
|
'action.toggle-debug-mode': 'Toggle debug mode',
|
||||||
'action.toggle-focus-mode.menu': 'Focus mode',
|
'action.toggle-focus-mode.menu': 'Focus mode',
|
||||||
'action.toggle-focus-mode': 'Toggle focus mode',
|
'action.toggle-focus-mode': 'Toggle focus mode',
|
||||||
|
'action.toggle-dynamic-size-mode.menu': 'Dynamic size',
|
||||||
|
'action.toggle-dynamic-size-mode': 'Toggle dynamic size',
|
||||||
'action.toggle-grid.menu': 'Show grid',
|
'action.toggle-grid.menu': 'Show grid',
|
||||||
'action.toggle-grid': 'Toggle grid',
|
'action.toggle-grid': 'Toggle grid',
|
||||||
'action.toggle-lock': 'Toggle locked',
|
'action.toggle-lock': 'Toggle locked',
|
||||||
|
|
|
@ -50,6 +50,7 @@ export type TLUiIconType =
|
||||||
| 'duplicate'
|
| 'duplicate'
|
||||||
| 'edit'
|
| 'edit'
|
||||||
| 'external-link'
|
| 'external-link'
|
||||||
|
| 'fill-fill'
|
||||||
| 'fill-none'
|
| 'fill-none'
|
||||||
| 'fill-pattern'
|
| 'fill-pattern'
|
||||||
| 'fill-semi'
|
| 'fill-semi'
|
||||||
|
@ -192,6 +193,7 @@ export const iconTypes = [
|
||||||
'duplicate',
|
'duplicate',
|
||||||
'edit',
|
'edit',
|
||||||
'external-link',
|
'external-link',
|
||||||
|
'fill-fill',
|
||||||
'fill-none',
|
'fill-none',
|
||||||
'fill-pattern',
|
'fill-pattern',
|
||||||
'fill-semi',
|
'fill-semi',
|
||||||
|
|
|
@ -67,6 +67,7 @@ exports[`buildFromV1Document test fixtures arrow-binding.tldr 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 114.39,
|
"h": 114.39,
|
||||||
"labelColor": "red",
|
"labelColor": "red",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -100,6 +101,7 @@ exports[`buildFromV1Document test fixtures arrow-binding.tldr 1`] = `
|
||||||
"font": "draw",
|
"font": "draw",
|
||||||
"labelColor": "red",
|
"labelColor": "red",
|
||||||
"labelPosition": 0.5,
|
"labelPosition": 0.5,
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"start": {
|
"start": {
|
||||||
"x": 146.32,
|
"x": 146.32,
|
||||||
|
@ -130,6 +132,7 @@ exports[`buildFromV1Document test fixtures arrow-binding.tldr 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 177.03,
|
"h": 177.03,
|
||||||
"labelColor": "red",
|
"labelColor": "red",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -212,6 +215,7 @@ exports[`buildFromV1Document test fixtures exact-arrow-binding.tldr 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 114.39,
|
"h": 114.39,
|
||||||
"labelColor": "red",
|
"labelColor": "red",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -245,6 +249,7 @@ exports[`buildFromV1Document test fixtures exact-arrow-binding.tldr 1`] = `
|
||||||
"font": "draw",
|
"font": "draw",
|
||||||
"labelColor": "red",
|
"labelColor": "red",
|
||||||
"labelPosition": 0.5,
|
"labelPosition": 0.5,
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"start": {
|
"start": {
|
||||||
"x": 293.36,
|
"x": 293.36,
|
||||||
|
@ -275,6 +280,7 @@ exports[`buildFromV1Document test fixtures exact-arrow-binding.tldr 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 177.03,
|
"h": 177.03,
|
||||||
"labelColor": "red",
|
"labelColor": "red",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -340,6 +346,7 @@ exports[`buildFromV1Document test fixtures incorrect-arrow-binding.tldr 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 114.39,
|
"h": 114.39,
|
||||||
"labelColor": "red",
|
"labelColor": "red",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -373,6 +380,7 @@ exports[`buildFromV1Document test fixtures incorrect-arrow-binding.tldr 1`] = `
|
||||||
"font": "draw",
|
"font": "draw",
|
||||||
"labelColor": "red",
|
"labelColor": "red",
|
||||||
"labelPosition": 0.5,
|
"labelPosition": 0.5,
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"start": {
|
"start": {
|
||||||
"x": 252.64,
|
"x": 252.64,
|
||||||
|
@ -403,6 +411,7 @@ exports[`buildFromV1Document test fixtures incorrect-arrow-binding.tldr 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 177.03,
|
"h": 177.03,
|
||||||
"labelColor": "red",
|
"labelColor": "red",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
|
|
@ -329,42 +329,6 @@ describe('When pressing enter on a selected shape', () => {
|
||||||
// })
|
// })
|
||||||
|
|
||||||
describe('When double clicking the selection edge', () => {
|
describe('When double clicking the selection edge', () => {
|
||||||
it('Resets text scale when double clicking the edge of the text', () => {
|
|
||||||
const id = createShapeId()
|
|
||||||
editor
|
|
||||||
.selectAll()
|
|
||||||
.deleteShapes(editor.getSelectedShapeIds())
|
|
||||||
.selectNone()
|
|
||||||
.createShapes([{ id, type: 'text', x: 100, y: 100, props: { scale: 2, text: 'hello' } }])
|
|
||||||
.select(id)
|
|
||||||
.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
|
||||||
|
|
||||||
editor.expectShapeToMatch({ id, props: { scale: 1 } })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Resets text autosize first when double clicking the edge of the text', () => {
|
|
||||||
const id = createShapeId()
|
|
||||||
editor
|
|
||||||
.selectAll()
|
|
||||||
.deleteShapes(editor.getSelectedShapeIds())
|
|
||||||
.selectNone()
|
|
||||||
.createShapes([
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
type: 'text',
|
|
||||||
props: { scale: 2, autoSize: false, w: 200, text: 'hello' },
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.select(id)
|
|
||||||
.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
|
||||||
|
|
||||||
editor.expectShapeToMatch({ id, props: { scale: 2, autoSize: true } })
|
|
||||||
|
|
||||||
editor.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
|
||||||
|
|
||||||
editor.expectShapeToMatch({ id, props: { scale: 1, autoSize: true } })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Begins editing the text if handler returns no change', () => {
|
it('Begins editing the text if handler returns no change', () => {
|
||||||
const id = createShapeId()
|
const id = createShapeId()
|
||||||
editor
|
editor
|
||||||
|
@ -382,10 +346,12 @@ describe('When double clicking the selection edge', () => {
|
||||||
.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
||||||
.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
||||||
|
|
||||||
expect(editor.getEditingShapeId()).toBe(null)
|
// Update:
|
||||||
editor.expectShapeToMatch({ id, props: { scale: 1, autoSize: true } })
|
// Previously, double clicking text edges would reset the scale and prevent editing. This is no longer the case.
|
||||||
|
//
|
||||||
editor.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
// expect(editor.getEditingShapeId()).toBe(null)
|
||||||
|
// editor.expectShapeToMatch({ id, props: { scale: 1, autoSize: true } })
|
||||||
|
// editor.doubleClick(100, 100, { target: 'selection', handle: 'left' })
|
||||||
|
|
||||||
expect(editor.getEditingShapeId()).toBe(id)
|
expect(editor.getEditingShapeId()).toBe(id)
|
||||||
})
|
})
|
||||||
|
|
|
@ -14,6 +14,7 @@ exports[`Draws a bunch: draw shape 1`] = `
|
||||||
"isClosed": false,
|
"isClosed": false,
|
||||||
"isComplete": true,
|
"isComplete": true,
|
||||||
"isPen": false,
|
"isPen": false,
|
||||||
|
"scale": 1,
|
||||||
"segments": [
|
"segments": [
|
||||||
{
|
{
|
||||||
"points": [
|
"points": [
|
||||||
|
|
|
@ -15,6 +15,7 @@ exports[`When resizing a shape with children Resizes a rotated draw shape: draw
|
||||||
"isClosed": false,
|
"isClosed": false,
|
||||||
"isComplete": false,
|
"isComplete": false,
|
||||||
"isPen": false,
|
"isPen": false,
|
||||||
|
"scale": 1,
|
||||||
"segments": [
|
"segments": [
|
||||||
{
|
{
|
||||||
"points": [
|
"points": [
|
||||||
|
|
|
@ -19,6 +19,7 @@ exports[`editor.packShapes packs rotated shapes: packed shapes 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 100,
|
"h": 100,
|
||||||
"labelColor": "black",
|
"labelColor": "black",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -48,6 +49,7 @@ exports[`editor.packShapes packs rotated shapes: packed shapes 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 100,
|
"h": 100,
|
||||||
"labelColor": "black",
|
"labelColor": "black",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -77,6 +79,7 @@ exports[`editor.packShapes packs rotated shapes: packed shapes 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 100,
|
"h": 100,
|
||||||
"labelColor": "black",
|
"labelColor": "black",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -111,6 +114,7 @@ exports[`editor.packShapes packs shapes: packed shapes 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 100,
|
"h": 100,
|
||||||
"labelColor": "black",
|
"labelColor": "black",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -140,6 +144,7 @@ exports[`editor.packShapes packs shapes: packed shapes 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 100,
|
"h": 100,
|
||||||
"labelColor": "black",
|
"labelColor": "black",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
@ -169,6 +174,7 @@ exports[`editor.packShapes packs shapes: packed shapes 1`] = `
|
||||||
"growY": 0,
|
"growY": 0,
|
||||||
"h": 100,
|
"h": 100,
|
||||||
"labelColor": "black",
|
"labelColor": "black",
|
||||||
|
"scale": 1,
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"text": "",
|
"text": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
|
|
|
@ -46,10 +46,11 @@ export const arrowShapeProps: {
|
||||||
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
||||||
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
||||||
end: T.Validator<VecModel>;
|
end: T.Validator<VecModel>;
|
||||||
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
|
fill: EnumStyleProp<"fill" | "none" | "pattern" | "semi" | "solid">;
|
||||||
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
||||||
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
||||||
labelPosition: T.Validator<number>;
|
labelPosition: T.Validator<number>;
|
||||||
|
scale: T.Validator<number>;
|
||||||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||||
start: T.Validator<VecModel>;
|
start: T.Validator<VecModel>;
|
||||||
text: T.Validator<string>;
|
text: T.Validator<string>;
|
||||||
|
@ -195,7 +196,7 @@ export const DefaultColorThemePalette: {
|
||||||
export const DefaultDashStyle: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
export const DefaultDashStyle: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const DefaultFillStyle: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
|
export const DefaultFillStyle: EnumStyleProp<"fill" | "none" | "pattern" | "semi" | "solid">;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const DefaultFontFamilies: {
|
export const DefaultFontFamilies: {
|
||||||
|
@ -235,10 +236,11 @@ export const drawShapeMigrations: TLPropsMigrations;
|
||||||
export const drawShapeProps: {
|
export const drawShapeProps: {
|
||||||
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
||||||
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
||||||
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
|
fill: EnumStyleProp<"fill" | "none" | "pattern" | "semi" | "solid">;
|
||||||
isClosed: T.Validator<boolean>;
|
isClosed: T.Validator<boolean>;
|
||||||
isComplete: T.Validator<boolean>;
|
isComplete: T.Validator<boolean>;
|
||||||
isPen: T.Validator<boolean>;
|
isPen: T.Validator<boolean>;
|
||||||
|
scale: T.Validator<number>;
|
||||||
segments: T.ArrayOfValidator<TLDrawShapeSegment>;
|
segments: T.ArrayOfValidator<TLDrawShapeSegment>;
|
||||||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||||
};
|
};
|
||||||
|
@ -535,12 +537,13 @@ export const geoShapeProps: {
|
||||||
align: EnumStyleProp<"end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start">;
|
align: EnumStyleProp<"end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start">;
|
||||||
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
||||||
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
||||||
fill: EnumStyleProp<"none" | "pattern" | "semi" | "solid">;
|
fill: EnumStyleProp<"fill" | "none" | "pattern" | "semi" | "solid">;
|
||||||
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
||||||
geo: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "heart" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
geo: EnumStyleProp<"arrow-down" | "arrow-left" | "arrow-right" | "arrow-up" | "check-box" | "cloud" | "diamond" | "ellipse" | "heart" | "hexagon" | "octagon" | "oval" | "pentagon" | "rectangle" | "rhombus-2" | "rhombus" | "star" | "trapezoid" | "triangle" | "x-box">;
|
||||||
growY: T.Validator<number>;
|
growY: T.Validator<number>;
|
||||||
h: T.Validator<number>;
|
h: T.Validator<number>;
|
||||||
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
labelColor: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
||||||
|
scale: T.Validator<number>;
|
||||||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||||
text: T.Validator<string>;
|
text: T.Validator<string>;
|
||||||
url: T.Validator<string>;
|
url: T.Validator<string>;
|
||||||
|
@ -573,6 +576,7 @@ export const highlightShapeProps: {
|
||||||
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
||||||
isComplete: T.Validator<boolean>;
|
isComplete: T.Validator<boolean>;
|
||||||
isPen: T.Validator<boolean>;
|
isPen: T.Validator<boolean>;
|
||||||
|
scale: T.Validator<number>;
|
||||||
segments: T.ArrayOfValidator<TLDrawShapeSegment>;
|
segments: T.ArrayOfValidator<TLDrawShapeSegment>;
|
||||||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||||
};
|
};
|
||||||
|
@ -750,6 +754,7 @@ export const lineShapeProps: {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
} & {}>;
|
} & {}>;
|
||||||
|
scale: T.Validator<number>;
|
||||||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||||
spline: EnumStyleProp<"cubic" | "line">;
|
spline: EnumStyleProp<"cubic" | "line">;
|
||||||
};
|
};
|
||||||
|
@ -767,6 +772,7 @@ export const noteShapeProps: {
|
||||||
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
||||||
fontSizeAdjustment: T.Validator<number>;
|
fontSizeAdjustment: T.Validator<number>;
|
||||||
growY: T.Validator<number>;
|
growY: T.Validator<number>;
|
||||||
|
scale: T.Validator<number>;
|
||||||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||||
text: T.Validator<string>;
|
text: T.Validator<string>;
|
||||||
url: T.Validator<string>;
|
url: T.Validator<string>;
|
||||||
|
@ -1061,6 +1067,8 @@ export type TLDefaultColorTheme = Expand<{
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export interface TLDefaultColorThemeColor {
|
export interface TLDefaultColorThemeColor {
|
||||||
|
// (undocumented)
|
||||||
|
fill: string;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
highlight: {
|
highlight: {
|
||||||
p3: string;
|
p3: string;
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { bookmarkShapeVersions } from './shapes/TLBookmarkShape'
|
||||||
import { drawShapeVersions } from './shapes/TLDrawShape'
|
import { drawShapeVersions } from './shapes/TLDrawShape'
|
||||||
import { embedShapeVersions } from './shapes/TLEmbedShape'
|
import { embedShapeVersions } from './shapes/TLEmbedShape'
|
||||||
import { geoShapeVersions } from './shapes/TLGeoShape'
|
import { geoShapeVersions } from './shapes/TLGeoShape'
|
||||||
|
import { highlightShapeVersions } from './shapes/TLHighlightShape'
|
||||||
import { imageShapeVersions } from './shapes/TLImageShape'
|
import { imageShapeVersions } from './shapes/TLImageShape'
|
||||||
import { lineShapeVersions } from './shapes/TLLineShape'
|
import { lineShapeVersions } from './shapes/TLLineShape'
|
||||||
import { noteShapeVersions } from './shapes/TLNoteShape'
|
import { noteShapeVersions } from './shapes/TLNoteShape'
|
||||||
|
@ -1901,6 +1902,78 @@ describe('Extract bindings from arrows', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Add scale to draw shape', () => {
|
||||||
|
const { up, down } = getTestMigration(drawShapeVersions.AddScale)
|
||||||
|
|
||||||
|
test('up works as expected', () => {
|
||||||
|
expect(up({ props: {} })).toEqual({ props: { scale: 1 } })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('down works as expected', () => {
|
||||||
|
expect(down({ props: { scale: 1 } })).toEqual({ props: {} })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Add scale to highlight shape', () => {
|
||||||
|
const { up, down } = getTestMigration(highlightShapeVersions.AddScale)
|
||||||
|
|
||||||
|
test('up works as expected', () => {
|
||||||
|
expect(up({ props: {} })).toEqual({ props: { scale: 1 } })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('down works as expected', () => {
|
||||||
|
expect(down({ props: { scale: 1 } })).toEqual({ props: {} })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Add scale to geo shape', () => {
|
||||||
|
const { up, down } = getTestMigration(geoShapeVersions.AddScale)
|
||||||
|
|
||||||
|
test('up works as expected', () => {
|
||||||
|
expect(up({ props: {} })).toEqual({ props: { scale: 1 } })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('down works as expected', () => {
|
||||||
|
expect(down({ props: { scale: 1 } })).toEqual({ props: {} })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Add scale to arrow shape', () => {
|
||||||
|
const { up, down } = getTestMigration(arrowShapeVersions.AddScale)
|
||||||
|
|
||||||
|
test('up works as expected', () => {
|
||||||
|
expect(up({ props: {} })).toEqual({ props: { scale: 1 } })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('down works as expected', () => {
|
||||||
|
expect(down({ props: { scale: 1 } })).toEqual({ props: {} })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Add scale to note shape', () => {
|
||||||
|
const { up, down } = getTestMigration(noteShapeVersions.AddScale)
|
||||||
|
|
||||||
|
test('up works as expected', () => {
|
||||||
|
expect(up({ props: {} })).toEqual({ props: { scale: 1 } })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('down works as expected', () => {
|
||||||
|
expect(down({ props: { scale: 1 } })).toEqual({ props: {} })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Add scale to line shape', () => {
|
||||||
|
const { up, down } = getTestMigration(lineShapeVersions.AddScale)
|
||||||
|
|
||||||
|
test('up works as expected', () => {
|
||||||
|
expect(up({ props: {} })).toEqual({ props: { scale: 1 } })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('down works as expected', () => {
|
||||||
|
expect(down({ props: { scale: 1 } })).toEqual({ props: {} })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
||||||
|
|
||||||
// check that all migrator fns were called at least once
|
// check that all migrator fns were called at least once
|
||||||
|
|
|
@ -55,6 +55,7 @@ export const arrowShapeProps = {
|
||||||
bend: T.number,
|
bend: T.number,
|
||||||
text: T.string,
|
text: T.string,
|
||||||
labelPosition: T.number,
|
labelPosition: T.number,
|
||||||
|
scale: T.nonZeroNumber,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -68,6 +69,7 @@ export const arrowShapeVersions = createShapePropsMigrationIds('arrow', {
|
||||||
AddIsPrecise: 2,
|
AddIsPrecise: 2,
|
||||||
AddLabelPosition: 3,
|
AddLabelPosition: 3,
|
||||||
ExtractBindings: 4,
|
ExtractBindings: 4,
|
||||||
|
AddScale: 5,
|
||||||
})
|
})
|
||||||
|
|
||||||
function propsMigration(migration: TLPropsMigration) {
|
function propsMigration(migration: TLPropsMigration) {
|
||||||
|
@ -197,5 +199,14 @@ export const arrowShapeMigrations = createMigrationSequence({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
propsMigration({
|
||||||
|
id: arrowShapeVersions.AddScale,
|
||||||
|
up: (props) => {
|
||||||
|
props.scale = 1
|
||||||
|
},
|
||||||
|
down: (props) => {
|
||||||
|
delete props.scale
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -29,6 +29,7 @@ export const drawShapeProps = {
|
||||||
isComplete: T.boolean,
|
isComplete: T.boolean,
|
||||||
isClosed: T.boolean,
|
isClosed: T.boolean,
|
||||||
isPen: T.boolean,
|
isPen: T.boolean,
|
||||||
|
scale: T.nonZeroNumber,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -39,6 +40,7 @@ export type TLDrawShape = TLBaseShape<'draw', TLDrawShapeProps>
|
||||||
|
|
||||||
const Versions = createShapePropsMigrationIds('draw', {
|
const Versions = createShapePropsMigrationIds('draw', {
|
||||||
AddInPen: 1,
|
AddInPen: 1,
|
||||||
|
AddScale: 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
export { Versions as drawShapeVersions }
|
export { Versions as drawShapeVersions }
|
||||||
|
@ -71,5 +73,14 @@ export const drawShapeMigrations = createShapePropsMigrationSequence({
|
||||||
},
|
},
|
||||||
down: 'retired',
|
down: 'retired',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: Versions.AddScale,
|
||||||
|
up: (props) => {
|
||||||
|
props.scale = 1
|
||||||
|
},
|
||||||
|
down: (props) => {
|
||||||
|
delete props.scale
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -60,6 +60,7 @@ export const geoShapeProps = {
|
||||||
h: T.nonZeroNumber,
|
h: T.nonZeroNumber,
|
||||||
growY: T.positiveNumber,
|
growY: T.positiveNumber,
|
||||||
text: T.string,
|
text: T.string,
|
||||||
|
scale: T.nonZeroNumber,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -77,6 +78,7 @@ const geoShapeVersions = createShapePropsMigrationIds('geo', {
|
||||||
MigrateLegacyAlign: 6,
|
MigrateLegacyAlign: 6,
|
||||||
AddCloud: 7,
|
AddCloud: 7,
|
||||||
MakeUrlsValid: 8,
|
MakeUrlsValid: 8,
|
||||||
|
AddScale: 9,
|
||||||
})
|
})
|
||||||
|
|
||||||
export { geoShapeVersions as geoShapeVersions }
|
export { geoShapeVersions as geoShapeVersions }
|
||||||
|
@ -158,5 +160,14 @@ export const geoShapeMigrations = createShapePropsMigrationSequence({
|
||||||
// noop
|
// noop
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: geoShapeVersions.AddScale,
|
||||||
|
up: (props) => {
|
||||||
|
props.scale = 1
|
||||||
|
},
|
||||||
|
down: (props) => {
|
||||||
|
delete props.scale
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import { createShapePropsMigrationSequence } from '../records/TLShape'
|
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from '../records/TLShape'
|
||||||
import { RecordPropsType } from '../recordsWithProps'
|
import { RecordPropsType } from '../recordsWithProps'
|
||||||
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
||||||
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
import { DefaultSizeStyle } from '../styles/TLSizeStyle'
|
||||||
|
@ -13,8 +13,15 @@ export const highlightShapeProps = {
|
||||||
segments: T.arrayOf(DrawShapeSegment),
|
segments: T.arrayOf(DrawShapeSegment),
|
||||||
isComplete: T.boolean,
|
isComplete: T.boolean,
|
||||||
isPen: T.boolean,
|
isPen: T.boolean,
|
||||||
|
scale: T.nonZeroNumber,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Versions = createShapePropsMigrationIds('highlight', {
|
||||||
|
AddScale: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
export { Versions as highlightShapeVersions }
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TLHighlightShapeProps = RecordPropsType<typeof highlightShapeProps>
|
export type TLHighlightShapeProps = RecordPropsType<typeof highlightShapeProps>
|
||||||
|
|
||||||
|
@ -22,4 +29,16 @@ export type TLHighlightShapeProps = RecordPropsType<typeof highlightShapeProps>
|
||||||
export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>
|
export type TLHighlightShape = TLBaseShape<'highlight', TLHighlightShapeProps>
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const highlightShapeMigrations = createShapePropsMigrationSequence({ sequence: [] })
|
export const highlightShapeMigrations = createShapePropsMigrationSequence({
|
||||||
|
sequence: [
|
||||||
|
{
|
||||||
|
id: Versions.AddScale,
|
||||||
|
up: (props) => {
|
||||||
|
props.scale = 1
|
||||||
|
},
|
||||||
|
down: (props) => {
|
||||||
|
delete props.scale
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
|
@ -31,6 +31,7 @@ export const lineShapeProps = {
|
||||||
size: DefaultSizeStyle,
|
size: DefaultSizeStyle,
|
||||||
spline: LineShapeSplineStyle,
|
spline: LineShapeSplineStyle,
|
||||||
points: T.dict(T.string, lineShapePointValidator),
|
points: T.dict(T.string, lineShapePointValidator),
|
||||||
|
scale: T.nonZeroNumber,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -45,6 +46,7 @@ export const lineShapeVersions = createShapePropsMigrationIds('line', {
|
||||||
RemoveExtraHandleProps: 2,
|
RemoveExtraHandleProps: 2,
|
||||||
HandlesToPoints: 3,
|
HandlesToPoints: 3,
|
||||||
PointIndexIds: 4,
|
PointIndexIds: 4,
|
||||||
|
AddScale: 5,
|
||||||
})
|
})
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -155,5 +157,14 @@ export const lineShapeMigrations = createShapePropsMigrationSequence({
|
||||||
props.points = sortedHandles.map(({ x, y }) => ({ x, y }))
|
props.points = sortedHandles.map(({ x, y }) => ({ x, y }))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: lineShapeVersions.AddScale,
|
||||||
|
up: (props) => {
|
||||||
|
props.scale = 1
|
||||||
|
},
|
||||||
|
down: (props) => {
|
||||||
|
delete props.scale
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,6 +19,7 @@ export const noteShapeProps = {
|
||||||
growY: T.positiveNumber,
|
growY: T.positiveNumber,
|
||||||
url: T.linkUrl,
|
url: T.linkUrl,
|
||||||
text: T.string,
|
text: T.string,
|
||||||
|
scale: T.nonZeroNumber,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -34,6 +35,7 @@ const Versions = createShapePropsMigrationIds('note', {
|
||||||
AddVerticalAlign: 4,
|
AddVerticalAlign: 4,
|
||||||
MakeUrlsValid: 5,
|
MakeUrlsValid: 5,
|
||||||
AddFontSizeAdjustment: 6,
|
AddFontSizeAdjustment: 6,
|
||||||
|
AddScale: 7,
|
||||||
})
|
})
|
||||||
|
|
||||||
export { Versions as noteShapeVersions }
|
export { Versions as noteShapeVersions }
|
||||||
|
@ -101,5 +103,14 @@ export const noteShapeMigrations = createShapePropsMigrationSequence({
|
||||||
delete props.fontSizeAdjustment
|
delete props.fontSizeAdjustment
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: Versions.AddScale,
|
||||||
|
up: (props) => {
|
||||||
|
props.scale = 1
|
||||||
|
},
|
||||||
|
down: (props) => {
|
||||||
|
delete props.scale
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -24,6 +24,7 @@ export interface TLDefaultColorThemeColor {
|
||||||
solid: string
|
solid: string
|
||||||
semi: string
|
semi: string
|
||||||
pattern: string
|
pattern: string
|
||||||
|
fill: string // same as solid
|
||||||
note: {
|
note: {
|
||||||
fill: string
|
fill: string
|
||||||
text: string
|
text: string
|
||||||
|
@ -56,6 +57,7 @@ export const DefaultColorThemePalette: {
|
||||||
solid: '#fcfffe',
|
solid: '#fcfffe',
|
||||||
black: {
|
black: {
|
||||||
solid: '#1d1d1d',
|
solid: '#1d1d1d',
|
||||||
|
fill: '#1d1d1d',
|
||||||
note: {
|
note: {
|
||||||
fill: '#FCE19C',
|
fill: '#FCE19C',
|
||||||
text: '#000000',
|
text: '#000000',
|
||||||
|
@ -69,6 +71,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
blue: {
|
blue: {
|
||||||
solid: '#4465e9',
|
solid: '#4465e9',
|
||||||
|
fill: '#4465e9',
|
||||||
note: {
|
note: {
|
||||||
fill: '#8AA3FF',
|
fill: '#8AA3FF',
|
||||||
text: '#000000',
|
text: '#000000',
|
||||||
|
@ -82,6 +85,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
green: {
|
green: {
|
||||||
solid: '#099268',
|
solid: '#099268',
|
||||||
|
fill: '#099268',
|
||||||
note: {
|
note: {
|
||||||
fill: '#6FC896',
|
fill: '#6FC896',
|
||||||
text: '#000000',
|
text: '#000000',
|
||||||
|
@ -95,6 +99,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
grey: {
|
grey: {
|
||||||
solid: '#9fa8b2',
|
solid: '#9fa8b2',
|
||||||
|
fill: '#9fa8b2',
|
||||||
note: {
|
note: {
|
||||||
fill: '#C0CAD3',
|
fill: '#C0CAD3',
|
||||||
text: '#000000',
|
text: '#000000',
|
||||||
|
@ -108,6 +113,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
'light-blue': {
|
'light-blue': {
|
||||||
solid: '#4ba1f1',
|
solid: '#4ba1f1',
|
||||||
|
fill: '#4ba1f1',
|
||||||
note: {
|
note: {
|
||||||
fill: '#9BC4FD',
|
fill: '#9BC4FD',
|
||||||
text: '#000000',
|
text: '#000000',
|
||||||
|
@ -121,6 +127,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
'light-green': {
|
'light-green': {
|
||||||
solid: '#4cb05e',
|
solid: '#4cb05e',
|
||||||
|
fill: '#4cb05e',
|
||||||
note: {
|
note: {
|
||||||
fill: '#98D08A',
|
fill: '#98D08A',
|
||||||
text: '#000000',
|
text: '#000000',
|
||||||
|
@ -134,6 +141,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
'light-red': {
|
'light-red': {
|
||||||
solid: '#f87777',
|
solid: '#f87777',
|
||||||
|
fill: '#f87777',
|
||||||
note: {
|
note: {
|
||||||
fill: '#F7A5A1',
|
fill: '#F7A5A1',
|
||||||
text: '#000000',
|
text: '#000000',
|
||||||
|
@ -147,6 +155,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
'light-violet': {
|
'light-violet': {
|
||||||
solid: '#e085f4',
|
solid: '#e085f4',
|
||||||
|
fill: '#e085f4',
|
||||||
note: {
|
note: {
|
||||||
fill: '#DFB0F9',
|
fill: '#DFB0F9',
|
||||||
text: '#000000',
|
text: '#000000',
|
||||||
|
@ -160,6 +169,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
orange: {
|
orange: {
|
||||||
solid: '#e16919',
|
solid: '#e16919',
|
||||||
|
fill: '#e16919',
|
||||||
note: {
|
note: {
|
||||||
fill: '#FAA475',
|
fill: '#FAA475',
|
||||||
text: '#000000',
|
text: '#000000',
|
||||||
|
@ -173,6 +183,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
red: {
|
red: {
|
||||||
solid: '#e03131',
|
solid: '#e03131',
|
||||||
|
fill: '#e03131',
|
||||||
note: {
|
note: {
|
||||||
fill: '#FC8282',
|
fill: '#FC8282',
|
||||||
text: '#000000',
|
text: '#000000',
|
||||||
|
@ -186,6 +197,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
violet: {
|
violet: {
|
||||||
solid: '#ae3ec9',
|
solid: '#ae3ec9',
|
||||||
|
fill: '#ae3ec9',
|
||||||
note: {
|
note: {
|
||||||
fill: '#DB91FD',
|
fill: '#DB91FD',
|
||||||
text: '#000000',
|
text: '#000000',
|
||||||
|
@ -199,6 +211,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
yellow: {
|
yellow: {
|
||||||
solid: '#f1ac4b',
|
solid: '#f1ac4b',
|
||||||
|
fill: '#f1ac4b',
|
||||||
note: {
|
note: {
|
||||||
fill: '#FED49A',
|
fill: '#FED49A',
|
||||||
text: '#000000',
|
text: '#000000',
|
||||||
|
@ -212,6 +225,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
white: {
|
white: {
|
||||||
solid: '#FFFFFF',
|
solid: '#FFFFFF',
|
||||||
|
fill: '#FFFFFF',
|
||||||
semi: '#f5f5f5',
|
semi: '#f5f5f5',
|
||||||
pattern: '#f9f9f9',
|
pattern: '#f9f9f9',
|
||||||
note: {
|
note: {
|
||||||
|
@ -232,6 +246,7 @@ export const DefaultColorThemePalette: {
|
||||||
|
|
||||||
black: {
|
black: {
|
||||||
solid: '#f2f2f2',
|
solid: '#f2f2f2',
|
||||||
|
fill: '#f2f2f2',
|
||||||
note: {
|
note: {
|
||||||
fill: '#2c2c2c',
|
fill: '#2c2c2c',
|
||||||
text: '#f2f2f2',
|
text: '#f2f2f2',
|
||||||
|
@ -245,6 +260,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
blue: {
|
blue: {
|
||||||
solid: '#4f72fc', // 3c60f0
|
solid: '#4f72fc', // 3c60f0
|
||||||
|
fill: '#4f72fc',
|
||||||
note: {
|
note: {
|
||||||
fill: '#2A3F98',
|
fill: '#2A3F98',
|
||||||
text: '#f2f2f2',
|
text: '#f2f2f2',
|
||||||
|
@ -258,6 +274,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
green: {
|
green: {
|
||||||
solid: '#099268',
|
solid: '#099268',
|
||||||
|
fill: '#099268',
|
||||||
note: {
|
note: {
|
||||||
fill: '#014429',
|
fill: '#014429',
|
||||||
text: '#f2f2f2',
|
text: '#f2f2f2',
|
||||||
|
@ -271,6 +288,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
grey: {
|
grey: {
|
||||||
solid: '#9398b0',
|
solid: '#9398b0',
|
||||||
|
fill: '#9398b0',
|
||||||
note: {
|
note: {
|
||||||
fill: '#56595F',
|
fill: '#56595F',
|
||||||
text: '#f2f2f2',
|
text: '#f2f2f2',
|
||||||
|
@ -284,6 +302,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
'light-blue': {
|
'light-blue': {
|
||||||
solid: '#4dabf7',
|
solid: '#4dabf7',
|
||||||
|
fill: '#4dabf7',
|
||||||
note: {
|
note: {
|
||||||
fill: '#1F5495',
|
fill: '#1F5495',
|
||||||
text: '#f2f2f2',
|
text: '#f2f2f2',
|
||||||
|
@ -297,6 +316,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
'light-green': {
|
'light-green': {
|
||||||
solid: '#40c057',
|
solid: '#40c057',
|
||||||
|
fill: '#40c057',
|
||||||
note: {
|
note: {
|
||||||
fill: '#21581D',
|
fill: '#21581D',
|
||||||
text: '#f2f2f2',
|
text: '#f2f2f2',
|
||||||
|
@ -310,6 +330,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
'light-red': {
|
'light-red': {
|
||||||
solid: '#ff8787',
|
solid: '#ff8787',
|
||||||
|
fill: '#ff8787',
|
||||||
note: {
|
note: {
|
||||||
fill: '#923632',
|
fill: '#923632',
|
||||||
text: '#f2f2f2',
|
text: '#f2f2f2',
|
||||||
|
@ -323,6 +344,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
'light-violet': {
|
'light-violet': {
|
||||||
solid: '#e599f7',
|
solid: '#e599f7',
|
||||||
|
fill: '#e599f7',
|
||||||
note: {
|
note: {
|
||||||
fill: '#762F8E',
|
fill: '#762F8E',
|
||||||
text: '#f2f2f2',
|
text: '#f2f2f2',
|
||||||
|
@ -336,6 +358,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
orange: {
|
orange: {
|
||||||
solid: '#f76707',
|
solid: '#f76707',
|
||||||
|
fill: '#f76707',
|
||||||
note: {
|
note: {
|
||||||
fill: '#843906',
|
fill: '#843906',
|
||||||
text: '#f2f2f2',
|
text: '#f2f2f2',
|
||||||
|
@ -349,6 +372,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
red: {
|
red: {
|
||||||
solid: '#e03131',
|
solid: '#e03131',
|
||||||
|
fill: '#e03131',
|
||||||
note: {
|
note: {
|
||||||
fill: '#89231A',
|
fill: '#89231A',
|
||||||
text: '#f2f2f2',
|
text: '#f2f2f2',
|
||||||
|
@ -362,6 +386,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
violet: {
|
violet: {
|
||||||
solid: '#ae3ec9',
|
solid: '#ae3ec9',
|
||||||
|
fill: '#ae3ec9',
|
||||||
note: {
|
note: {
|
||||||
fill: '#681683',
|
fill: '#681683',
|
||||||
text: '#f2f2f2',
|
text: '#f2f2f2',
|
||||||
|
@ -375,6 +400,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
yellow: {
|
yellow: {
|
||||||
solid: '#ffc034',
|
solid: '#ffc034',
|
||||||
|
fill: '#ffc034',
|
||||||
note: {
|
note: {
|
||||||
fill: '#98571B',
|
fill: '#98571B',
|
||||||
text: '#f2f2f2',
|
text: '#f2f2f2',
|
||||||
|
@ -388,6 +414,7 @@ export const DefaultColorThemePalette: {
|
||||||
},
|
},
|
||||||
white: {
|
white: {
|
||||||
solid: '#f3f3f3',
|
solid: '#f3f3f3',
|
||||||
|
fill: '#f3f3f3',
|
||||||
semi: '#f5f5f5',
|
semi: '#f5f5f5',
|
||||||
pattern: '#f9f9f9',
|
pattern: '#f9f9f9',
|
||||||
note: {
|
note: {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { StyleProp } from './StyleProp'
|
||||||
/** @public */
|
/** @public */
|
||||||
export const DefaultFillStyle = StyleProp.defineEnum('tldraw:fill', {
|
export const DefaultFillStyle = StyleProp.defineEnum('tldraw:fill', {
|
||||||
defaultValue: 'none',
|
defaultValue: 'none',
|
||||||
values: ['none', 'semi', 'solid', 'pattern'],
|
values: ['none', 'semi', 'solid', 'pattern', 'fill'],
|
||||||
})
|
})
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
|
|
@ -57,6 +57,7 @@ const oldArrow: TLBaseShape<'arrow', Omit<TLArrowShapeProps, 'labelColor'>> = {
|
||||||
text: '',
|
text: '',
|
||||||
font: 'draw',
|
font: 'draw',
|
||||||
labelPosition: 0.5,
|
labelPosition: 0.5,
|
||||||
|
scale: 1,
|
||||||
},
|
},
|
||||||
meta: {},
|
meta: {},
|
||||||
}
|
}
|
||||||
|
|