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