Adds copy, fix bug on mutating bound shapes, adds binding indicator, adds binding to text

This commit is contained in:
Steve Ruiz 2021-09-01 12:18:50 +01:00
parent 89dfd22bac
commit f6934dedb8
10 changed files with 174 additions and 58 deletions

View file

@ -10,7 +10,7 @@ export function Defs({ zoom }: DefProps): JSX.Element {
<circle id="dot" className="tl-counter-scaled tl-dot" r={4} />
<circle id="handle-bg" className="tl-handle-bg" pointerEvents="all" r={12} />
<circle id="handle" className="tl-counter-scaled tl-handle" pointerEvents="none" r={4} />
<g id="cross" className="tl-binding-indicator">
<g id="cross" className="tl-anchor-indicator">
<line x1={-6} y1={-6} x2={6} y2={6} />
<line x1={6} y1={-6} x2={-6} y2={6} />
</g>

View file

@ -16,6 +16,7 @@ function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
shapes: TLPage<T, TLBinding>['shapes'],
selectedIds: string[],
pageState: {
bindingTargetId?: string
bindingId?: string
hoveredId?: string
currentParentId?: string
@ -28,7 +29,7 @@ function addToShapeTree<T extends TLShape, M extends Record<string, unknown>>(
shape,
isCurrentParent: pageState.currentParentId === shape.id,
isEditing: pageState.editingId === shape.id,
isBinding: pageState.bindingId === shape.id,
isBinding: pageState.bindingTargetId === shape.id,
meta,
}
@ -102,13 +103,17 @@ export function useShapeTree<T extends TLShape, M extends Record<string, unknown
rPreviousCount.current = shapesToRender.length
}
const bindingTargetId = pageState.bindingId ? page.bindings[pageState.bindingId].toId : undefined
// Populate the shape tree
const tree: IShapeTreeNode<M>[] = []
shapesToRender
.sort((a, b) => a.childIndex - b.childIndex)
.forEach((shape) => addToShapeTree(shape, tree, page.shapes, selectedIds, pageState, meta))
.forEach((shape) =>
addToShapeTree(shape, tree, page.shapes, selectedIds, { ...pageState, bindingTargetId }, meta)
)
return tree
}

View file

@ -241,7 +241,7 @@ const tlcss = css`
}
.tl-binding-indicator {
stroke-width: calc(3px * var(--tl-scale));
fill: none;
fill: var(--tl-selectFill);
stroke: var(--tl-selected);
}
.tl-shape-group {

View file

@ -101,12 +101,12 @@ export const ContextMenu = React.memo(({ children }: ContextMenuProps): JSX.Elem
tlstate.delete()
}, [tlstate])
const handleCopyAsJson = React.useCallback(() => {
tlstate.copyAsJson()
const handlecopyJson = React.useCallback(() => {
tlstate.copyJson()
}, [tlstate])
const handleCopyAsSvg = React.useCallback(() => {
tlstate.copyAsSvg()
const handlecopySvg = React.useCallback(() => {
tlstate.copySvg()
}, [tlstate])
const handleUndo = React.useCallback(() => {
@ -180,12 +180,12 @@ export const ContextMenu = React.memo(({ children }: ContextMenuProps): JSX.Elem
)}
{/* <MoveToPageMenu /> */}
{isDebugMode && (
<ContextMenuButton onSelect={handleCopyAsJson}>
<ContextMenuButton onSelect={handlecopyJson}>
<span>Copy Data</span>
<Kbd variant="menu">#C</Kbd>
</ContextMenuButton>
)}
<ContextMenuButton onSelect={handleCopyAsSvg}>
<ContextMenuButton onSelect={handlecopySvg}>
<span>Copy to SVG</span>
<Kbd variant="menu">#C</Kbd>
</ContextMenuButton>

View file

@ -66,8 +66,8 @@ function SelectedShapeContent(): JSX.Element {
tlstate.paste()
}, [tlstate])
const handleCopyAsSvg = React.useCallback(() => {
tlstate.copyAsSvg()
const handlecopySvg = React.useCallback(() => {
tlstate.copySvg()
}, [tlstate])
return (
@ -88,7 +88,7 @@ function SelectedShapeContent(): JSX.Element {
<span>Paste</span>
{showKbds && <Kbd variant="menu">#V</Kbd>}
</RowButton>
<RowButton bp={breakpoints} onClick={handleCopyAsSvg}>
<RowButton bp={breakpoints} onClick={handlecopySvg}>
<span>Copy to SVG</span>
{showKbds && <Kbd variant="menu">#C</Kbd>}
</RowButton>

View file

@ -11,6 +11,8 @@ import {
import styled from '~styles'
import TextAreaUtils from './text-utils'
const LETTER_SPACING = -1.5
function normalizeText(text: string) {
return text.replace(/\r?\n|\r/g, '\n')
}
@ -31,7 +33,7 @@ function getMeasurementDiv() {
border: '1px solid red',
padding: '4px',
margin: '0px',
letterSpacing: '-2.5px',
letterSpacing: `${LETTER_SPACING}px`,
opacity: '0',
position: 'absolute',
top: '-500px',
@ -93,6 +95,7 @@ export class Text extends TLDrawShapeUtil<TextShape> {
ref,
meta,
isEditing,
isBinding,
onTextBlur,
onTextChange,
onTextFocus,
@ -162,15 +165,15 @@ export class Text extends TLDrawShapeUtil<TextShape> {
if (!isEditing) {
return (
<>
{/* {isBinding && (
<BindingIndicator
as="rect"
x={-32}
y={-32}
width={bounds.width + 64}
height={bounds.height + 64}
{isBinding && (
<rect
className="tl-binding-indicator"
x={-16}
y={-16}
width={bounds.width + 32}
height={bounds.height + 32}
/>
)} */}
)}
{text.split('\n').map((str, i) => (
<text
key={i}
@ -179,7 +182,7 @@ export class Text extends TLDrawShapeUtil<TextShape> {
fontFamily="Caveat Brush"
fontStyle="normal"
fontWeight="500"
letterSpacing="-2.5"
letterSpacing={LETTER_SPACING}
fontSize={fontSize}
width={bounds.width}
height={bounds.height}
@ -372,6 +375,81 @@ export class Text extends TLDrawShapeUtil<TextShape> {
return shape.text.trim().length === 0
}
getBindingPoint(
shape: TextShape,
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 {
// 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,
}
}
// getBindingPoint(shape, point, origin, direction, expandDistance) {
// const bounds = this.getBounds(shape)
@ -442,7 +520,7 @@ const StyledTextArea = styled('textarea', {
minHeight: 1,
minWidth: 1,
lineHeight: 1.4,
letterSpacing: -2.5,
letterSpacing: LETTER_SPACING,
outline: 0,
fontWeight: '500',
backgroundColor: '$boundsBg',

View file

@ -7,9 +7,7 @@ export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>):
const { before, after } = TLDR.mutateShapes(
data,
ids,
(shape) => {
return { style: { ...shape.style, ...changes } }
},
(shape) => ({ style: { ...shape.style, ...changes } }),
currentPageId
)
@ -18,7 +16,9 @@ export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>):
before: {
document: {
pages: {
[currentPageId]: { shapes: before },
[currentPageId]: {
shapes: before,
},
},
},
appState: {
@ -28,7 +28,9 @@ export function style(data: Data, ids: string[], changes: Partial<ShapeStyles>):
after: {
document: {
pages: {
[currentPageId]: { shapes: after },
[currentPageId]: {
shapes: after,
},
},
},
appState: {

View file

@ -427,9 +427,19 @@ export class TLDR {
afterShapes[id] = change
})
const dataWithMutations = Utils.deepMerge(data, {
document: {
pages: {
[data.appState.currentPageId]: {
shapes: afterShapes,
},
},
},
})
const dataWithChildrenChanges = ids.reduce<Data>((cData, id) => {
return this.recursivelyUpdateChildren(cData, id, beforeShapes, afterShapes, pageId)
}, data)
}, dataWithMutations)
const dataWithParentChanges = ids.reduce<Data>((cData, id) => {
return this.recursivelyUpdateParents(cData, id, beforeShapes, afterShapes, pageId)

View file

@ -228,10 +228,12 @@ describe('TLDrawState', () => {
})
describe('Copies to JSON', () => {
// TODO
tlstate.selectAll()
tlstate.copyJson()
})
describe('Copies to SVG', () => {
// TODO
tlstate.selectAll()
tlstate.copySvg()
})
})

View file

@ -717,7 +717,7 @@ export class TLDrawState extends StateManager<Data> {
* @param pageId The page from which to copy the shapes.
* @returns A string containing the JSON.
*/
copyAsSvg = (ids = this.selectedIds, pageId = this.currentPageId) => {
copySvg = (ids = this.selectedIds, pageId = this.currentPageId) => {
if (ids.length === 0) return
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
@ -767,7 +767,7 @@ export class TLDrawState extends StateManager<Data> {
* @param pageId The page from which to copy the shapes.
* @returns A string containing the JSON.
*/
copyAsJson = (ids = this.selectedIds, pageId = this.currentPageId) => {
copyJson = (ids = this.selectedIds, pageId = this.currentPageId) => {
const shapes = ids.map((id) => this.getShape(id, pageId))
const json = JSON.stringify(shapes, null, 2)
TLDR.copyStringToClipboard(json)
@ -784,7 +784,10 @@ export class TLDrawState extends StateManager<Data> {
* @param args arguments of the session's start method.
* @returns this
*/
startSession<T extends Session>(session: T, ...args: ParametersExceptFirst<T['start']>): this {
startSession = <T extends Session>(
session: T,
...args: ParametersExceptFirst<T['start']>
): this => {
this.session = session
const result = session.start(this.state, ...args)
@ -813,7 +816,7 @@ export class TLDrawState extends StateManager<Data> {
* @param args The arguments of the current session's update method.
* @returns this
*/
updateSession<T extends Session>(...args: ParametersExceptFirst<T['update']>): this {
updateSession = <T extends Session>(...args: ParametersExceptFirst<T['update']>): this => {
const { session } = this
if (!session) return this
const patch = session.update(this.state, ...args)
@ -826,7 +829,7 @@ export class TLDrawState extends StateManager<Data> {
* @param args The arguments of the current session's cancel method.
* @returns this
*/
cancelSession<T extends Session>(...args: ParametersExceptFirst<T['cancel']>): this {
cancelSession = <T extends Session>(...args: ParametersExceptFirst<T['cancel']>): this => {
const { session } = this
if (!session) return this
this.session = undefined
@ -882,7 +885,7 @@ export class TLDrawState extends StateManager<Data> {
* @param args The arguments of the current session's complete method.
* @returns this
*/
completeSession<T extends Session>(...args: ParametersExceptFirst<T['complete']>) {
completeSession = <T extends Session>(...args: ParametersExceptFirst<T['complete']>): this => {
const { session } = this
if (!session) return this
@ -1008,20 +1011,22 @@ export class TLDrawState extends StateManager<Data> {
/**
* Clear the selection history (undo/redo stack for selection).
*/
private clearSelectHistory() {
private clearSelectHistory = (): this => {
this.selectHistory.pointer = 0
this.selectHistory.stack = [this.selectedIds]
return this
}
/**
* Adds a selection to the selection history (undo/redo stack for selection).
*/
private addToSelectHistory(ids: string[]) {
private addToSelectHistory = (ids: string[]): this => {
if (this.selectHistory.pointer < this.selectHistory.stack.length) {
this.selectHistory.stack = this.selectHistory.stack.slice(0, this.selectHistory.pointer + 1)
}
this.selectHistory.pointer++
this.selectHistory.stack.push(ids)
return this
}
/**
@ -1030,7 +1035,7 @@ export class TLDrawState extends StateManager<Data> {
* @param push Whether to add the ids to the current selection instead.
* @returns this
*/
private setSelectedIds(ids: string[], push = false): this {
private setSelectedIds = (ids: string[], push = false): this => {
return this.patchState(
{
appState: {
@ -1053,7 +1058,7 @@ export class TLDrawState extends StateManager<Data> {
* Undo the most recent selection.
* @returns this
*/
undoSelect(): this {
undoSelect = (): this => {
if (this.selectHistory.pointer > 0) {
this.selectHistory.pointer--
this.setSelectedIds(this.selectHistory.stack[this.selectHistory.pointer])
@ -1065,7 +1070,7 @@ export class TLDrawState extends StateManager<Data> {
* Redo the previous selection.
* @returns this
*/
redoSelect(): this {
redoSelect = (): this => {
if (this.selectHistory.pointer < this.selectHistory.stack.length - 1) {
this.selectHistory.pointer++
this.setSelectedIds(this.selectHistory.stack[this.selectHistory.pointer])
@ -1178,7 +1183,8 @@ export class TLDrawState extends StateManager<Data> {
* @param ids The ids of the shapes to change (defaults to selection).
* @returns this
*/
style = (style: Partial<ShapeStyles>, ids = this.selectedIds) => {
style = (style: Partial<ShapeStyles>, ids = this.selectedIds): this => {
if (ids.length === 0) return this
return this.setState(Commands.style(this.state, ids, style))
}
@ -1188,7 +1194,8 @@ export class TLDrawState extends StateManager<Data> {
* @param ids The ids of the shapes to change (defaults to selection).
* @returns this
*/
align = (type: AlignType, ids = this.selectedIds) => {
align = (type: AlignType, ids = this.selectedIds): this => {
if (ids.length < 2) return this
return this.setState(Commands.align(this.state, ids, type))
}
@ -1198,7 +1205,8 @@ export class TLDrawState extends StateManager<Data> {
* @param ids The ids of the shapes to change (defaults to selection).
* @returns this
*/
distribute = (direction: DistributeType, ids = this.selectedIds) => {
distribute = (direction: DistributeType, ids = this.selectedIds): this => {
if (ids.length < 3) return this
return this.setState(Commands.distribute(this.state, ids, direction))
}
@ -1208,7 +1216,8 @@ export class TLDrawState extends StateManager<Data> {
* @param ids The ids of the shapes to change (defaults to selection).
* @returns this
*/
stretch = (direction: StretchType, ids = this.selectedIds) => {
stretch = (direction: StretchType, ids = this.selectedIds): this => {
if (ids.length < 2) return this
return this.setState(Commands.stretch(this.state, ids, direction))
}
@ -1217,7 +1226,8 @@ export class TLDrawState extends StateManager<Data> {
* @param ids The ids of the shapes to change (defaults to selection).
* @returns this
*/
flipHorizontal = (ids = this.selectedIds) => {
flipHorizontal = (ids = this.selectedIds): this => {
if (ids.length === 0) return this
return this.setState(Commands.flip(this.state, ids, FlipType.Horizontal))
}
@ -1226,7 +1236,8 @@ export class TLDrawState extends StateManager<Data> {
* @param ids The ids of the shapes to change (defaults to selection).
* @returns this
*/
flipVertical = (ids = this.selectedIds) => {
flipVertical = (ids = this.selectedIds): this => {
if (ids.length === 0) return this
return this.setState(Commands.flip(this.state, ids, FlipType.Vertical))
}
@ -1235,7 +1246,8 @@ export class TLDrawState extends StateManager<Data> {
* @param ids The ids of the shapes to change (defaults to selection).
* @returns this
*/
moveToBack = (ids = this.selectedIds) => {
moveToBack = (ids = this.selectedIds): this => {
if (ids.length === 0) return this
return this.setState(Commands.move(this.state, ids, MoveType.ToBack))
}
@ -1244,7 +1256,8 @@ export class TLDrawState extends StateManager<Data> {
* @param ids The ids of the shapes to change (defaults to selection).
* @returns this
*/
moveBackward = (ids = this.selectedIds) => {
moveBackward = (ids = this.selectedIds): this => {
if (ids.length === 0) return this
return this.setState(Commands.move(this.state, ids, MoveType.Backward))
}
@ -1253,7 +1266,8 @@ export class TLDrawState extends StateManager<Data> {
* @param ids The ids of the shapes to change (defaults to selection).
* @returns this
*/
moveForward = (ids = this.selectedIds) => {
moveForward = (ids = this.selectedIds): this => {
if (ids.length === 0) return this
return this.setState(Commands.move(this.state, ids, MoveType.Forward))
}
@ -1262,7 +1276,8 @@ export class TLDrawState extends StateManager<Data> {
* @param ids The ids of the shapes to change (defaults to selection).
* @returns this
*/
moveToFront = (ids = this.selectedIds) => {
moveToFront = (ids = this.selectedIds): this => {
if (ids.length === 0) return this
return this.setState(Commands.move(this.state, ids, MoveType.ToFront))
}
@ -1274,6 +1289,7 @@ export class TLDrawState extends StateManager<Data> {
* @returns this
*/
nudge = (delta: number[], isMajor = false, ids = this.selectedIds): this => {
if (ids.length === 0) return this
return this.setState(Commands.translate(this.state, ids, Vec.mul(delta, isMajor ? 10 : 1)))
}
@ -1283,6 +1299,7 @@ export class TLDrawState extends StateManager<Data> {
* @returns this
*/
duplicate = (ids = this.selectedIds): this => {
if (ids.length === 0) return this
return this.setState(Commands.duplicate(this.state, ids))
}
@ -1292,6 +1309,7 @@ export class TLDrawState extends StateManager<Data> {
* @returns this
*/
toggleHidden = (ids = this.selectedIds): this => {
if (ids.length === 0) return this
return this.setState(Commands.toggle(this.state, ids, 'isHidden'))
}
@ -1301,6 +1319,7 @@ export class TLDrawState extends StateManager<Data> {
* @returns this
*/
toggleLocked = (ids = this.selectedIds): this => {
if (ids.length === 0) return this
return this.setState(Commands.toggle(this.state, ids, 'isLocked'))
}
@ -1310,6 +1329,7 @@ export class TLDrawState extends StateManager<Data> {
* @returns this
*/
toggleAspectRatioLocked = (ids = this.selectedIds): this => {
if (ids.length === 0) return this
return this.setState(Commands.toggle(this.state, ids, 'isAspectRatioLocked'))
}
@ -1320,13 +1340,10 @@ export class TLDrawState extends StateManager<Data> {
* @returns this
*/
toggleDecoration = (handleId: string, ids = this.selectedIds): this => {
if (handleId === 'start' || handleId === 'end') {
if (ids.length === 0 || !(handleId === 'start' || handleId === 'end')) return this
return this.setState(Commands.toggleDecoration(this.state, ids, handleId))
}
return this
}
/**
* Rotate one or more shapes by a delta.
* @param delta The delta in radians.
@ -1334,6 +1351,7 @@ export class TLDrawState extends StateManager<Data> {
* @returns this
*/
rotate = (delta = Math.PI * -0.5, ids = this.selectedIds): this => {
if (ids.length === 0) return this
return this.setState(Commands.rotate(this.state, ids, delta))
}
@ -1343,6 +1361,7 @@ export class TLDrawState extends StateManager<Data> {
* @todo
*/
group = (): this => {
// TODO
return this
}