Push a few more methods to the base shape utils class
This commit is contained in:
parent
f4e8631482
commit
dea7d5c7d4
14 changed files with 347 additions and 35 deletions
|
@ -0,0 +1,15 @@
|
|||
import * as React from 'react'
|
||||
|
||||
interface HTMLContainerProps extends React.HTMLProps<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const HTMLContainer = React.memo(
|
||||
React.forwardRef<HTMLDivElement, HTMLContainerProps>(({ children, ...rest }, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="tl-positioned-div" {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)
|
1
packages/core/src/components/html-container/index.ts
Normal file
1
packages/core/src/components/html-container/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './html-container'
|
|
@ -1,3 +1,4 @@
|
|||
export * from './renderer'
|
||||
export { brushUpdater } from './brush'
|
||||
export * from './svg-container'
|
||||
export * from './html-container'
|
||||
|
|
|
@ -153,6 +153,13 @@ const tlcss = css`
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.tl-positioned-div {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: var(--tl-padding);
|
||||
}
|
||||
|
||||
.tl-layer {
|
||||
transform: scale(var(--tl-zoom), var(--tl-zoom))
|
||||
translate(var(--tl-camera-x), var(--tl-camera-y));
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* --------------------- Primary -------------------- */
|
||||
|
||||
import { Intersect, Vec } from '+utils'
|
||||
import React, { ForwardedRef } from 'react'
|
||||
|
||||
export type Patch<T> = Partial<{ [P in keyof T]: T | Partial<T> | Patch<T[P]> }>
|
||||
|
@ -304,16 +305,6 @@ export abstract class TLShapeUtil<T extends TLShape, E extends HTMLElement | SVG
|
|||
|
||||
abstract getRotatedBounds(shape: T): TLBounds
|
||||
|
||||
abstract hitTest(shape: T, point: number[]): boolean
|
||||
|
||||
abstract hitTestBounds(shape: T, bounds: TLBounds): boolean
|
||||
|
||||
abstract transform(shape: T, bounds: TLBounds, info: TLTransformInfo<T>): Partial<T>
|
||||
|
||||
transformSingle(shape: T, bounds: TLBounds, info: TLTransformInfo<T>): Partial<T> {
|
||||
return this.transform(shape, bounds, info)
|
||||
}
|
||||
|
||||
shouldRender(_prev: T, _next: T): boolean {
|
||||
return true
|
||||
}
|
||||
|
@ -356,6 +347,14 @@ export abstract class TLShapeUtil<T extends TLShape, E extends HTMLElement | SVG
|
|||
return { ...shape, ...props }
|
||||
}
|
||||
|
||||
transform(shape: T, bounds: TLBounds, info: TLTransformInfo<T>): Partial<T> | void {
|
||||
return undefined
|
||||
}
|
||||
|
||||
transformSingle(shape: T, bounds: TLBounds, info: TLTransformInfo<T>): Partial<T> | void {
|
||||
return this.transform(shape, bounds, info)
|
||||
}
|
||||
|
||||
updateChildren<K extends TLShape>(shape: T, children: K[]): Partial<K>[] | void {
|
||||
return
|
||||
}
|
||||
|
@ -409,6 +408,40 @@ export abstract class TLShapeUtil<T extends TLShape, E extends HTMLElement | SVG
|
|||
onStyleChange(shape: T): Partial<T> | void {
|
||||
return
|
||||
}
|
||||
|
||||
hitTest(shape: T, point: number[]) {
|
||||
const bounds = this.getBounds(shape)
|
||||
return !(
|
||||
point[0] < bounds.minX ||
|
||||
point[0] > bounds.maxX ||
|
||||
point[1] < bounds.minY ||
|
||||
point[1] > bounds.maxY
|
||||
)
|
||||
}
|
||||
|
||||
hitTestBounds(shape: T, bounds: TLBounds) {
|
||||
const { minX, minY, maxX, maxY, width, height } = this.getBounds(shape)
|
||||
const center = [minX + width / 2, minY + height / 2]
|
||||
|
||||
const corners = [
|
||||
[minX, minY],
|
||||
[maxX, minY],
|
||||
[maxX, maxY],
|
||||
[minX, maxY],
|
||||
].map((point) => Vec.rotWith(point, center, shape.rotation || 0))
|
||||
|
||||
return (
|
||||
corners.every(
|
||||
(point) =>
|
||||
!(
|
||||
point[0] < bounds.minX ||
|
||||
point[0] > bounds.maxX ||
|
||||
point[1] < bounds.minY ||
|
||||
point[1] > bounds.maxY
|
||||
)
|
||||
) || Intersect.polyline.bounds(corners, bounds).length > 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------- Internal -------------------- */
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react'
|
|||
import { IdProvider } from '@radix-ui/react-id'
|
||||
import { Renderer } from '@tldraw/core'
|
||||
import styled from '~styles'
|
||||
import type { Data, TLDrawDocument } from '~types'
|
||||
import { Data, TLDrawDocument, TLDrawStatus, TLDrawToolType } from '~types'
|
||||
import { TLDrawState } from '~state'
|
||||
import { TLDrawContext, useCustomFonts, useKeyboardShortcuts, useTLDrawContext } from '~hooks'
|
||||
import { tldrawShapeUtils } from '~shape'
|
||||
|
@ -109,7 +109,8 @@ function InnerTldraw({
|
|||
const hideHandles = isInSession || !isSelecting
|
||||
|
||||
// Hide indicators when not using the select tool, or when in session
|
||||
const hideIndicators = isInSession || !isSelecting
|
||||
const hideIndicators =
|
||||
(isInSession && tlstate.appState.status.current !== TLDrawStatus.Brushing) || !isSelecting
|
||||
|
||||
// Custom rendering meta, with dark mode for shapes
|
||||
const meta = React.useMemo(() => ({ isDarkMode }), [isDarkMode])
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Rectangle, Ellipse, Arrow, Draw, Text, Group } from './shapes'
|
||||
import { Rectangle, Ellipse, Arrow, Draw, Text, Group, PostIt } from './shapes'
|
||||
import { TLDrawShapeType, TLDrawShape, TLDrawShapeUtil, TLDrawShapeUtils } from '~types'
|
||||
|
||||
export const tldrawShapeUtils: TLDrawShapeUtils = {
|
||||
|
@ -8,6 +8,7 @@ export const tldrawShapeUtils: TLDrawShapeUtils = {
|
|||
[TLDrawShapeType.Arrow]: new Arrow(),
|
||||
[TLDrawShapeType.Text]: new Text(),
|
||||
[TLDrawShapeType.Group]: new Group(),
|
||||
[TLDrawShapeType.PostIt]: new PostIt(),
|
||||
} as TLDrawShapeUtils
|
||||
|
||||
export type ShapeByType<T extends keyof TLDrawShapeUtils> = TLDrawShapeUtils[T]
|
||||
|
|
|
@ -4,3 +4,4 @@ export * from './rectangle'
|
|||
export * from './ellipse'
|
||||
export * from './text'
|
||||
export * from './group'
|
||||
export * from './post-it'
|
||||
|
|
1
packages/tldraw/src/shape/shapes/post-it/index.ts
Normal file
1
packages/tldraw/src/shape/shapes/post-it/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './post-it'
|
|
@ -0,0 +1,7 @@
|
|||
import { PostIt } from './post-it'
|
||||
|
||||
describe('Post-It shape', () => {
|
||||
it('Creates an instance', () => {
|
||||
new PostIt()
|
||||
})
|
||||
})
|
250
packages/tldraw/src/shape/shapes/post-it/post-it.tsx
Normal file
250
packages/tldraw/src/shape/shapes/post-it/post-it.tsx
Normal file
|
@ -0,0 +1,250 @@
|
|||
import * as React from 'react'
|
||||
import {
|
||||
TLBounds,
|
||||
Utils,
|
||||
Vec,
|
||||
TLTransformInfo,
|
||||
Intersect,
|
||||
TLShapeProps,
|
||||
HTMLContainer,
|
||||
} from '@tldraw/core'
|
||||
import { defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||
import { PostItShape, TLDrawShapeUtil, TLDrawShapeType, TLDrawToolType, ArrowShape } from '~types'
|
||||
|
||||
// TODO
|
||||
// [ ] - Make sure that fill does not extend drawn shape at corners
|
||||
|
||||
export class PostIt extends TLDrawShapeUtil<PostItShape, HTMLDivElement> {
|
||||
type = TLDrawShapeType.PostIt as const
|
||||
toolType = TLDrawToolType.Bounds
|
||||
canBind = true
|
||||
pathCache = new WeakMap<number[], string>([])
|
||||
|
||||
defaultProps: PostItShape = {
|
||||
id: 'id',
|
||||
type: TLDrawShapeType.PostIt as const,
|
||||
name: 'PostIt',
|
||||
parentId: 'page',
|
||||
childIndex: 1,
|
||||
point: [0, 0],
|
||||
size: [1, 1],
|
||||
text: '',
|
||||
rotation: 0,
|
||||
style: defaultStyle,
|
||||
}
|
||||
|
||||
shouldRender(prev: PostItShape, next: PostItShape) {
|
||||
return next.size !== prev.size || next.style !== prev.style
|
||||
}
|
||||
|
||||
render = React.forwardRef<HTMLDivElement, TLShapeProps<PostItShape, HTMLDivElement>>(
|
||||
({ shape, isBinding, meta, events }, ref) => {
|
||||
const [count, setCount] = React.useState(0)
|
||||
|
||||
return (
|
||||
<HTMLContainer ref={ref} {...events}>
|
||||
<div
|
||||
style={{
|
||||
pointerEvents: 'all',
|
||||
backgroundColor: 'rgba(255, 220, 100)',
|
||||
border: '1px solid black',
|
||||
fontFamily: 'sans-serif',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div onPointerDown={(e) => e.preventDefault()}>
|
||||
<input
|
||||
type="textarea"
|
||||
style={{ width: '100%', height: '50%', background: 'none' }}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<button onPointerDown={() => setCount((count) => count + 1)}>{count}</button>
|
||||
</div>
|
||||
</div>
|
||||
</HTMLContainer>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
renderIndicator(shape: PostItShape) {
|
||||
const {
|
||||
style,
|
||||
size: [width, height],
|
||||
} = shape
|
||||
|
||||
const styles = getShapeStyle(style, false)
|
||||
const strokeWidth = +styles.strokeWidth
|
||||
|
||||
const sw = strokeWidth
|
||||
|
||||
return (
|
||||
<rect
|
||||
x={sw / 2}
|
||||
y={sw / 2}
|
||||
rx={1}
|
||||
ry={1}
|
||||
width={Math.max(1, width - sw)}
|
||||
height={Math.max(1, height - sw)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
getBounds(shape: PostItShape) {
|
||||
const bounds = Utils.getFromCache(this.boundsCache, shape, () => {
|
||||
const [width, height] = shape.size
|
||||
return {
|
||||
minX: 0,
|
||||
maxX: width,
|
||||
minY: 0,
|
||||
maxY: height,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
})
|
||||
|
||||
return Utils.translateBounds(bounds, shape.point)
|
||||
}
|
||||
|
||||
getRotatedBounds(shape: PostItShape) {
|
||||
return Utils.getBoundsFromPoints(Utils.getRotatedCorners(this.getBounds(shape), shape.rotation))
|
||||
}
|
||||
|
||||
getCenter(shape: PostItShape): number[] {
|
||||
return Utils.getBoundsCenter(this.getBounds(shape))
|
||||
}
|
||||
|
||||
getBindingPoint(
|
||||
shape: PostItShape,
|
||||
fromShape: ArrowShape,
|
||||
point: number[],
|
||||
origin: number[],
|
||||
direction: number[],
|
||||
padding: number,
|
||||
anywhere: boolean
|
||||
) {
|
||||
const bounds = this.getBounds(shape)
|
||||
|
||||
const expandedBounds = Utils.expandBounds(bounds, padding)
|
||||
|
||||
let bindingPoint: number[]
|
||||
let distance: number
|
||||
|
||||
// The point must be inside of the expanded bounding box
|
||||
if (!Utils.pointInBounds(point, expandedBounds)) return
|
||||
|
||||
// The point is inside of the shape, so we'll assume the user is
|
||||
// indicating a specific point inside of the shape.
|
||||
if (anywhere) {
|
||||
if (Vec.dist(point, this.getCenter(shape)) < 12) {
|
||||
bindingPoint = [0.5, 0.5]
|
||||
} else {
|
||||
bindingPoint = Vec.divV(Vec.sub(point, [expandedBounds.minX, expandedBounds.minY]), [
|
||||
expandedBounds.width,
|
||||
expandedBounds.height,
|
||||
])
|
||||
}
|
||||
|
||||
distance = 0
|
||||
} else {
|
||||
// TODO: What if the shape has a curve? In that case, should we
|
||||
// intersect the circle-from-three-points instead?
|
||||
|
||||
// Find furthest intersection between ray from
|
||||
// origin through point and expanded bounds.
|
||||
|
||||
// TODO: Make this a ray vs rounded rect intersection
|
||||
const intersection = Intersect.ray
|
||||
.bounds(origin, direction, expandedBounds)
|
||||
.filter((int) => int.didIntersect)
|
||||
.map((int) => int.points[0])
|
||||
.sort((a, b) => Vec.dist(b, origin) - Vec.dist(a, origin))[0]
|
||||
// The anchor is a point between the handle and the intersection
|
||||
const anchor = Vec.med(point, intersection)
|
||||
|
||||
// If we're close to the center, snap to the center
|
||||
if (Vec.distanceToLineSegment(point, anchor, this.getCenter(shape)) < 12) {
|
||||
bindingPoint = [0.5, 0.5]
|
||||
} else {
|
||||
// Or else calculate a normalized point
|
||||
bindingPoint = Vec.divV(Vec.sub(anchor, [expandedBounds.minX, expandedBounds.minY]), [
|
||||
expandedBounds.width,
|
||||
expandedBounds.height,
|
||||
])
|
||||
}
|
||||
|
||||
if (Utils.pointInBounds(point, bounds)) {
|
||||
distance = 16
|
||||
} else {
|
||||
// If the binding point was close to the shape's center, snap to the center
|
||||
// Find the distance between the point and the real bounds of the shape
|
||||
distance = Math.max(
|
||||
16,
|
||||
Utils.getBoundsSides(bounds)
|
||||
.map((side) => Vec.distanceToLineSegment(side[1][0], side[1][1], point))
|
||||
.sort((a, b) => a - b)[0]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
point: Vec.clampV(bindingPoint, 0, 1),
|
||||
distance,
|
||||
}
|
||||
}
|
||||
|
||||
hitTestBounds(shape: PostItShape, bounds: TLBounds) {
|
||||
const rotatedCorners = Utils.getRotatedCorners(this.getBounds(shape), shape.rotation)
|
||||
|
||||
return (
|
||||
rotatedCorners.every((point) => Utils.pointInBounds(point, bounds)) ||
|
||||
Intersect.polyline.bounds(rotatedCorners, bounds).length > 0
|
||||
)
|
||||
}
|
||||
|
||||
transform(
|
||||
shape: PostItShape,
|
||||
bounds: TLBounds,
|
||||
{ initialShape, transformOrigin, scaleX, scaleY }: TLTransformInfo<PostItShape>
|
||||
) {
|
||||
if (!shape.rotation && !shape.isAspectRatioLocked) {
|
||||
return {
|
||||
point: Vec.round([bounds.minX, bounds.minY]),
|
||||
size: Vec.round([bounds.width, bounds.height]),
|
||||
}
|
||||
} else {
|
||||
const size = Vec.round(
|
||||
Vec.mul(initialShape.size, Math.min(Math.abs(scaleX), Math.abs(scaleY)))
|
||||
)
|
||||
|
||||
const point = Vec.round([
|
||||
bounds.minX +
|
||||
(bounds.width - shape.size[0]) *
|
||||
(scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
|
||||
bounds.minY +
|
||||
(bounds.height - shape.size[1]) *
|
||||
(scaleY < 0 ? 1 - transformOrigin[1] : transformOrigin[1]),
|
||||
])
|
||||
|
||||
const rotation =
|
||||
(scaleX < 0 && scaleY >= 0) || (scaleY < 0 && scaleX >= 0)
|
||||
? initialShape.rotation
|
||||
? -initialShape.rotation
|
||||
: 0
|
||||
: initialShape.rotation
|
||||
|
||||
return {
|
||||
size,
|
||||
point,
|
||||
rotation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transformSingle(_shape: PostItShape, bounds: TLBounds) {
|
||||
return {
|
||||
size: Vec.round([bounds.width, bounds.height]),
|
||||
point: Vec.round([bounds.minX, bounds.minY]),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import {
|
|||
Intersect,
|
||||
TLShapeProps,
|
||||
SVGContainer,
|
||||
HTMLContainer,
|
||||
} from '@tldraw/core'
|
||||
import getStroke from 'perfect-freehand'
|
||||
import { getPerfectDashProps, defaultStyle, getShapeStyle } from '~shape/shape-styles'
|
||||
|
@ -120,7 +121,7 @@ export class Rectangle extends TLDrawShapeUtil<RectangleShape, SVGSVGElement> {
|
|||
})
|
||||
|
||||
return (
|
||||
<g ref={ref} {...events}>
|
||||
<SVGContainer ref={ref} {...events}>
|
||||
{isBinding && (
|
||||
<rect
|
||||
className="tl-binding-indicator"
|
||||
|
@ -141,7 +142,7 @@ export class Rectangle extends TLDrawShapeUtil<RectangleShape, SVGSVGElement> {
|
|||
pointerEvents="all"
|
||||
/>
|
||||
<g pointerEvents="stroke">{paths}</g>
|
||||
</g>
|
||||
</SVGContainer>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -272,19 +273,6 @@ export class Rectangle extends TLDrawShapeUtil<RectangleShape, SVGSVGElement> {
|
|||
}
|
||||
}
|
||||
|
||||
hitTest(shape: RectangleShape, point: number[]) {
|
||||
return Utils.pointInBounds(point, this.getBounds(shape))
|
||||
}
|
||||
|
||||
hitTestBounds(shape: RectangleShape, bounds: TLBounds) {
|
||||
const rotatedCorners = Utils.getRotatedCorners(this.getBounds(shape), shape.rotation)
|
||||
|
||||
return (
|
||||
rotatedCorners.every((point) => Utils.pointInBounds(point, bounds)) ||
|
||||
Intersect.polyline.bounds(rotatedCorners, bounds).length > 0
|
||||
)
|
||||
}
|
||||
|
||||
transform(
|
||||
shape: RectangleShape,
|
||||
bounds: TLBounds,
|
||||
|
|
|
@ -2394,6 +2394,8 @@ export class TLDrawState extends StateManager<Data> {
|
|||
}
|
||||
|
||||
// Start a brush session
|
||||
// TODO: Don't start a brush session right away: we might
|
||||
// be "maybe brushing" or "maybe double clicking"
|
||||
this.startBrushSession(this.getPagePoint(info.point))
|
||||
break
|
||||
}
|
||||
|
@ -2407,13 +2409,9 @@ export class TLDrawState extends StateManager<Data> {
|
|||
// Unused
|
||||
switch (this.appState.status.current) {
|
||||
case TLDrawStatus.Idle: {
|
||||
switch (this.appState.activeTool) {
|
||||
case TLDrawShapeType.Text: {
|
||||
// Create a text shape
|
||||
this.createActiveToolShape(info.point)
|
||||
break
|
||||
}
|
||||
}
|
||||
// TODO: Create a text shape
|
||||
// this.selectTool(TLDrawShapeType.Text)
|
||||
// this.createActiveToolShape(info.point)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
@ -134,6 +134,7 @@ export enum TLDrawToolType {
|
|||
}
|
||||
|
||||
export enum TLDrawShapeType {
|
||||
PostIt = 'post-it',
|
||||
Ellipse = 'ellipse',
|
||||
Rectangle = 'rectangle',
|
||||
Draw = 'draw',
|
||||
|
@ -191,6 +192,12 @@ export interface GroupShape extends TLDrawBaseShape {
|
|||
children: string[]
|
||||
}
|
||||
|
||||
export interface PostItShape extends TLDrawBaseShape {
|
||||
type: TLDrawShapeType.PostIt
|
||||
size: number[]
|
||||
text: string
|
||||
}
|
||||
|
||||
export type TLDrawShape =
|
||||
| RectangleShape
|
||||
| EllipseShape
|
||||
|
@ -198,6 +205,7 @@ export type TLDrawShape =
|
|||
| ArrowShape
|
||||
| TextShape
|
||||
| GroupShape
|
||||
| PostItShape
|
||||
|
||||
export abstract class TLDrawShapeUtil<
|
||||
T extends TLDrawShape,
|
||||
|
|
Loading…
Reference in a new issue