kopia lustrzana https://github.com/Tldraw/Tldraw
195 wiersze
5.5 KiB
TypeScript
195 wiersze
5.5 KiB
TypeScript
import { Matrix2d } from '@tldraw/primitives'
|
|
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
|
import * as React from 'react'
|
|
import {
|
|
track,
|
|
// @ts-expect-error 'private' export
|
|
useStateTracking,
|
|
} from 'signia-react'
|
|
import { useApp } from '../..'
|
|
import { TLShapeUtil } from '../app/shapeutils/TLShapeUtil'
|
|
import { useEditorComponents } from '../hooks/useEditorComponents'
|
|
import { useQuickReactor } from '../hooks/useQuickReactor'
|
|
import { useShapeEvents } from '../hooks/useShapeEvents'
|
|
import { OptionalErrorBoundary } from './ErrorBoundary'
|
|
|
|
/*
|
|
This component renders shapes on the canvas. There are two stages: positioning
|
|
and styling the shape's container using CSS, and then rendering the shape's
|
|
JSX using its shape util's render method. Rendering the "inside" of a shape is
|
|
more expensive than positioning it or changing its color, so we use React.memo
|
|
to wrap the inner shape and only re-render it when the shape's props change.
|
|
|
|
The shape also receives props for its index and opacity. The index is used to
|
|
determine the z-index of the shape, and the opacity is used to set the shape's
|
|
opacity based on its own opacity and that of its parent's.
|
|
*/
|
|
export const Shape = track(function Shape({
|
|
id,
|
|
index,
|
|
backgroundIndex,
|
|
opacity,
|
|
isCulled,
|
|
}: {
|
|
id: TLShapeId
|
|
index: number
|
|
backgroundIndex: number
|
|
opacity: number
|
|
isCulled: boolean
|
|
}) {
|
|
const app = useApp()
|
|
|
|
const { ShapeErrorFallback } = useEditorComponents()
|
|
|
|
const events = useShapeEvents(id)
|
|
|
|
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
const backgroundContainerRef = React.useRef<HTMLDivElement>(null)
|
|
|
|
const setProperty = React.useCallback((property: string, value: string) => {
|
|
containerRef.current?.style.setProperty(property, value)
|
|
backgroundContainerRef.current?.style.setProperty(property, value)
|
|
}, [])
|
|
|
|
useQuickReactor(
|
|
'set shape container transform position',
|
|
() => {
|
|
const shape = app.getShapeById(id)
|
|
const pageTransform = app.getPageTransformById(id)
|
|
|
|
if (!shape || !pageTransform) return null
|
|
|
|
const transform = Matrix2d.toCssString(pageTransform)
|
|
setProperty('transform', transform)
|
|
},
|
|
[app, setProperty]
|
|
)
|
|
|
|
useQuickReactor(
|
|
'set shape container clip path / color',
|
|
() => {
|
|
const shape = app.getShapeById(id)
|
|
if (!shape) return null
|
|
|
|
const clipPath = app.getClipPathById(id)
|
|
setProperty('clip-path', clipPath ?? 'none')
|
|
if ('color' in shape.props) {
|
|
setProperty('color', app.getCssColor(shape.props.color))
|
|
}
|
|
},
|
|
[app, setProperty]
|
|
)
|
|
|
|
useQuickReactor(
|
|
'set shape height and width',
|
|
() => {
|
|
const shape = app.getShapeById(id)
|
|
if (!shape) return null
|
|
|
|
const util = app.getShapeUtil(shape)
|
|
const bounds = util.bounds(shape)
|
|
setProperty('width', Math.ceil(bounds.width) + 'px')
|
|
setProperty('height', Math.ceil(bounds.height) + 'px')
|
|
},
|
|
[app]
|
|
)
|
|
|
|
// Set the opacity of the container when the opacity changes
|
|
React.useLayoutEffect(() => {
|
|
setProperty('opacity', opacity + '')
|
|
containerRef.current?.style.setProperty('z-index', index + '')
|
|
backgroundContainerRef.current?.style.setProperty('z-index', backgroundIndex + '')
|
|
}, [opacity, index, backgroundIndex, setProperty])
|
|
|
|
const shape = app.getShapeById(id)
|
|
|
|
if (!shape) return null
|
|
|
|
const util = app.getShapeUtil(shape)
|
|
|
|
return (
|
|
<>
|
|
{util.renderBackground && (
|
|
<div
|
|
ref={backgroundContainerRef}
|
|
className="tl-shape tl-shape-background"
|
|
data-shape-type={shape.type}
|
|
draggable={false}
|
|
>
|
|
{!isCulled && (
|
|
<OptionalErrorBoundary
|
|
fallback={ShapeErrorFallback ? (error) => <ShapeErrorFallback error={error} /> : null}
|
|
onError={(error) =>
|
|
app.annotateError(error, { origin: 'react.shape', willCrashApp: false })
|
|
}
|
|
>
|
|
<InnerShapeBackground shape={shape} util={util} />
|
|
</OptionalErrorBoundary>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div
|
|
ref={containerRef}
|
|
className="tl-shape"
|
|
data-shape-type={shape.type}
|
|
draggable={false}
|
|
onPointerDown={events.onPointerDown}
|
|
onPointerMove={events.onPointerMove}
|
|
onPointerUp={events.onPointerUp}
|
|
onPointerEnter={events.onPointerEnter}
|
|
onPointerLeave={events.onPointerLeave}
|
|
>
|
|
{isCulled && util.canUnmount(shape) ? (
|
|
<CulledShape shape={shape} util={util} />
|
|
) : (
|
|
<OptionalErrorBoundary
|
|
fallback={ShapeErrorFallback ? (error) => <ShapeErrorFallback error={error} /> : null}
|
|
onError={(error) =>
|
|
app.annotateError(error, { origin: 'react.shape', willCrashApp: false })
|
|
}
|
|
>
|
|
<InnerShape shape={shape} util={util} />
|
|
</OptionalErrorBoundary>
|
|
)}
|
|
</div>
|
|
</>
|
|
)
|
|
})
|
|
|
|
const InnerShape = React.memo(
|
|
function InnerShape<T extends TLShape>({ shape, util }: { shape: T; util: TLShapeUtil<T> }) {
|
|
return useStateTracking('InnerShape:' + util.type, () => util.render(shape))
|
|
},
|
|
(prev, next) => prev.shape.props === next.shape.props
|
|
)
|
|
|
|
const InnerShapeBackground = React.memo(
|
|
function InnerShapeBackground<T extends TLShape>({
|
|
shape,
|
|
util,
|
|
}: {
|
|
shape: T
|
|
util: TLShapeUtil<T>
|
|
}) {
|
|
return useStateTracking('InnerShape:' + util.type, () => util.renderBackground?.(shape))
|
|
},
|
|
(prev, next) => prev.shape.props === next.shape.props
|
|
)
|
|
|
|
const CulledShape = React.memo(
|
|
function CulledShap<T extends TLShape>({ shape, util }: { shape: T; util: TLShapeUtil<T> }) {
|
|
const bounds = util.bounds(shape)
|
|
return (
|
|
<div
|
|
className="tl-shape__culled"
|
|
style={{
|
|
transform: `translate(${bounds.minX}px, ${bounds.minY}px)`,
|
|
width: bounds.width,
|
|
height: bounds.height,
|
|
}}
|
|
/>
|
|
)
|
|
},
|
|
() => true
|
|
)
|