Changes usePosition to useLayoutEffect (#91)
This commit is contained in:
parent
f5b7190010
commit
17a7b15f9a
17 changed files with 573 additions and 69 deletions
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as React from 'react'
|
||||
import type { TLBounds } from '+types'
|
||||
import { useBoundsEvents, usePosition } from '+hooks'
|
||||
import { useBoundsEvents } from '+hooks'
|
||||
import { Container } from '+components/container'
|
||||
import { SVGContainer } from '+components/svg-container'
|
||||
|
||||
|
|
|
@ -12,10 +12,10 @@ interface ContainerProps {
|
|||
|
||||
export const Container = React.memo(
|
||||
({ id, bounds, rotation = 0, className, children }: ContainerProps) => {
|
||||
const rBounds = usePosition(bounds, rotation)
|
||||
const rPositioned = usePosition(bounds, rotation)
|
||||
|
||||
return (
|
||||
<div id={id} ref={rBounds} className={['tl-positioned', className || ''].join(' ')}>
|
||||
<div id={id} ref={rPositioned} className={['tl-positioned', className || ''].join(' ')}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -68,13 +68,14 @@ export function Page<T extends TLShape, M extends Record<string, unknown>>({
|
|||
selectedIds
|
||||
.filter(Boolean)
|
||||
.map((id) => (
|
||||
<ShapeIndicator key={'selected_' + id} shape={page.shapes[id]} variant="selected" />
|
||||
<ShapeIndicator key={'selected_' + id} shape={page.shapes[id]} meta={meta} isSelected />
|
||||
))}
|
||||
{!hideIndicators && hoveredId && (
|
||||
<ShapeIndicator
|
||||
key={'hovered_' + hoveredId}
|
||||
shape={page.shapes[hoveredId]}
|
||||
variant="hovered"
|
||||
meta={meta}
|
||||
isHovered
|
||||
/>
|
||||
)}
|
||||
{!hideHandles && shapeWithHandles && <Handles shape={shapeWithHandles} />}
|
||||
|
|
|
@ -5,7 +5,12 @@ import { ShapeIndicator } from './shape-indicator'
|
|||
describe('shape indicator', () => {
|
||||
test('mounts component without crashing', () => {
|
||||
renderWithSvg(
|
||||
<ShapeIndicator shape={mockUtils.box.create({ id: 'box1' })} variant={'selected'} />
|
||||
<ShapeIndicator
|
||||
shape={mockUtils.box.create({ id: 'box1' })}
|
||||
isSelected={true}
|
||||
isHovered={false}
|
||||
meta={undefined}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,24 +1,35 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import * as React from 'react'
|
||||
import type { TLShape } from '+types'
|
||||
import { usePosition, useTLContext } from '+hooks'
|
||||
|
||||
interface IndicatorProps<T extends TLShape, M = any> {
|
||||
shape: T
|
||||
meta: M extends any ? M : undefined
|
||||
isSelected?: boolean
|
||||
isHovered?: boolean
|
||||
}
|
||||
|
||||
export const ShapeIndicator = React.memo(
|
||||
({ shape, variant }: { shape: TLShape; variant: 'selected' | 'hovered' }) => {
|
||||
<T extends TLShape, M = any>({ isHovered, isSelected, shape, meta }: IndicatorProps<T, M>) => {
|
||||
const { shapeUtils } = useTLContext()
|
||||
const utils = shapeUtils[shape.type]
|
||||
const bounds = utils.getBounds(shape)
|
||||
const rBounds = usePosition(bounds, shape.rotation)
|
||||
const rPositioned = usePosition(bounds, shape.rotation)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rBounds}
|
||||
className={
|
||||
'tl-indicator tl-absolute ' + (variant === 'selected' ? 'tl-selected' : 'tl-hovered')
|
||||
}
|
||||
ref={rPositioned}
|
||||
className={'tl-indicator tl-absolute ' + (isSelected ? 'tl-selected' : 'tl-hovered')}
|
||||
>
|
||||
<svg width="100%" height="100%">
|
||||
<g className="tl-centered-g">
|
||||
<utils.Indicator shape={shape} />
|
||||
<utils.Indicator
|
||||
shape={shape}
|
||||
meta={meta}
|
||||
isSelected={isSelected}
|
||||
isHovered={isHovered}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as React from 'react'
|
||||
import { useShapeEvents } from '+hooks'
|
||||
|
@ -5,39 +6,44 @@ import type { IShapeTreeNode, TLShape, TLShapeUtil } from '+types'
|
|||
import { RenderedShape } from './rendered-shape'
|
||||
import { Container } from '+components/container'
|
||||
import { useTLContext } from '+hooks'
|
||||
import { useForceUpdate } from '+hooks/useForceUpdate'
|
||||
|
||||
interface ShapeProps<T extends TLShape, E extends Element, M> extends IShapeTreeNode<T, M> {
|
||||
utils: TLShapeUtil<T, E, M>
|
||||
}
|
||||
|
||||
export const Shape = <T extends TLShape, E extends Element, M>({
|
||||
shape,
|
||||
utils,
|
||||
isEditing,
|
||||
isBinding,
|
||||
isHovered,
|
||||
isSelected,
|
||||
isCurrentParent,
|
||||
meta,
|
||||
}: ShapeProps<T, E, M>) => {
|
||||
const { callbacks } = useTLContext()
|
||||
const bounds = utils.getBounds(shape)
|
||||
const events = useShapeEvents(shape.id, isCurrentParent)
|
||||
export const Shape = React.memo(
|
||||
<T extends TLShape, E extends Element, M>({
|
||||
shape,
|
||||
utils,
|
||||
isEditing,
|
||||
isBinding,
|
||||
isHovered,
|
||||
isSelected,
|
||||
isCurrentParent,
|
||||
meta,
|
||||
}: ShapeProps<T, E, M>) => {
|
||||
const { callbacks } = useTLContext()
|
||||
const bounds = utils.getBounds(shape)
|
||||
const events = useShapeEvents(shape.id, isCurrentParent)
|
||||
|
||||
return (
|
||||
<Container id={shape.id} className="tl-shape" bounds={bounds} rotation={shape.rotation}>
|
||||
<RenderedShape
|
||||
shape={shape}
|
||||
isBinding={isBinding}
|
||||
isCurrentParent={isCurrentParent}
|
||||
isEditing={isEditing}
|
||||
isHovered={isHovered}
|
||||
isSelected={isSelected}
|
||||
utils={utils as any}
|
||||
meta={meta}
|
||||
events={events}
|
||||
onShapeChange={callbacks.onShapeChange}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
useForceUpdate()
|
||||
|
||||
return (
|
||||
<Container id={shape.id} className="tl-shape" bounds={bounds} rotation={shape.rotation}>
|
||||
<RenderedShape
|
||||
shape={shape}
|
||||
isBinding={isBinding}
|
||||
isCurrentParent={isCurrentParent}
|
||||
isEditing={isEditing}
|
||||
isHovered={isHovered}
|
||||
isSelected={isSelected}
|
||||
utils={utils as any}
|
||||
meta={meta}
|
||||
events={events}
|
||||
onShapeChange={callbacks.onShapeChange}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
6
packages/core/src/hooks/useForceUpdate.ts
Normal file
6
packages/core/src/hooks/useForceUpdate.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function useForceUpdate() {
|
||||
const forceUpdate = React.useReducer((s) => s + 1, 0)
|
||||
React.useLayoutEffect(() => forceUpdate[1](), [])
|
||||
}
|
|
@ -5,18 +5,20 @@ import type { TLBounds } from '+types'
|
|||
export function usePosition(bounds: TLBounds, rotation = 0) {
|
||||
const rBounds = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
React.useLayoutEffect(() => {
|
||||
const elm = rBounds.current!
|
||||
const transform = `
|
||||
translate(calc(${bounds.minX}px - var(--tl-padding)),calc(${bounds.minY}px - var(--tl-padding)))
|
||||
rotate(${rotation + (bounds.rotation || 0)}rad)`
|
||||
elm.style.setProperty('transform', transform)
|
||||
|
||||
elm.style.setProperty('width', `calc(${Math.floor(bounds.width)}px + (var(--tl-padding) * 2))`)
|
||||
|
||||
elm.style.setProperty(
|
||||
'height',
|
||||
`calc(${Math.floor(bounds.height)}px + (var(--tl-padding) * 2))`
|
||||
)
|
||||
}, [rBounds, bounds, rotation])
|
||||
}, [bounds, rotation])
|
||||
|
||||
return rBounds
|
||||
}
|
||||
|
|
|
@ -150,6 +150,7 @@ const tlcss = css`
|
|||
left: 0;
|
||||
height: 0;
|
||||
width: 0;
|
||||
contain: layout size;
|
||||
transform: scale(var(--tl-zoom), var(--tl-zoom))
|
||||
translate(var(--tl-camera-x), var(--tl-camera-y));
|
||||
}
|
||||
|
@ -171,6 +172,7 @@ const tlcss = css`
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: clip;
|
||||
contain: layout size paint;
|
||||
}
|
||||
|
||||
.tl-positioned-svg {
|
||||
|
@ -319,18 +321,6 @@ const tlcss = css`
|
|||
stroke: var(--tl-selected);
|
||||
}
|
||||
|
||||
.tl-shape {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tl-shape > *[data-shy='true'] {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tl-shape:hover > *[data-shy='true'] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tl-centered-g {
|
||||
transform: translate(var(--tl-padding), var(--tl-padding));
|
||||
}
|
||||
|
|
|
@ -113,6 +113,7 @@ export type TLWheelEventHandler = (
|
|||
info: TLPointerInfo<string>,
|
||||
e: React.WheelEvent<Element> | WheelEvent
|
||||
) => void
|
||||
|
||||
export type TLPinchEventHandler = (
|
||||
info: TLPointerInfo<string>,
|
||||
e:
|
||||
|
@ -123,9 +124,20 @@ export type TLPinchEventHandler = (
|
|||
| React.PointerEvent<Element>
|
||||
| PointerEventInit
|
||||
) => void
|
||||
|
||||
export type TLShapeChangeHandler<T, K = any> = (
|
||||
shape: { id: string } & Partial<T>,
|
||||
info?: K
|
||||
) => void
|
||||
|
||||
export type TLShapeBlurHandler<K = any> = (info?: K) => void
|
||||
|
||||
export type TLPointerEventHandler = (info: TLPointerInfo<string>, e: React.PointerEvent) => void
|
||||
|
||||
export type TLCanvasEventHandler = (info: TLPointerInfo<'canvas'>, e: React.PointerEvent) => void
|
||||
|
||||
export type TLBoundsEventHandler = (info: TLPointerInfo<'bounds'>, e: React.PointerEvent) => void
|
||||
|
||||
export type TLBoundsHandleEventHandler = (
|
||||
info: TLPointerInfo<TLBoundsCorner | TLBoundsEdge | 'rotate'>,
|
||||
e: React.PointerEvent
|
||||
|
@ -188,9 +200,9 @@ export interface TLCallbacks<T extends TLShape> {
|
|||
onReleaseHandle: TLPointerEventHandler
|
||||
|
||||
// Misc
|
||||
onShapeChange: TLShapeChangeHandler<T, any>
|
||||
onShapeBlur: TLShapeBlurHandler<any>
|
||||
onRenderCountChange: (ids: string[]) => void
|
||||
onShapeChange: (shape: { id: string } & Partial<T>) => void
|
||||
onShapeBlur: () => void
|
||||
onError: (error: Error) => void
|
||||
}
|
||||
|
||||
|
@ -287,7 +299,10 @@ export type TLShapeUtil<
|
|||
ref: React.ForwardedRef<E>
|
||||
): React.ReactElement<TLRenderInfo<T, E, M>, E['tagName']>
|
||||
|
||||
Indicator(this: TLShapeUtil<T, E, M>, props: { shape: T }): React.ReactElement | null
|
||||
Indicator(
|
||||
this: TLShapeUtil<T, E, M>,
|
||||
props: { shape: T; meta: M; isHovered: boolean; isSelected: boolean }
|
||||
): React.ReactElement | null
|
||||
|
||||
getBounds(this: TLShapeUtil<T, E, M>, shape: T): TLBounds
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import Controlled from './controlled'
|
|||
import Imperative from './imperative'
|
||||
import Embedded from './embedded'
|
||||
import ChangingId from './changing-id'
|
||||
import Core from './core'
|
||||
import './styles.css'
|
||||
|
||||
export default function App(): JSX.Element {
|
||||
|
@ -14,6 +15,9 @@ export default function App(): JSX.Element {
|
|||
<Route path="/basic">
|
||||
<Basic />
|
||||
</Route>
|
||||
<Route path="/core">
|
||||
<Core />
|
||||
</Route>
|
||||
<Route path="/controlled">
|
||||
<Controlled />
|
||||
</Route>
|
||||
|
@ -31,6 +35,9 @@ export default function App(): JSX.Element {
|
|||
<li>
|
||||
<Link to="/basic">basic</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/core">core</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/controlled">controlled</Link>
|
||||
</li>
|
||||
|
|
35
packages/dev/src/core/index.tsx
Normal file
35
packages/dev/src/core/index.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import * as React from 'react'
|
||||
import { Renderer } from '@tldraw/core'
|
||||
import { Rectangle } from './rectangle'
|
||||
import { Label } from './label'
|
||||
import { appState } from './state'
|
||||
|
||||
const shapeUtils: any = {
|
||||
rectangle: Rectangle,
|
||||
label: Label,
|
||||
}
|
||||
|
||||
export default function Core() {
|
||||
const page = appState.useStore((s) => s.page)
|
||||
const pageState = appState.useStore((s) => s.pageState)
|
||||
const meta = appState.useStore((s) => s.meta)
|
||||
|
||||
return (
|
||||
<div className="tldraw">
|
||||
<Renderer
|
||||
shapeUtils={shapeUtils}
|
||||
page={page}
|
||||
pageState={pageState}
|
||||
meta={meta}
|
||||
onDoubleClickBounds={appState.onDoubleClickBounds}
|
||||
onDoubleClickShape={appState.onDoubleClickShape}
|
||||
onPointShape={appState.onPointShape}
|
||||
onPointCanvas={appState.onPointCanvas}
|
||||
onPointerDown={appState.onPointerDown}
|
||||
onPointerMove={appState.onPointerMove}
|
||||
onPointerUp={appState.onPointerUp}
|
||||
onShapeChange={appState.onShapeChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
128
packages/dev/src/core/label.tsx
Normal file
128
packages/dev/src/core/label.tsx
Normal file
|
@ -0,0 +1,128 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* refresh-reset */
|
||||
|
||||
import * as React from 'react'
|
||||
import { TLShape, Utils, TLBounds, ShapeUtil, HTMLContainer } from '@tldraw/core'
|
||||
import { appState } from './state'
|
||||
|
||||
// Define a custom shape
|
||||
|
||||
export interface LabelShape extends TLShape {
|
||||
type: 'label'
|
||||
text: string
|
||||
}
|
||||
|
||||
// Create a "shape utility" class that interprets that shape
|
||||
|
||||
export const Label = new ShapeUtil<LabelShape, HTMLDivElement, { isDarkMode: boolean }>(() => ({
|
||||
type: 'label',
|
||||
|
||||
defaultProps: {
|
||||
id: 'example1',
|
||||
type: 'label',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
name: 'Example Shape',
|
||||
point: [0, 0],
|
||||
rotation: 0,
|
||||
text: 'Hello world!',
|
||||
},
|
||||
|
||||
Component({ shape, events, meta, onShapeChange, isSelected }, ref) {
|
||||
const color = meta.isDarkMode ? 'white' : 'black'
|
||||
|
||||
const bounds = this.getBounds(shape)
|
||||
|
||||
const rInput = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
function updateShapeSize() {
|
||||
const elm = rInput.current!
|
||||
|
||||
appState.changeShapeText(shape.id, elm.innerText)
|
||||
|
||||
onShapeChange?.({
|
||||
id: shape.id,
|
||||
text: elm.innerText,
|
||||
})
|
||||
}
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const elm = rInput.current!
|
||||
elm.innerText = shape.text
|
||||
updateShapeSize()
|
||||
const observer = new MutationObserver(updateShapeSize)
|
||||
|
||||
observer.observe(elm, {
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<HTMLContainer>
|
||||
<div
|
||||
{...events}
|
||||
style={{
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
pointerEvents: 'all',
|
||||
display: 'flex',
|
||||
fontSize: 20,
|
||||
fontFamily: 'sans-serif',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: `2px solid ${color}`,
|
||||
color,
|
||||
}}
|
||||
>
|
||||
<div ref={ref} onPointerDown={(e) => isSelected && e.stopPropagation()}>
|
||||
<div
|
||||
ref={rInput}
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textAlign: 'center',
|
||||
outline: 'none',
|
||||
userSelect: isSelected ? 'all' : 'none',
|
||||
}}
|
||||
contentEditable={isSelected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HTMLContainer>
|
||||
)
|
||||
},
|
||||
Indicator({ shape }) {
|
||||
const bounds = this?.getBounds(shape)
|
||||
|
||||
return (
|
||||
<rect
|
||||
fill="none"
|
||||
stroke="blue"
|
||||
strokeWidth={1}
|
||||
width={bounds.width}
|
||||
height={bounds.height}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
)
|
||||
},
|
||||
getBounds(shape) {
|
||||
const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
|
||||
const ref = this.getRef(shape)
|
||||
const width = ref.current?.offsetWidth || 0
|
||||
const height = ref.current?.offsetHeight || 0
|
||||
|
||||
return {
|
||||
minX: 0,
|
||||
maxX: width,
|
||||
minY: 0,
|
||||
maxY: height,
|
||||
width,
|
||||
height,
|
||||
} as TLBounds
|
||||
})
|
||||
|
||||
return Utils.translateBounds(bounds, shape.point)
|
||||
},
|
||||
}))
|
133
packages/dev/src/core/rectangle.tsx
Normal file
133
packages/dev/src/core/rectangle.tsx
Normal file
|
@ -0,0 +1,133 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* refresh-reset */
|
||||
|
||||
import * as React from 'react'
|
||||
import { TLShape, Utils, TLBounds, ShapeUtil, HTMLContainer } from '@tldraw/core'
|
||||
|
||||
// Define a custom shape
|
||||
|
||||
export interface RectangleShape extends TLShape {
|
||||
type: 'rectangle'
|
||||
size: number[]
|
||||
text: string
|
||||
}
|
||||
|
||||
// Create a "shape utility" class that interprets that shape
|
||||
|
||||
export const Rectangle = new ShapeUtil<RectangleShape, HTMLDivElement, { isDarkMode: boolean }>(
|
||||
() => ({
|
||||
type: 'rectangle',
|
||||
defaultProps: {
|
||||
id: 'example1',
|
||||
type: 'rectangle',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
name: 'Example Shape',
|
||||
point: [0, 0],
|
||||
size: [100, 100],
|
||||
rotation: 0,
|
||||
text: 'Hello world!',
|
||||
},
|
||||
Component({ shape, events, meta, onShapeChange, isEditing }, ref) {
|
||||
const color = meta.isDarkMode ? 'white' : 'black'
|
||||
|
||||
const rInput = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
function updateShapeSize() {
|
||||
const elm = rInput.current!
|
||||
|
||||
onShapeChange?.({
|
||||
...shape,
|
||||
text: elm.innerText,
|
||||
size: [elm.offsetWidth + 44, elm.offsetHeight + 44],
|
||||
})
|
||||
}
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const elm = rInput.current!
|
||||
|
||||
const observer = new MutationObserver(updateShapeSize)
|
||||
|
||||
observer.observe(elm, {
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
})
|
||||
|
||||
elm.innerText = shape.text
|
||||
updateShapeSize()
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isEditing) {
|
||||
rInput.current!.focus()
|
||||
}
|
||||
}, [isEditing])
|
||||
|
||||
return (
|
||||
<HTMLContainer ref={ref}>
|
||||
<div
|
||||
{...events}
|
||||
style={{
|
||||
pointerEvents: 'all',
|
||||
width: shape.size[0],
|
||||
height: shape.size[1],
|
||||
display: 'flex',
|
||||
fontSize: 20,
|
||||
fontFamily: 'sans-serif',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: `2px solid ${color}`,
|
||||
color,
|
||||
}}
|
||||
>
|
||||
<div onPointerDown={(e) => isEditing && e.stopPropagation()}>
|
||||
<div
|
||||
ref={rInput}
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textAlign: 'center',
|
||||
outline: 'none',
|
||||
userSelect: isEditing ? 'all' : 'none',
|
||||
}}
|
||||
contentEditable={isEditing}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HTMLContainer>
|
||||
)
|
||||
},
|
||||
Indicator({ shape }) {
|
||||
return (
|
||||
<rect
|
||||
fill="none"
|
||||
stroke="blue"
|
||||
strokeWidth={1}
|
||||
width={shape.size[0]}
|
||||
height={shape.size[1]}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
)
|
||||
},
|
||||
getBounds(shape) {
|
||||
const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
|
||||
const [width, height] = shape.size
|
||||
return {
|
||||
minX: 0,
|
||||
maxX: width,
|
||||
minY: 0,
|
||||
maxY: height,
|
||||
width,
|
||||
height,
|
||||
} as TLBounds
|
||||
})
|
||||
|
||||
return Utils.translateBounds(bounds, shape.point)
|
||||
},
|
||||
})
|
||||
)
|
143
packages/dev/src/core/state.ts
Normal file
143
packages/dev/src/core/state.ts
Normal file
|
@ -0,0 +1,143 @@
|
|||
import type {
|
||||
TLBinding,
|
||||
TLPage,
|
||||
TLPageState,
|
||||
TLPointerEventHandler,
|
||||
TLShapeChangeHandler,
|
||||
} from '@tldraw/core'
|
||||
import type { RectangleShape } from './rectangle'
|
||||
import type { LabelShape } from './label'
|
||||
import { StateManager } from 'rko'
|
||||
|
||||
type Shapes = RectangleShape | LabelShape
|
||||
|
||||
interface State {
|
||||
page: TLPage<Shapes, TLBinding>
|
||||
pageState: TLPageState
|
||||
meta: {
|
||||
isDarkMode: boolean
|
||||
}
|
||||
}
|
||||
|
||||
class AppState extends StateManager<State> {
|
||||
/* ----------------------- API ---------------------- */
|
||||
|
||||
selectShape(shapeId: string) {
|
||||
this.patchState({
|
||||
pageState: {
|
||||
selectedIds: [shapeId],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
deselect() {
|
||||
this.patchState({
|
||||
pageState: {
|
||||
selectedIds: [],
|
||||
editingId: undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
startEditingShape(shapeId: string) {
|
||||
this.patchState({
|
||||
pageState: {
|
||||
selectedIds: [shapeId],
|
||||
editingId: shapeId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
changeShapeText = (id: string, text: string) => {
|
||||
this.patchState({
|
||||
page: {
|
||||
shapes: {
|
||||
[id]: { text },
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/* --------------------- Events --------------------- */
|
||||
|
||||
onPointCanvas: TLPointerEventHandler = (info) => {
|
||||
this.deselect()
|
||||
}
|
||||
|
||||
onPointShape: TLPointerEventHandler = (info) => {
|
||||
this.selectShape(info.target)
|
||||
}
|
||||
|
||||
onDoubleClickShape: TLPointerEventHandler = (info) => {
|
||||
this.startEditingShape(info.target)
|
||||
}
|
||||
|
||||
onDoubleClickBounds: TLPointerEventHandler = (info) => {
|
||||
// Todo
|
||||
}
|
||||
|
||||
onPointerDown: TLPointerEventHandler = (info) => {
|
||||
// Todo
|
||||
}
|
||||
|
||||
onPointerUp: TLPointerEventHandler = (info) => {
|
||||
// Todo
|
||||
}
|
||||
|
||||
onPointerMove: TLPointerEventHandler = (info) => {
|
||||
// Todo
|
||||
}
|
||||
|
||||
onShapeChange: TLShapeChangeHandler<Shapes> = (shape) => {
|
||||
if (shape.type === 'rectangle' && shape.size) {
|
||||
this.patchState({
|
||||
page: {
|
||||
shapes: {
|
||||
[shape.id]: { ...shape, size: [...shape.size] },
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const appState = new AppState({
|
||||
page: {
|
||||
id: 'page1',
|
||||
shapes: {
|
||||
rect1: {
|
||||
id: 'rect1',
|
||||
parentId: 'page1',
|
||||
name: 'Rectangle',
|
||||
childIndex: 1,
|
||||
type: 'rectangle',
|
||||
point: [0, 0],
|
||||
rotation: 0,
|
||||
size: [100, 100],
|
||||
text: 'Hello world!',
|
||||
},
|
||||
label1: {
|
||||
id: 'label1',
|
||||
parentId: 'page1',
|
||||
name: 'Label',
|
||||
childIndex: 2,
|
||||
type: 'label',
|
||||
point: [200, 200],
|
||||
rotation: 0,
|
||||
text: 'Hello world!',
|
||||
},
|
||||
},
|
||||
bindings: {},
|
||||
},
|
||||
pageState: {
|
||||
id: 'page1',
|
||||
selectedIds: [],
|
||||
camera: {
|
||||
point: [0, 0],
|
||||
zoom: 1,
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
isDarkMode: false,
|
||||
},
|
||||
})
|
|
@ -7,10 +7,12 @@ import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
|||
import { DrawShape, DashStyle, TLDrawShapeType, TLDrawToolType, TLDrawMeta } from '~types'
|
||||
|
||||
const pointsBoundsCache = new WeakMap<DrawShape['points'], TLBounds>([])
|
||||
const shapeBoundsCache = new Map<string, TLBounds>()
|
||||
const rotatedCache = new WeakMap<DrawShape, number[][]>([])
|
||||
const drawPathCache = new WeakMap<DrawShape['points'], string>([])
|
||||
const simplePathCache = new WeakMap<DrawShape['points'], string>([])
|
||||
const polygonCache = new WeakMap<DrawShape['points'], string>([])
|
||||
const pointCache = new WeakSet<DrawShape['point']>([])
|
||||
|
||||
export const Draw = new ShapeUtil<DrawShape, SVGSVGElement, TLDrawMeta>(() => ({
|
||||
type: TLDrawShapeType.Draw,
|
||||
|
@ -159,12 +161,32 @@ export const Draw = new ShapeUtil<DrawShape, SVGSVGElement, TLDrawMeta>(() => ({
|
|||
},
|
||||
|
||||
getBounds(shape: DrawShape): TLBounds {
|
||||
return Utils.translateBounds(
|
||||
Utils.getFromCache(pointsBoundsCache, shape.points, () =>
|
||||
Utils.getBoundsFromPoints(shape.points)
|
||||
),
|
||||
shape.point
|
||||
)
|
||||
// The goal here is to avoid recalculating the bounds from the
|
||||
// points array, which is expensive. However, we still need a
|
||||
// new bounds if the point has changed, but we will reuse the
|
||||
// previous bounds-from-points result if we can.
|
||||
|
||||
const pointsHaveChanged = !pointsBoundsCache.has(shape.points)
|
||||
const pointHasChanged = !pointCache.has(shape.point)
|
||||
|
||||
if (pointsHaveChanged) {
|
||||
// If the points have changed, then bust the points cache
|
||||
const bounds = Utils.getBoundsFromPoints(shape.points)
|
||||
pointsBoundsCache.set(shape.points, bounds)
|
||||
shapeBoundsCache.set(shape.id, Utils.translateBounds(bounds, shape.point))
|
||||
pointCache.add(shape.point)
|
||||
} else if (pointHasChanged && !pointsHaveChanged) {
|
||||
// If the point have has changed, then bust the point cache
|
||||
pointCache.add(shape.point)
|
||||
shapeBoundsCache.set(
|
||||
shape.id,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
Utils.translateBounds(pointsBoundsCache.get(shape.points)!, shape.point)
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return shapeBoundsCache.get(shape.id)!
|
||||
},
|
||||
|
||||
shouldRender(prev: DrawShape, next: DrawShape): boolean {
|
||||
|
|
|
@ -90,6 +90,9 @@ export class DrawSession implements Session {
|
|||
// Don't add duplicate points.
|
||||
if (Vec.isEqual(this.last, newPoint)) return
|
||||
|
||||
// Add the new adjusted point to the points array
|
||||
this.points.push(newPoint)
|
||||
|
||||
// The new adjusted point is now the previous adjusted point.
|
||||
this.last = newPoint
|
||||
|
||||
|
@ -100,9 +103,6 @@ export class DrawSession implements Session {
|
|||
|
||||
const delta = Vec.sub(topLeft, this.origin)
|
||||
|
||||
// Add the new adjusted point to the points array
|
||||
this.points.push(newPoint)
|
||||
|
||||
// Time to shift some points!
|
||||
let points: number[][]
|
||||
|
||||
|
|
Loading…
Reference in a new issue