Replaces isDarkMode with meta, a more flexible option for custom rendering context

This commit is contained in:
Steve Ruiz 2021-08-30 11:44:42 +01:00
parent 7d18be27cf
commit 64d00dc427
47 changed files with 391 additions and 336 deletions

View file

@ -1,7 +1,7 @@
{
"createComment": {
"scope": "typescript,typescriptreact",
"prefix": "/**",
"prefix": "ccc",
"body": [
"/**",
" * ${1:description}",
@ -10,8 +10,7 @@
" *",
" *```ts",
" * ${2:example}",
" *```",
" */"
" *```"
],
"description": "comment"
}

View file

@ -19,11 +19,12 @@
"test": "jest",
"test:watch": "test --watchAll",
"lerna": "lerna",
"start": "yarn build:packages && lerna run start --parallel",
"start": "lerna run start --stream --parallel",
"start:www": "yarn build:packages && lerna run start --parallel & cd packages/www && yarn dev",
"build": "yarn build:packages && cd packages/www && yarn build",
"build:packages": "cd packages/core && yarn build && cd ../tldraw && yarn build",
"publish:patch": "yarn build:packages && lerna publish patch"
"publish:patch": "yarn build:packages && lerna publish patch",
"docs": "lerna run docs --stream"
},
"devDependencies": {
"@babel/plugin-syntax-import-meta": "^7.10.4",
@ -46,6 +47,7 @@
"react-dom": "^17.0.2",
"ts-jest": "^27.0.5",
"tslib": "^2.3.0",
"typedoc": "^0.21.9",
"typescript": "^4.4.2"
},
"dependencies": {},

View file

@ -23,13 +23,13 @@ interface CanvasProps<T extends TLShape> {
hideBounds?: boolean
hideHandles?: boolean
hideIndicators?: boolean
isDarkMode?: boolean
meta?: Record<string, unknown>
}
export const Canvas = React.memo(function Canvas<T extends TLShape>({
page,
pageState,
isDarkMode = false,
meta,
hideHandles = false,
hideBounds = false,
hideIndicators = false,
@ -58,7 +58,7 @@ export const Canvas = React.memo(function Canvas<T extends TLShape>({
hideBounds={hideBounds}
hideIndicators={hideIndicators}
hideHandles={hideHandles}
isDarkMode={isDarkMode}
meta={meta}
/>
<Brush />
</g>

View file

@ -11,7 +11,6 @@ describe('page', () => {
hideBounds={false}
hideIndicators={false}
hideHandles={false}
isDarkMode={false}
/>
)
})

View file

@ -13,22 +13,30 @@ interface PageProps<T extends TLShape> {
hideBounds: boolean
hideHandles: boolean
hideIndicators: boolean
isDarkMode: boolean
meta?: Record<string, unknown>
}
/**
* The Page component renders the current page.
*
* ### Example
*
*```ts
* example
*``` */
export function Page<T extends TLShape>({
page,
pageState,
hideBounds,
hideHandles,
hideIndicators,
isDarkMode,
meta,
}: PageProps<T>): JSX.Element {
const { callbacks, shapeUtils } = useTLContext()
useRenderOnResize()
const shapeTree = useShapeTree(page, pageState, shapeUtils, isDarkMode, callbacks.onChange)
const shapeTree = useShapeTree(page, pageState, shapeUtils, meta, callbacks.onChange)
const { shapeWithHandles } = useHandles(page, pageState)

View file

@ -3,7 +3,6 @@ import type {
TLShape,
TLPage,
TLPageState,
TLSettings,
TLCallbacks,
TLShapeUtils,
TLTheme,
@ -13,30 +12,79 @@ import type {
import { Canvas } from '../canvas'
import { useTLTheme, TLContext } from '../../hooks'
export interface RendererProps<T extends TLShape>
extends Partial<TLSettings>,
Partial<TLCallbacks> {
export interface RendererProps<T extends TLShape, M extends Record<string, unknown>>
extends Partial<TLCallbacks> {
/**
* An object containing instances of your shape classes.
*/
shapeUtils: TLShapeUtils<T>
/**
* The current page, containing shapes and bindings.
*/
page: TLPage<T, TLBinding>
/**
* The current page state.
*/
pageState: TLPageState
/**
* An object of custom theme colors.
*/
theme?: Partial<TLTheme>
/**
* When true, the renderer will not show the bounds for selected objects.
*/
hideBounds?: boolean
/**
* When true, the renderer will not show the handles of shapes with handles.
*/
hideHandles?: boolean
/**
* When true, the renderer will not show indicators for selected or hovered objects,
*/
hideIndicators?: boolean
isDarkMode?: boolean
/**
* When true, the renderer will ignore all inputs that were not made by a stylus or pen-type device.
*/
isPenMode?: boolean
/**
* An object of custom options that should be passed to rendered shapes.
*/
meta?: M
}
export function Renderer<T extends TLShape>({
/**
The Renderer component is the main component of the library. It accepts the current `page`, the `shapeUtils` needed to interpret and render the shapes and bindings on the `page`, and the current `pageState`.
* It also (optionally) accepts several settings and visibility flags,
* a `theme` to use, and callbacks to respond to various user interactions.
*
* ### Example
*
*```tsx
* <Renderer
* shapeUtils={shapeUtils}
* page={page}
* pageState={pageState}
* />
*```
*/
/**
* The Renderer component is the main component of the library. It accepts the current `page`, the `shapeUtils` needed to interpret and render the shapes and bindings on the `page`, and the current `pageState`.
* @param props
* @returns
*/
export function Renderer<T extends TLShape, M extends Record<string, unknown>>({
shapeUtils,
page,
pageState,
theme,
meta,
hideHandles = false,
hideIndicators = false,
hideBounds = false,
isDarkMode = false,
...rest
}: RendererProps<T>): JSX.Element {
}: RendererProps<T, M>): JSX.Element {
useTLTheme(theme)
const rScreenBounds = React.useRef<TLBounds>(null)
const rPageState = React.useRef<TLPageState>(pageState)
@ -60,7 +108,7 @@ export function Renderer<T extends TLShape>({
hideBounds={hideBounds}
hideIndicators={hideIndicators}
hideHandles={hideHandles}
isDarkMode={isDarkMode}
meta={meta}
/>
</TLContext.Provider>
)

View file

@ -2,19 +2,21 @@ import { useTLContext } from '+hooks'
import * as React from 'react'
import type { TLShapeUtil, TLRenderInfo, TLShape } from '+types'
interface EditingShapeProps<T extends TLShape> extends TLRenderInfo {
interface EditingShapeProps<T extends TLShape, M extends Record<string, unknown>>
extends TLRenderInfo {
shape: T
utils: TLShapeUtil<T>
meta?: M
}
export function EditingTextShape({
export function EditingTextShape<M extends Record<string, unknown>>({
shape,
utils,
isEditing,
isBinding,
isDarkMode,
isCurrentParent,
}: EditingShapeProps<TLShape>) {
meta,
}: EditingShapeProps<TLShape, M>) {
const {
callbacks: { onTextChange, onTextBlur, onTextFocus, onTextKeyDown, onTextKeyUp },
} = useTLContext()
@ -26,11 +28,11 @@ export function EditingTextShape({
isEditing,
isCurrentParent,
isBinding,
isDarkMode,
onTextChange,
onTextBlur,
onTextFocus,
onTextKeyDown,
onTextKeyUp,
meta,
})
}

View file

@ -1,32 +1,34 @@
import * as React from 'react'
import type { TLShapeUtil, TLRenderInfo, TLShape } from '+types'
interface RenderedShapeProps<T extends TLShape> extends TLRenderInfo {
interface RenderedShapeProps<T extends TLShape, M extends Record<string, unknown>>
extends TLRenderInfo {
shape: T
utils: TLShapeUtil<T>
meta?: M
}
export const RenderedShape = React.memo(
function RenderedShape({
function RenderedShape<M extends Record<string, unknown>>({
shape,
utils,
isEditing,
isBinding,
isDarkMode,
isCurrentParent,
}: RenderedShapeProps<TLShape>) {
meta,
}: RenderedShapeProps<TLShape, M>) {
return utils.render(shape, {
isEditing,
isBinding,
isDarkMode,
isCurrentParent,
meta,
})
},
(prev, next) => {
if (
prev.isEditing !== next.isEditing ||
prev.isDarkMode !== next.isDarkMode ||
prev.isBinding !== next.isBinding ||
prev.meta !== next.meta ||
prev.isCurrentParent !== next.isCurrentParent
) {
return false

View file

@ -3,15 +3,22 @@ import type { IShapeTreeNode } from '+types'
import { Shape } from './shape'
export const ShapeNode = React.memo(
({ shape, children, isEditing, isDarkMode, isBinding, isCurrentParent }: IShapeTreeNode) => {
<M extends Record<string, unknown>>({
shape,
children,
isEditing,
isBinding,
isCurrentParent,
meta,
}: IShapeTreeNode<M>) => {
return (
<>
<Shape
shape={shape}
isEditing={isEditing}
isDarkMode={isDarkMode}
isBinding={isBinding}
isCurrentParent={isCurrentParent}
meta={meta}
/>
{children &&
children.map((childNode) => <ShapeNode key={childNode.shape.id} {...childNode} />)}

View file

@ -9,7 +9,6 @@ describe('handles', () => {
shape={mockUtils.box.create({})}
isEditing={false}
isBinding={false}
isDarkMode={false}
isCurrentParent={false}
/>
)

View file

@ -5,7 +5,13 @@ import { RenderedShape } from './rendered-shape'
import { EditingTextShape } from './editing-text-shape'
export const Shape = React.memo(
({ shape, isEditing, isBinding, isDarkMode, isCurrentParent }: IShapeTreeNode) => {
<M extends Record<string, unknown>>({
shape,
isEditing,
isBinding,
isCurrentParent,
meta,
}: IShapeTreeNode<M>) => {
const { shapeUtils } = useTLContext()
const events = useShapeEvents(shape.id, isCurrentParent)
const utils = shapeUtils[shape.type]
@ -26,9 +32,9 @@ export const Shape = React.memo(
shape={shape}
isBinding={false}
isCurrentParent={false}
isDarkMode={isDarkMode}
isEditing={true}
utils={utils}
meta={meta}
/>
) : (
<RenderedShape
@ -36,8 +42,8 @@ export const Shape = React.memo(
utils={utils}
isBinding={isBinding}
isCurrentParent={isCurrentParent}
isDarkMode={isDarkMode}
isEditing={isEditing}
meta={meta}
/>
)}
</g>

View file

@ -10,26 +10,26 @@ import type {
} from '+types'
import { Utils, Vec } from '+utils'
function addToShapeTree<T extends TLShape>(
function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
shape: TLShape,
branch: IShapeTreeNode[],
branch: IShapeTreeNode<M>[],
shapes: TLPage<T, TLBinding>['shapes'],
selectedIds: string[],
info: {
pageState: {
bindingId?: string
hoveredId?: string
currentParentId?: string
editingId?: string
editingBindingId?: string
isDarkMode?: boolean
}
},
meta?: M
) {
const node: IShapeTreeNode = {
const node: IShapeTreeNode<M> = {
shape,
isCurrentParent: info.currentParentId === shape.id,
isEditing: info.editingId === shape.id,
isBinding: info.bindingId === shape.id,
isDarkMode: info.isDarkMode || false,
isCurrentParent: pageState.currentParentId === shape.id,
isEditing: pageState.editingId === shape.id,
isBinding: pageState.bindingId === shape.id,
meta,
}
branch.push(node)
@ -41,16 +41,16 @@ function addToShapeTree<T extends TLShape>(
.sort((a, b) => a.childIndex - b.childIndex)
.forEach((childShape) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
addToShapeTree(childShape, node.children!, shapes, selectedIds, info)
addToShapeTree(childShape, node.children!, shapes, selectedIds, pageState, meta)
)
}
}
export function useShapeTree<T extends TLShape>(
export function useShapeTree<T extends TLShape, M extends Record<string, unknown>>(
page: TLPage<T, TLBinding>,
pageState: TLPageState,
shapeUtils: TLShapeUtils<T>,
isDarkMode: boolean,
meta?: M,
onChange?: TLCallbacks['onChange']
) {
const rPreviousCount = React.useRef(0)
@ -102,13 +102,11 @@ export function useShapeTree<T extends TLShape>(
// Populate the shape tree
const tree: IShapeTreeNode[] = []
const tree: IShapeTreeNode<M>[] = []
shapesToRender
.sort((a, b) => a.childIndex - b.childIndex)
.forEach((shape) =>
addToShapeTree(shape, tree, page.shapes, selectedIds, { ...pageState, isDarkMode })
)
.forEach((shape) => addToShapeTree(shape, tree, page.shapes, selectedIds, pageState, meta))
return tree
}

View file

@ -2,7 +2,3 @@ export * from './components'
export * from './types'
export * from './utils'
export * from './inputs'
export interface Steve {
age: string
}

View file

@ -54,17 +54,17 @@ export interface TLShape {
export type TLShapeUtils<T extends TLShape> = Record<string, TLShapeUtil<T>>
export interface TLRenderInfo<T extends SVGElement | HTMLElement = any> {
export interface TLRenderInfo<M = any, T extends SVGElement | HTMLElement = any> {
ref?: React.RefObject<T>
isEditing: boolean
isBinding: boolean
isDarkMode: boolean
isCurrentParent: boolean
ref?: React.RefObject<T>
onTextChange?: TLCallbacks['onTextChange']
onTextBlur?: TLCallbacks['onTextBlur']
onTextFocus?: TLCallbacks['onTextFocus']
onTextKeyDown?: TLCallbacks['onTextKeyDown']
onTextKeyUp?: TLCallbacks['onTextKeyUp']
meta: M extends any ? M : never
}
export interface TLTool {
@ -79,12 +79,6 @@ export interface TLBinding {
fromId: string
}
export interface TLSettings {
isDebugMode: boolean
isDarkMode: boolean
isPenMode: boolean
}
export interface TLTheme {
brushFill?: string
brushStroke?: string
@ -375,24 +369,26 @@ export abstract class TLShapeUtil<T extends TLShape> {
/* -------------------- Internal -------------------- */
export interface IShapeTreeNode {
export interface IShapeTreeNode<M extends Record<string, unknown>> {
shape: TLShape
children?: IShapeTreeNode[]
children?: IShapeTreeNode<M>[]
isEditing: boolean
isBinding: boolean
isDarkMode: boolean
isCurrentParent: boolean
meta?: M
}
/* -------------------------------------------------- */
/* Utility Types */
/* -------------------------------------------------- */
/** @internal */
export type MappedByType<T extends { type: string }> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[P in T['type']]: T extends any ? (P extends T['type'] ? T : never) : never
}
/** @internal */
export type RequiredKeys<T> = {
[K in keyof T]-?: Record<string, unknown> extends Pick<T, K> ? never : K
}[keyof T]

View file

@ -1718,14 +1718,6 @@ export default Utils
// Helper types
export type DeepPartial<T> = T extends Function
? T
: T extends object
? T extends unknown[]
? DeepPartial<T[number]>[]
: { [P in keyof T]?: DeepPartial<T[P]> }
: T
type Entry<T> = {
[K in keyof T]: [K, T[K]]
}[keyof T]

View file

@ -5,7 +5,13 @@ This package contains the [tldraw](https://tldraw.com) editor as a standalone Re
## Installation
```bash
yarn add @tldraw/tldraw --peer
npm i @tldraw/tldraw
```
or
```bash
yarn add @tldraw/tldraw
```
## Usage

View file

@ -18,13 +18,16 @@ import type { Data } from '~types'
const sortedSelector = (s: Data) =>
Object.values(s.document.pages).sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
const currentPageSelector = (s: Data) => s.document.pages[s.appState.currentPageId]
const currentPageNameSelector = (s: Data) => s.document.pages[s.appState.currentPageId].name
const currentPageIdSelector = (s: Data) => s.document.pages[s.appState.currentPageId].id
export function PagePanel(): JSX.Element {
const rIsOpen = React.useRef(false)
const [isOpen, setIsOpen] = React.useState(false)
const { useSelector } = useTLDrawContext()
const { tlstate, useSelector } = useTLDrawContext()
const rIsOpen = React.useRef(false)
const [isOpen, setIsOpen] = React.useState(false)
React.useEffect(() => {
if (rIsOpen.current !== isOpen) {
@ -32,67 +35,83 @@ export function PagePanel(): JSX.Element {
}
}, [isOpen])
const handleClose = React.useCallback(() => {
setIsOpen(false)
}, [setIsOpen])
const handleOpenChange = React.useCallback(
(isOpen: boolean) => {
if (rIsOpen.current !== isOpen) {
setIsOpen(isOpen)
}
},
[setIsOpen]
)
const currentPageName = useSelector(currentPageNameSelector)
return (
<DropdownMenu.Root dir="ltr" open={isOpen} onOpenChange={handleOpenChange}>
<FloatingContainer>
<RowButton as={DropdownMenu.Trigger} bp={breakpoints} variant="noIcon">
<span>{currentPageName || 'Page'}</span>
</RowButton>
</FloatingContainer>
<MenuContent as={DropdownMenu.Content} sideOffset={8} align="start">
{isOpen && <PageMenuContent onClose={handleClose} />}
</MenuContent>
</DropdownMenu.Root>
)
}
function PageMenuContent({ onClose }: { onClose: () => void }) {
const { tlstate, useSelector } = useTLDrawContext()
const sortedPages = useSelector(sortedSelector)
const currentPageId = useSelector(currentPageIdSelector)
const handleCreatePage = React.useCallback(() => {
tlstate.createPage()
}, [tlstate])
const handleChangePage = React.useCallback(
(id: string) => {
setIsOpen(false)
onClose()
tlstate.changePage(id)
},
[tlstate]
)
const currentPage = useSelector(currentPageSelector)
const sortedPages = useSelector(sortedSelector)
return (
<DropdownMenu.Root
dir="ltr"
open={isOpen}
onOpenChange={(isOpen) => {
if (rIsOpen.current !== isOpen) {
setIsOpen(isOpen)
}
}}
>
<FloatingContainer>
<RowButton as={DropdownMenu.Trigger} bp={breakpoints} variant="noIcon">
<span>{currentPage.name || 'Page'}</span>
</RowButton>
</FloatingContainer>
<MenuContent as={DropdownMenu.Content} sideOffset={8} align="start">
<DropdownMenu.RadioGroup value={currentPage.id} onValueChange={handleChangePage}>
{sortedPages.map((page) => (
<ButtonWithOptions key={page.id}>
<DropdownMenu.RadioItem
as={RowButton}
bp={breakpoints}
value={page.id}
variant="pageButton"
>
<span>{page.name || 'Page'}</span>
<DropdownMenu.ItemIndicator>
<IconWrapper size="small">
<CheckIcon />
</IconWrapper>
</DropdownMenu.ItemIndicator>
</DropdownMenu.RadioItem>
<PageOptionsDialog page={page} />
</ButtonWithOptions>
))}
</DropdownMenu.RadioGroup>
<DropdownMenuDivider />
<DropdownMenuButton onSelect={handleCreatePage}>
<span>Create Page</span>
<IconWrapper size="small">
<PlusIcon />
</IconWrapper>
</DropdownMenuButton>
</MenuContent>
</DropdownMenu.Root>
<>
<DropdownMenu.RadioGroup value={currentPageId} onValueChange={handleChangePage}>
{sortedPages.map((page) => (
<ButtonWithOptions key={page.id}>
<DropdownMenu.RadioItem
as={RowButton}
bp={breakpoints}
value={page.id}
variant="pageButton"
>
<span>{page.name || 'Page'}</span>
<DropdownMenu.ItemIndicator>
<IconWrapper size="small">
<CheckIcon />
</IconWrapper>
</DropdownMenu.ItemIndicator>
</DropdownMenu.RadioItem>
<PageOptionsDialog page={page} />
</ButtonWithOptions>
))}
</DropdownMenu.RadioGroup>
<DropdownMenuDivider />
<DropdownMenuButton onSelect={handleCreatePage}>
<span>Create Page</span>
<IconWrapper size="small">
<PlusIcon />
</IconWrapper>
</DropdownMenuButton>
</>
)
}

View file

@ -53,6 +53,8 @@ export function TLDraw({ document, currentPageId, onMount, onChange: _onChange }
// Hide indicators when not using the select tool, or when in session
const hideIndicators = !isSelecting || isInSession
const meta = React.useMemo(() => ({ isDarkMode }), [isDarkMode])
React.useEffect(() => {
if (!document) return
tlstate.loadDocument(document, _onChange)
@ -92,7 +94,7 @@ export function TLDraw({ document, currentPageId, onMount, onChange: _onChange }
pageState={pageState}
shapeUtils={tldrawShapeUtils}
theme={theme}
isDarkMode={isDarkMode}
meta={meta}
hideBounds={hideBounds}
hideHandles={hideHandles}
hideIndicators={hideIndicators}

View file

@ -4,7 +4,6 @@ import {
Utils,
Vec,
TLTransformInfo,
TLRenderInfo,
Intersect,
TLHandle,
TLPointerInfo,
@ -20,6 +19,7 @@ import {
DashStyle,
TLDrawShape,
ArrowBinding,
TLDrawRenderInfo,
} from '~types'
export class Arrow extends TLDrawShapeUtil<ArrowShape> {
@ -70,7 +70,7 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
return next.handles !== prev.handles || next.style !== prev.style
}
render = (shape: ArrowShape, { isDarkMode }: TLRenderInfo) => {
render = (shape: ArrowShape, { meta }: TLDrawRenderInfo) => {
const {
handles: { start, bend, end },
decorations = {},
@ -79,57 +79,11 @@ export class Arrow extends TLDrawShapeUtil<ArrowShape> {
const isDraw = style.dash === DashStyle.Draw
// if (!isDraw) {
// const styles = getShapeStyle(style, isDarkMode)
// const { strokeWidth } = styles
// const arrowDist = Vec.dist(start.point, end.point)
// const sw = strokeWidth * 1.618
// const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
// arrowDist,
// sw,
// shape.style.dash,
// 2
// )
// const path = getArrowPath(shape)
// return (
// <g pointerEvents="none">
// <path
// d={path}
// fill="none"
// stroke="transparent"
// strokeWidth={Math.max(8, strokeWidth * 2)}
// strokeDasharray="none"
// strokeDashoffset="none"
// strokeLinecap="round"
// strokeLinejoin="round"
// pointerEvents="stroke"
// />
// <path
// d={path}
// fill={isDraw ? styles.stroke : 'none'}
// stroke={styles.stroke}
// strokeWidth={sw}
// strokeDasharray={strokeDasharray}
// strokeDashoffset={strokeDashoffset}
// strokeLinecap="round"
// strokeLinejoin="round"
// pointerEvents="stroke"
// />
// </g>
// )
// }
// TODO: Improve drawn arrows
const isStraightLine = Vec.dist(bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
const styles = getShapeStyle(style, isDarkMode)
const styles = getShapeStyle(style, meta.isDarkMode)
const { strokeWidth } = styles
@ -724,21 +678,6 @@ function renderCurvedFreehandArrowShaft(shape: ArrowShape, circle: number[]) {
return path
}
function getArrowHeadPath(shape: ArrowShape, point: number[], inset: number[]) {
const { left, right } = getArrowHeadPoints(shape, point, inset)
return ['M', left, 'L', point, right].join(' ')
}
function getArrowHeadPoints(shape: ArrowShape, point: number[], inset: number[]) {
// Use the shape's random seed to create minor offsets for the angles
const getRandom = Utils.rng(shape.id)
return {
left: Vec.rotWith(inset, point, Math.PI / 6 + (Math.PI / 12) * getRandom()),
right: Vec.rotWith(inset, point, -Math.PI / 6 + (Math.PI / 12) * getRandom()),
}
}
function getCtp(shape: ArrowShape) {
const { start, end, bend } = shape.handles
return Utils.circleFromThreePoints(start.point, end.point, bend.point)
@ -844,52 +783,67 @@ function getArrowPath(shape: ArrowShape) {
return path.join(' ')
}
function getDrawArrowPath(shape: ArrowShape) {
const {
decorations,
handles: { start, end, bend: _bend },
style,
} = shape
// function getArrowHeadPath(shape: ArrowShape, point: number[], inset: number[]) {
// const { left, right } = getArrowHeadPoints(shape, point, inset)
// return ['M', left, 'L', point, right].join(' ')
// }
const { strokeWidth } = getShapeStyle(style, false)
// function getArrowHeadPoints(shape: ArrowShape, point: number[], inset: number[]) {
// // Use the shape's random seed to create minor offsets for the angles
// const getRandom = Utils.rng(shape.id)
const arrowDist = Vec.dist(start.point, end.point)
// return {
// left: Vec.rotWith(inset, point, Math.PI / 6 + (Math.PI / 12) * getRandom()),
// right: Vec.rotWith(inset, point, -Math.PI / 6 + (Math.PI / 12) * getRandom()),
// }
// }
const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8)
// function getDrawArrowPath(shape: ArrowShape) {
// const {
// decorations,
// handles: { start, end, bend: _bend },
// style,
// } = shape
const path: (string | number)[] = []
// const { strokeWidth } = getShapeStyle(style, false)
const isStraightLine = Vec.dist(_bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
// const arrowDist = Vec.dist(start.point, end.point)
if (isStraightLine) {
// Path (line segment)
path.push(`M ${start.point} L ${end.point}`)
// const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8)
// Start arrow head
if (decorations?.start) {
path.push(getStraightArrowHeadPath(start.point, end.point, arrowHeadLength))
}
// const path: (string | number)[] = []
// End arrow head
if (decorations?.end) {
path.push(getStraightArrowHeadPath(end.point, start.point, arrowHeadLength))
}
} else {
const { center, radius, length } = getArrowArc(shape)
// const isStraightLine = Vec.dist(_bend.point, Vec.round(Vec.med(start.point, end.point))) < 1
// Path (arc)
path.push(`M ${start.point} A ${radius} ${radius} 0 0 ${length > 0 ? '1' : '0'} ${end.point}`)
// if (isStraightLine) {
// // Path (line segment)
// path.push(`M ${start.point} L ${end.point}`)
// Start Arrow head
if (decorations?.start) {
path.push(getCurvedArrowHeadPath(start.point, arrowHeadLength, center, radius, length < 0))
}
// // Start arrow head
// if (decorations?.start) {
// path.push(getStraightArrowHeadPath(start.point, end.point, arrowHeadLength))
// }
// End arrow head
if (decorations?.end) {
path.push(getCurvedArrowHeadPath(end.point, arrowHeadLength, center, radius, length >= 0))
}
}
// // End arrow head
// if (decorations?.end) {
// path.push(getStraightArrowHeadPath(end.point, start.point, arrowHeadLength))
// }
// } else {
// const { center, radius, length } = getArrowArc(shape)
return path.join(' ')
}
// // Path (arc)
// path.push(`M ${start.point} A ${radius} ${radius} 0 0 ${length > 0 ? '1' : '0'} ${end.point}`)
// // Start Arrow head
// if (decorations?.start) {
// path.push(getCurvedArrowHeadPath(start.point, arrowHeadLength, center, radius, length < 0))
// }
// // End arrow head
// if (decorations?.end) {
// path.push(getCurvedArrowHeadPath(end.point, arrowHeadLength, center, radius, length >= 0))
// }
// }
// return path.join(' ')
// }

View file

@ -1,8 +1,15 @@
import * as React from 'react'
import { TLBounds, Utils, Vec, TLTransformInfo, TLRenderInfo, Intersect } from '@tldraw/core'
import { TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core'
import getStroke, { getStrokePoints } from 'perfect-freehand'
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
import { DrawShape, DashStyle, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType } from '~types'
import {
DrawShape,
DashStyle,
TLDrawShapeUtil,
TLDrawShapeType,
TLDrawToolType,
TLDrawRenderInfo,
} from '~types'
export class Draw extends TLDrawShapeUtil<DrawShape> {
type = TLDrawShapeType.Draw as const
@ -30,10 +37,10 @@ export class Draw extends TLDrawShapeUtil<DrawShape> {
return next.points !== prev.points || next.style !== prev.style
}
render(shape: DrawShape, { isDarkMode, isEditing }: TLRenderInfo): JSX.Element {
render(shape: DrawShape, { meta, isEditing }: TLDrawRenderInfo): JSX.Element {
const { points, style } = shape
const styles = getShapeStyle(style, isDarkMode)
const styles = getShapeStyle(style, meta.isDarkMode)
const strokeWidth = styles.strokeWidth

View file

@ -1,6 +1,13 @@
import * as React from 'react'
import { Utils, TLTransformInfo, TLBounds, Intersect, Vec, TLRenderInfo } from '@tldraw/core'
import { DashStyle, EllipseShape, TLDrawShapeType, TLDrawShapeUtil, TLDrawToolType } from '~types'
import { Utils, TLTransformInfo, TLBounds, Intersect, Vec } from '@tldraw/core'
import {
DashStyle,
EllipseShape,
TLDrawRenderInfo,
TLDrawShapeType,
TLDrawShapeUtil,
TLDrawToolType,
} from '~types'
import { defaultStyle, getPerfectDashProps, getShapeStyle } from '~shape/shape-styles'
import getStroke from 'perfect-freehand'
@ -26,13 +33,13 @@ export class Ellipse extends TLDrawShapeUtil<EllipseShape> {
return next.radius !== prev.radius || next.style !== prev.style
}
render(shape: EllipseShape, { isDarkMode, isBinding }: TLRenderInfo) {
render(shape: EllipseShape, { meta, isBinding }: TLDrawRenderInfo) {
const {
radius: [radiusX, radiusY],
style,
} = shape
const styles = getShapeStyle(style, isDarkMode)
const styles = getShapeStyle(style, meta.isDarkMode)
const strokeWidth = +styles.strokeWidth
const rx = Math.max(0, radiusX - strokeWidth / 2)

View file

@ -1,8 +1,15 @@
import * as React from 'react'
import { TLBounds, Utils, Vec, TLTransformInfo, TLRenderInfo, Intersect } from '@tldraw/core'
import { TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core'
import getStroke from 'perfect-freehand'
import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles'
import { RectangleShape, DashStyle, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType } from '~types'
import {
RectangleShape,
DashStyle,
TLDrawShapeUtil,
TLDrawShapeType,
TLDrawToolType,
TLDrawRenderInfo,
} from '~types'
export class Rectangle extends TLDrawShapeUtil<RectangleShape> {
type = TLDrawShapeType.Rectangle as const
@ -27,9 +34,9 @@ export class Rectangle extends TLDrawShapeUtil<RectangleShape> {
return next.size !== prev.size || next.style !== prev.style
}
render(shape: RectangleShape, { isBinding, isDarkMode }: TLRenderInfo) {
render(shape: RectangleShape, { isBinding, meta }: TLDrawRenderInfo) {
const { id, size, style } = shape
const styles = getShapeStyle(style, isDarkMode)
const styles = getShapeStyle(style, meta.isDarkMode)
const strokeWidth = +styles.strokeWidth
if (style.dash === DashStyle.Draw) {

View file

@ -1,7 +1,13 @@
import * as React from 'react'
import { TLBounds, Utils, Vec, TLTransformInfo, TLRenderInfo, Intersect } from '@tldraw/core'
import { TLBounds, Utils, Vec, TLTransformInfo, Intersect } from '@tldraw/core'
import { getShapeStyle, getFontSize, getFontStyle, defaultStyle } from '~shape/shape-styles'
import { TextShape, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType } from '~types'
import {
TextShape,
TLDrawShapeUtil,
TLDrawShapeType,
TLDrawRenderInfo,
TLDrawToolType,
} from '~types'
import styled from '~styles'
import TextAreaUtils from './text-utils'
@ -77,17 +83,17 @@ export class Text extends TLDrawShapeUtil<TextShape> {
shape: TextShape,
{
ref,
meta,
isEditing,
isDarkMode,
onTextBlur,
onTextChange,
onTextFocus,
onTextKeyDown,
onTextKeyUp,
}: TLRenderInfo
}: TLDrawRenderInfo
): JSX.Element {
const { id, text, style } = shape
const styles = getShapeStyle(style, isDarkMode)
const styles = getShapeStyle(style, meta.isDarkMode)
const font = getFontStyle(shape.style)
const bounds = this.getBounds(shape)

View file

@ -1,9 +1,9 @@
import { Utils } from '@tldraw/core'
import { AlignType } from '~types'
import type { Data, Command } from '~types'
import { AlignType, TLDrawCommand } from '~types'
import type { Data } from '~types'
import { TLDR } from '~state/tldr'
export function align(data: Data, ids: string[], type: AlignType): Command {
export function align(data: Data, ids: string[], type: AlignType): TLDrawCommand {
const { currentPageId } = data.appState
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))

View file

@ -1,7 +1,6 @@
import type { TLDrawShape, Data, Command } from '~types'
import { TLDR } from '~state/tldr'
import type { Data, TLDrawCommand } from '~types'
export function changePage(data: Data, pageId: string): Command {
export function changePage(data: Data, pageId: string): TLDrawCommand {
return {
id: 'change_page',
before: {

View file

@ -1,7 +1,7 @@
import type { Data, Command } from '~types'
import type { Data, TLDrawCommand } from '~types'
import { Utils } from '@tldraw/core'
export function createPage(data: Data): Command {
export function createPage(data: Data): TLDrawCommand {
const newId = Utils.uniqueId()
const { currentPageId } = data.appState

View file

@ -1,11 +1,11 @@
import type { DeepPartial } from '~../../core/dist/types/utils/utils'
import type { Patch } from 'rko'
import { TLDR } from '~state/tldr'
import type { TLDrawShape, Data, Command } from '~types'
import type { TLDrawShape, Data, TLDrawCommand } from '~types'
export function create(data: Data, shapes: TLDrawShape[]): Command {
export function create(data: Data, shapes: TLDrawShape[]): TLDrawCommand {
const { currentPageId } = data.appState
const beforeShapes: Record<string, DeepPartial<TLDrawShape> | undefined> = {}
const afterShapes: Record<string, DeepPartial<TLDrawShape> | undefined> = {}
const beforeShapes: Record<string, Patch<TLDrawShape> | undefined> = {}
const afterShapes: Record<string, Patch<TLDrawShape> | undefined> = {}
shapes.forEach((shape) => {
beforeShapes[shape.id] = undefined

View file

@ -1,6 +1,6 @@
import type { Data, Command } from '~types'
import type { Data, TLDrawCommand } from '~types'
export function deletePage(data: Data, pageId: string): Command {
export function deletePage(data: Data, pageId: string): TLDrawCommand {
const { currentPageId } = data.appState
const pagesArr = Object.values(data.document.pages).sort(

View file

@ -1,11 +1,11 @@
import { TLDR } from '~state/tldr'
import type { Data, Command, PagePartial } from '~types'
import type { Data, TLDrawCommand, PagePartial } from '~types'
// - [x] Delete shapes
// - [x] Delete bindings too
// - [ ] Update parents and possibly delete parents
export function deleteShapes(data: Data, ids: string[]): Command {
export function deleteShapes(data: Data, ids: string[]): TLDrawCommand {
const { currentPageId } = data.appState
const before: PagePartial = {

View file

@ -1,8 +1,8 @@
import { Utils } from '@tldraw/core'
import { DistributeType, TLDrawShape, Data, Command } from '~types'
import { DistributeType, TLDrawShape, Data, TLDrawCommand } from '~types'
import { TLDR } from '~state/tldr'
export function distribute(data: Data, ids: string[], type: DistributeType): Command {
export function distribute(data: Data, ids: string[], type: DistributeType): TLDrawCommand {
const { currentPageId } = data.appState
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
const deltaMap = Object.fromEntries(getDistributions(initialShapes, type).map((d) => [d.id, d]))

View file

@ -1,7 +1,7 @@
import type { Data, Command } from '~types'
import type { Data, TLDrawCommand } from '~types'
import { Utils } from '@tldraw/core'
export function duplicatePage(data: Data, pageId: string): Command {
export function duplicatePage(data: Data, pageId: string): TLDrawCommand {
const newId = Utils.uniqueId()
const { currentPageId } = data.appState

View file

@ -1,8 +1,8 @@
import { Utils, Vec } from '@tldraw/core'
import { TLDR } from '~state/tldr'
import type { Data, Command } from '~types'
import type { Data, TLDrawCommand } from '~types'
export function duplicate(data: Data, ids: string[]): Command {
export function duplicate(data: Data, ids: string[]): TLDrawCommand {
const { currentPageId } = data.appState
const delta = Vec.div([16, 16], TLDR.getCamera(data, currentPageId).zoom)

View file

@ -1,9 +1,9 @@
import { FlipType } from '~types'
import { TLBoundsCorner, Utils } from '@tldraw/core'
import type { Data, Command } from '~types'
import type { Data, TLDrawCommand } from '~types'
import { TLDR } from '~state/tldr'
export function flip(data: Data, ids: string[], type: FlipType): Command {
export function flip(data: Data, ids: string[], type: FlipType): TLDrawCommand {
const { currentPageId } = data.appState
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))

View file

@ -1,7 +1,7 @@
import { MoveType, Data, TLDrawShape, Command } from '~types'
import { MoveType, Data, TLDrawShape, TLDrawCommand } from '~types'
import { TLDR } from '~state/tldr'
export function move(data: Data, ids: string[], type: MoveType): Command {
export function move(data: Data, ids: string[], type: MoveType): TLDrawCommand {
const { currentPageId } = data.appState
// Get the unique parent ids for the selected elements

View file

@ -1,6 +1,6 @@
import type { Data, Command } from '~types'
import type { Data, TLDrawCommand } from '~types'
export function renamePage(data: Data, pageId: string, name: string): Command {
export function renamePage(data: Data, pageId: string, name: string): TLDrawCommand {
const page = data.document.pages[pageId]
return {
id: 'rename_page',

View file

@ -1,10 +1,10 @@
import { Utils, Vec } from '@tldraw/core'
import type { Command, Data } from '~types'
import type { TLDrawCommand, Data } from '~types'
import { TLDR } from '~state/tldr'
const PI2 = Math.PI * 2
export function rotate(data: Data, ids: string[], delta = -PI2 / 4): Command {
export function rotate(data: Data, ids: string[], delta = -PI2 / 4): TLDrawCommand {
const { currentPageId } = data.appState
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))

View file

@ -1,9 +1,9 @@
import { TLBoundsCorner, Utils } from '@tldraw/core'
import { StretchType } from '~types'
import type { Data, Command } from '~types'
import type { Data, TLDrawCommand } from '~types'
import { TLDR } from '~state/tldr'
export function stretch(data: Data, ids: string[], type: StretchType): Command {
export function stretch(data: Data, ids: string[], type: StretchType): TLDrawCommand {
const { currentPageId } = data.appState
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))

View file

@ -1,7 +1,7 @@
import type { ShapeStyles, Command, Data } from '~types'
import type { ShapeStyles, TLDrawCommand, Data } from '~types'
import { TLDR } from '~state/tldr'
export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>): Command {
export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>): TLDrawCommand {
const { currentPageId } = data.appState
const { before, after } = TLDR.mutateShapes(

View file

@ -1,8 +1,12 @@
import { Decoration } from '~types'
import type { ArrowShape, Command, Data } from '~types'
import type { ArrowShape, TLDrawCommand, Data } from '~types'
import { TLDR } from '~state/tldr'
export function toggleDecoration(data: Data, ids: string[], handleId: 'start' | 'end'): Command {
export function toggleDecoration(
data: Data,
ids: string[],
handleId: 'start' | 'end'
): TLDrawCommand {
const { currentPageId } = data.appState
const { before, after } = TLDR.mutateShapes<ArrowShape>(
data,

View file

@ -1,7 +1,7 @@
import type { TLDrawShape, Data, Command } from '~types'
import type { TLDrawShape, Data, TLDrawCommand } from '~types'
import { TLDR } from '~state/tldr'
export function toggle(data: Data, ids: string[], prop: keyof TLDrawShape): Command {
export function toggle(data: Data, ids: string[], prop: keyof TLDrawShape): TLDrawCommand {
const { currentPageId } = data.appState
const initialShapes = ids.map((id) => TLDR.getShape(data, id, currentPageId))
const isAllToggled = initialShapes.every((shape) => shape[prop])

View file

@ -1,8 +1,8 @@
import { Vec } from '@tldraw/core'
import type { Data, Command, PagePartial } from '~types'
import type { Data, TLDrawCommand, PagePartial } from '~types'
import { TLDR } from '~state/tldr'
export function translate(data: Data, ids: string[], delta: number[]): Command {
export function translate(data: Data, ids: string[], delta: number[]): TLDrawCommand {
const before: PagePartial = {
shapes: {},
bindings: {},

View file

@ -1,8 +1,7 @@
import { brushUpdater, Utils, Vec } from '@tldraw/core'
import { Data, Session, TLDrawStatus } from '~types'
import { Data, Session, TLDrawPatch, TLDrawStatus } from '~types'
import { getShapeUtils } from '~shape'
import { TLDR } from '~state/tldr'
import type { DeepPartial } from '~../../core/dist/types/utils/utils'
export class BrushSession implements Session {
id = 'brush'
@ -17,7 +16,7 @@ export class BrushSession implements Session {
start = () => void null
update = (data: Data, point: number[], containMode = false): DeepPartial<Data> => {
update = (data: Data, point: number[], containMode = false): TLDrawPatch => {
const { snapshot, origin } = this
const { currentPageId } = data.appState

View file

@ -1,5 +1,5 @@
import { TLPageState, Utils, Vec } from '@tldraw/core'
import { TLDrawShape, TLDrawBinding, Session, Data, Command, TLDrawStatus } from '~types'
import { TLDrawShape, TLDrawBinding, Session, Data, TLDrawCommand, TLDrawStatus } from '~types'
import { TLDR } from '~state/tldr'
export class TranslateSession implements Session {
@ -184,7 +184,7 @@ export class TranslateSession implements Session {
}
}
complete(data: Data): Command {
complete(data: Data): TLDrawCommand {
const pageId = data.appState.currentPageId
const { initialShapes, bindingsToDelete, clones, clonedBindings } = this.snapshot

View file

@ -2,13 +2,14 @@ import { TLBounds, TLTransformInfo, Vec, Utils, TLPageState } from '@tldraw/core
import { getShapeUtils } from '~shape'
import type {
Data,
DeepPartial,
ShapeStyles,
ShapesWithProp,
TLDrawShape,
TLDrawShapeUtil,
TLDrawBinding,
TLDrawPage,
TLDrawCommand,
TLDrawPatch,
} from '~types'
export class TLDR {
@ -445,12 +446,8 @@ export class TLDR {
}
}
static createShapes(
data: Data,
shapes: TLDrawShape[],
pageId: string
): { before: DeepPartial<Data>; after: DeepPartial<Data> } {
const before: DeepPartial<Data> = {
static createShapes(data: Data, shapes: TLDrawShape[], pageId: string): TLDrawCommand {
const before: TLDrawPatch = {
document: {
pages: {
[pageId]: {
@ -477,7 +474,7 @@ export class TLDR {
},
}
const after: DeepPartial<Data> = {
const after: TLDrawPatch = {
document: {
pages: {
[pageId]: {
@ -516,7 +513,7 @@ export class TLDR {
data: Data,
shapes: TLDrawShape[] | string[],
pageId?: string
): { before: DeepPartial<Data>; after: DeepPartial<Data> } {
): TLDrawCommand {
pageId = pageId ? pageId : data.appState.currentPageId
const page = this.getPage(data, pageId)
@ -526,7 +523,7 @@ export class TLDR {
? (shapes as string[])
: (shapes as TLDrawShape[]).map((shape) => shape.id)
const before: DeepPartial<Data> = {
const before: TLDrawPatch = {
document: {
pages: {
[pageId]: {
@ -565,7 +562,7 @@ export class TLDR {
},
}
const after: DeepPartial<Data> = {
const after: TLDrawPatch = {
document: {
pages: {
[pageId]: {

View file

@ -1,9 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/ban-types */
import type { TLBinding } from '@tldraw/core'
import type { TLBinding, TLRenderInfo } from '@tldraw/core'
import { TLShape, TLShapeUtil, TLHandle } from '@tldraw/core'
import type { TLPage, TLPageState, TLSettings } from '@tldraw/core'
import type { TLPage, TLPageState } from '@tldraw/core'
import type { StoreApi } from 'zustand'
import type { Command, Patch } from 'rko'
export type TLStore = StoreApi<Data>
@ -17,12 +18,21 @@ export interface TLDrawDocument {
pageStates: Record<string, TLPageState>
}
export interface TLDrawSettings extends TLSettings {
export interface TLDrawSettings {
isDarkMode: boolean
isDebugMode: boolean
isPenMode: boolean
isReadonlyMode: boolean
nudgeDistanceSmall: number
nudgeDistanceLarge: number
}
export interface TLDrawMeta {
isDarkMode: boolean
}
export type TLDrawRenderInfo = TLRenderInfo<TLDrawMeta>
export interface Data {
document: TLDrawDocument
settings: TLDrawSettings
@ -41,30 +51,13 @@ export interface Data {
}
}
export type TLDrawPatch = DeepPartial<Data>
export type TLDrawPatch = Patch<Data>
export type TLDrawCommand = Command<Data>
export type PagePartial = {
shapes: DeepPartial<TLDrawPage['shapes']>
bindings: DeepPartial<TLDrawPage['bindings']>
}
export type DeepPartial<T> = T extends Function
? T
: T extends object
? T extends unknown[]
? DeepPartial<T[number]>[]
: { [P in keyof T]?: DeepPartial<T[P]> }
: T
export interface Command {
id: string
before: TLDrawPatch
after: TLDrawPatch
}
export interface History {
pointer: number
stack: Command[]
shapes: Patch<TLDrawPage['shapes']>
bindings: Patch<TLDrawPage['bindings']>
}
export interface SelectHistory {
@ -77,7 +70,7 @@ export interface Session {
status: TLDrawStatus
start: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | undefined
update: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | undefined
complete: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | Command | undefined
complete: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | TLDrawCommand | undefined
cancel: (data: Readonly<Data>, ...args: any[]) => TLDrawPatch | undefined
}

View file

@ -6,6 +6,7 @@
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"importsNotUsedAsValues": "error",
"stripInternal": true,
"incremental": true,
"importHelpers": true,
"moduleResolution": "node",

File diff suppressed because one or more lines are too long