[Snapping 2/5] Fix line-handle mid-point snapping (#2831)
Currently, only the end handles of the line tool snap. It should be all of them. Line handles work kind of weirdly at the moment: instead of just storing the positions, we store full `TLHandle` objects complete with IDs, `canSnap`/`canBind` properties, etc. Currently, all the handles get written to the store with `canSnap: false`, when really it should be up to the shape util to decide which handles are snappable. This diff replaces the current handles map (from arbitrary ID to `TLHandle`) with just the data we need: a map from index to x/y. The extra information that the `Editor` needs for `TLHandle` is hydrated at runtime (with `canSnap` set to `true` this time!) Fixes TLD-2200 This PR is part of a series - please don't merge it until the things before it have landed! 1. #2827 2. #2831 (you are here) 3. #2793 4. #2841 5. #2845 ### Change Type - [x] `major` — Breaking change [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. Create a funky line shape on tldraw.com 2. Paste it into staging and make sure it comes across ok 3. Make some funky line shape in staging - make sure you use dragging, mid-point creation, and shift-clicking - [x] Unit Tests ### Release Notes - Simplify the contents of `TLLineShape.props.handles`
This commit is contained in:
parent
93c2ed615c
commit
4bfea7649d
13 changed files with 317 additions and 147 deletions
|
@ -38,7 +38,6 @@ export class HandleSnaps {
|
||||||
let minDistance = snapThreshold
|
let minDistance = snapThreshold
|
||||||
let nearestPoint: Vec | null = null
|
let nearestPoint: Vec | null = null
|
||||||
let C: VecModel, D: VecModel, nearest: Vec, distance: number
|
let C: VecModel, D: VecModel, nearest: Vec, distance: number
|
||||||
|
|
||||||
const allSegments = [...outlinesInPageSpace, ...additionalSegments]
|
const allSegments = [...outlinesInPageSpace, ...additionalSegments]
|
||||||
for (const outline of allSegments) {
|
for (const outline of allSegments) {
|
||||||
for (let i = 0; i < outline.length - 1; i++) {
|
for (let i = 0; i < outline.length - 1; i++) {
|
||||||
|
|
|
@ -921,7 +921,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
||||||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||||
spline: EnumStyleProp<"cubic" | "line">;
|
spline: EnumStyleProp<"cubic" | "line">;
|
||||||
handles: DictValidator<string, TLHandle>;
|
handles: DictValidator<IndexKey, VecModel>;
|
||||||
};
|
};
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
toSvg(shape: TLLineShape): SVGGElement;
|
toSvg(shape: TLLineShape): SVGGElement;
|
||||||
|
|
|
@ -10994,12 +10994,21 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<string, "
|
"text": "<import(\"@tldraw/editor\")."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
"text": "TLHandle",
|
"text": "IndexKey",
|
||||||
"canonicalReference": "@tldraw/tlschema!TLHandle:interface"
|
"canonicalReference": "@tldraw/utils!IndexKey:type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ", import(\"@tldraw/editor\")."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "VecModel",
|
||||||
|
"canonicalReference": "@tldraw/tlschema!VecModel:interface"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -11016,7 +11025,7 @@
|
||||||
"name": "props",
|
"name": "props",
|
||||||
"propertyTypeTokenRange": {
|
"propertyTypeTokenRange": {
|
||||||
"startIndex": 1,
|
"startIndex": 1,
|
||||||
"endIndex": 14
|
"endIndex": 16
|
||||||
},
|
},
|
||||||
"isStatic": true,
|
"isStatic": true,
|
||||||
"isProtected": false,
|
"isProtected": false,
|
||||||
|
|
|
@ -66,8 +66,8 @@ describe('When dragging the line', () => {
|
||||||
y: 0,
|
y: 0,
|
||||||
props: {
|
props: {
|
||||||
handles: {
|
handles: {
|
||||||
start: { id: 'start', index: 'a1', type: 'vertex', x: 0, y: 0 },
|
a1: { x: 0, y: 0 },
|
||||||
end: { id: 'end', index: 'a2', type: 'vertex', x: 10, y: 10 },
|
a2: { x: 10, y: 10 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { IndexKey, TLGeoShape, TLLineShape, createShapeId, deepCopy } from '@tldraw/editor'
|
import { IndexKey, TLGeoShape, TLLineShape, createShapeId, deepCopy } from '@tldraw/editor'
|
||||||
import { TestEditor } from '../../../test/TestEditor'
|
import { TestEditor } from '../../../test/TestEditor'
|
||||||
|
import { TL } from '../../../test/test-jsx'
|
||||||
|
import { LineShapeUtil } from './LineShapeUtil'
|
||||||
|
|
||||||
jest.mock('nanoid', () => {
|
jest.mock('nanoid', () => {
|
||||||
let i = 0
|
let i = 0
|
||||||
|
@ -24,19 +26,11 @@ beforeEach(() => {
|
||||||
y: 150,
|
y: 150,
|
||||||
props: {
|
props: {
|
||||||
handles: {
|
handles: {
|
||||||
start: {
|
a1: {
|
||||||
id: 'start',
|
|
||||||
type: 'vertex',
|
|
||||||
canBind: false,
|
|
||||||
index: 'a1',
|
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
end: {
|
a2: {
|
||||||
id: 'end',
|
|
||||||
type: 'vertex',
|
|
||||||
canBind: false,
|
|
||||||
index: 'a2',
|
|
||||||
x: 100,
|
x: 100,
|
||||||
y: 100,
|
y: 100,
|
||||||
},
|
},
|
||||||
|
@ -75,6 +69,7 @@ describe('Translating', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Mid-point handles', () => {
|
||||||
it('create new handle', () => {
|
it('create new handle', () => {
|
||||||
editor.select(id)
|
editor.select(id)
|
||||||
|
|
||||||
|
@ -98,16 +93,79 @@ it('create new handle', () => {
|
||||||
props: {
|
props: {
|
||||||
handles: {
|
handles: {
|
||||||
...shape.props.handles,
|
...shape.props.handles,
|
||||||
'handle:a1V': {
|
a1V: { x: 200, y: 200 },
|
||||||
id: 'handle:a1V',
|
|
||||||
type: 'vertex',
|
|
||||||
canBind: false,
|
|
||||||
index: 'a1V',
|
|
||||||
x: 200,
|
|
||||||
y: 200,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows snapping with mid-point handles', () => {
|
||||||
|
editor.createShapesFromJsx([<TL.geo x={200} y={200} w={100} h={100} />])
|
||||||
|
|
||||||
|
editor.select(id)
|
||||||
|
|
||||||
|
const shape = editor.getShape<TLLineShape>(id)!
|
||||||
|
const util = editor.getShapeUtil('line') as LineShapeUtil
|
||||||
|
editor
|
||||||
|
.pointerDown(200, 200, {
|
||||||
|
target: 'handle',
|
||||||
|
shape,
|
||||||
|
handle: util.getHandles(shape).find((h) => h.id === 'mid-0')!,
|
||||||
|
})
|
||||||
|
.pointerMove(198, 230, undefined, { ctrlKey: true })
|
||||||
|
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||||
|
editor.expectShapeToMatch({
|
||||||
|
id: id,
|
||||||
|
props: {
|
||||||
|
handles: {
|
||||||
|
...shape.props.handles,
|
||||||
|
a1V: { x: 50, y: 80 },
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows snapping with created mid-point handles', () => {
|
||||||
|
editor.createShapesFromJsx([<TL.geo x={200} y={200} w={100} h={100} />])
|
||||||
|
editor.select(id)
|
||||||
|
|
||||||
|
const getShape = () => editor.getShape<TLLineShape>(id)!
|
||||||
|
const util = editor.getShapeUtil('line') as LineShapeUtil
|
||||||
|
|
||||||
|
// use a mid-point handle to create a new handle
|
||||||
|
editor
|
||||||
|
.pointerDown(200, 200, {
|
||||||
|
target: 'handle',
|
||||||
|
shape: getShape(),
|
||||||
|
handle: util.getHandles(getShape()).find((h) => h.id === 'mid-0')!,
|
||||||
|
})
|
||||||
|
.pointerMove(230, 200)
|
||||||
|
.pointerMove(200, 200)
|
||||||
|
.pointerUp()
|
||||||
|
|
||||||
|
// 3 actual points, plus 2 mid-points:
|
||||||
|
expect(util.getHandles(getShape())).toHaveLength(5)
|
||||||
|
|
||||||
|
// now, try dragging the newly created handle. it should still snap:
|
||||||
|
editor
|
||||||
|
.pointerDown(200, 200, {
|
||||||
|
target: 'handle',
|
||||||
|
shape: getShape(),
|
||||||
|
handle: util.getHandles(getShape()).find((h) => h.id === 'a1V')!,
|
||||||
|
})
|
||||||
|
.pointerMove(198, 230, undefined, { ctrlKey: true })
|
||||||
|
|
||||||
|
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||||
|
editor.expectShapeToMatch({
|
||||||
|
id: id,
|
||||||
|
props: {
|
||||||
|
handles: {
|
||||||
|
...getShape().props.handles,
|
||||||
|
a1V: { x: 50, y: 80 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
import {
|
import {
|
||||||
CubicSpline2d,
|
CubicSpline2d,
|
||||||
IndexKey,
|
|
||||||
Polyline2d,
|
Polyline2d,
|
||||||
SVGContainer,
|
SVGContainer,
|
||||||
ShapeUtil,
|
ShapeUtil,
|
||||||
|
@ -14,8 +13,10 @@ import {
|
||||||
deepCopy,
|
deepCopy,
|
||||||
getDefaultColorTheme,
|
getDefaultColorTheme,
|
||||||
getIndexBetween,
|
getIndexBetween,
|
||||||
|
getIndices,
|
||||||
lineShapeMigrations,
|
lineShapeMigrations,
|
||||||
lineShapeProps,
|
lineShapeProps,
|
||||||
|
objectMapEntries,
|
||||||
sortByIndex,
|
sortByIndex,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
|
||||||
|
@ -45,27 +46,18 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
override hideSelectionBoundsBg = () => true
|
override hideSelectionBoundsBg = () => true
|
||||||
|
|
||||||
override getDefaultProps(): TLLineShape['props'] {
|
override getDefaultProps(): TLLineShape['props'] {
|
||||||
|
const [startIndex, endIndex] = getIndices(2)
|
||||||
return {
|
return {
|
||||||
dash: 'draw',
|
dash: 'draw',
|
||||||
size: 'm',
|
size: 'm',
|
||||||
color: 'black',
|
color: 'black',
|
||||||
spline: 'line',
|
spline: 'line',
|
||||||
handles: {
|
handles: {
|
||||||
start: {
|
[startIndex]: {
|
||||||
id: 'start',
|
|
||||||
type: 'vertex',
|
|
||||||
canBind: false,
|
|
||||||
canSnap: true,
|
|
||||||
index: 'a1' as IndexKey,
|
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
},
|
},
|
||||||
end: {
|
[endIndex]: {
|
||||||
id: 'end',
|
|
||||||
type: 'vertex',
|
|
||||||
canBind: false,
|
|
||||||
canSnap: true,
|
|
||||||
index: 'a2' as IndexKey,
|
|
||||||
x: 0.1,
|
x: 0.1,
|
||||||
y: 0.1,
|
y: 0.1,
|
||||||
},
|
},
|
||||||
|
@ -84,7 +76,18 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
|
|
||||||
const spline = getGeometryForLineShape(shape)
|
const spline = getGeometryForLineShape(shape)
|
||||||
|
|
||||||
const sortedHandles = Object.values(handles).sort(sortByIndex)
|
const sortedHandles = objectMapEntries(handles)
|
||||||
|
.map(
|
||||||
|
([index, handle]): TLHandle => ({
|
||||||
|
id: index,
|
||||||
|
index,
|
||||||
|
...handle,
|
||||||
|
type: 'vertex',
|
||||||
|
canBind: false,
|
||||||
|
canSnap: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.sort(sortByIndex)
|
||||||
const results = sortedHandles.slice()
|
const results = sortedHandles.slice()
|
||||||
|
|
||||||
// Add "create" handles between each vertex handle
|
// Add "create" handles between each vertex handle
|
||||||
|
@ -99,6 +102,8 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
index,
|
index,
|
||||||
x: point.x,
|
x: point.x,
|
||||||
y: point.y,
|
y: point.y,
|
||||||
|
canSnap: true,
|
||||||
|
canBind: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,9 +123,9 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
|
|
||||||
const handles = deepCopy(shape.props.handles)
|
const handles = deepCopy(shape.props.handles)
|
||||||
|
|
||||||
Object.values(shape.props.handles).forEach(({ id, x, y }) => {
|
objectMapEntries(shape.props.handles).forEach(([index, { x, y }]) => {
|
||||||
handles[id].x = x * scaleX
|
handles[index].x = x * scaleX
|
||||||
handles[id].y = y * scaleY
|
handles[index].y = y * scaleY
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -131,45 +136,16 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override onHandleDrag: TLOnHandleDragHandler<TLLineShape> = (shape, { handle }) => {
|
override onHandleDrag: TLOnHandleDragHandler<TLLineShape> = (shape, { handle }) => {
|
||||||
const next = deepCopy(shape)
|
return {
|
||||||
|
...shape,
|
||||||
switch (handle.id) {
|
props: {
|
||||||
case 'start':
|
...shape.props,
|
||||||
case 'end': {
|
handles: {
|
||||||
next.props.handles[handle.id] = {
|
...shape.props.handles,
|
||||||
...next.props.handles[handle.id],
|
[handle.index]: { x: handle.x, y: handle.y },
|
||||||
x: handle.x,
|
},
|
||||||
y: handle.y,
|
},
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
const id = 'handle:' + handle.index
|
|
||||||
const existing = shape.props.handles[id]
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
next.props.handles[id] = {
|
|
||||||
...existing,
|
|
||||||
x: handle.x,
|
|
||||||
y: handle.y,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next.props.handles[id] = {
|
|
||||||
id,
|
|
||||||
type: 'vertex',
|
|
||||||
canBind: false,
|
|
||||||
index: handle.index,
|
|
||||||
x: handle.x,
|
|
||||||
y: handle.y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return next
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component(shape: TLLineShape) {
|
component(shape: TLLineShape) {
|
||||||
|
@ -409,7 +385,10 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
||||||
/** @public */
|
/** @public */
|
||||||
export function getGeometryForLineShape(shape: TLLineShape): CubicSpline2d | Polyline2d {
|
export function getGeometryForLineShape(shape: TLLineShape): CubicSpline2d | Polyline2d {
|
||||||
const { spline, handles } = shape.props
|
const { spline, handles } = shape.props
|
||||||
const handlePoints = Object.values(handles).sort(sortByIndex).map(Vec.From)
|
const handlePoints = objectMapEntries(handles)
|
||||||
|
.map(([index, position]) => ({ index, ...position }))
|
||||||
|
.sort(sortByIndex)
|
||||||
|
.map(Vec.From)
|
||||||
|
|
||||||
switch (spline) {
|
switch (spline) {
|
||||||
case 'cubic': {
|
case 'cubic': {
|
||||||
|
|
|
@ -12,22 +12,14 @@ exports[`Misc resizes: line shape after resize 1`] = `
|
||||||
"color": "black",
|
"color": "black",
|
||||||
"dash": "draw",
|
"dash": "draw",
|
||||||
"handles": {
|
"handles": {
|
||||||
"end": {
|
"a1": {
|
||||||
"canBind": false,
|
|
||||||
"id": "end",
|
|
||||||
"index": "a2",
|
|
||||||
"type": "vertex",
|
|
||||||
"x": 100,
|
|
||||||
"y": 700,
|
|
||||||
},
|
|
||||||
"start": {
|
|
||||||
"canBind": false,
|
|
||||||
"id": "start",
|
|
||||||
"index": "a1",
|
|
||||||
"type": "vertex",
|
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
},
|
},
|
||||||
|
"a2": {
|
||||||
|
"x": 100,
|
||||||
|
"y": 700,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"size": "m",
|
"size": "m",
|
||||||
"spline": "line",
|
"spline": "line",
|
|
@ -3,11 +3,11 @@ import {
|
||||||
Mat,
|
Mat,
|
||||||
StateNode,
|
StateNode,
|
||||||
TLEventHandlers,
|
TLEventHandlers,
|
||||||
TLHandle,
|
|
||||||
TLInterruptEvent,
|
TLInterruptEvent,
|
||||||
TLLineShape,
|
TLLineShape,
|
||||||
TLShapeId,
|
TLShapeId,
|
||||||
Vec,
|
Vec,
|
||||||
|
VecModel,
|
||||||
createShapeId,
|
createShapeId,
|
||||||
getIndexAbove,
|
getIndexAbove,
|
||||||
last,
|
last,
|
||||||
|
@ -51,7 +51,7 @@ export class Pointing extends StateNode {
|
||||||
new Vec(this.shape.x, this.shape.y)
|
new Vec(this.shape.x, this.shape.y)
|
||||||
)
|
)
|
||||||
|
|
||||||
let nextEndHandleIndex: IndexKey, nextEndHandleId: string, nextEndHandle: TLHandle
|
let nextEndHandleIndex: IndexKey, nextEndHandle: VecModel
|
||||||
|
|
||||||
const nextPoint = Vec.Sub(currentPagePoint, shapePagePoint)
|
const nextPoint = Vec.Sub(currentPagePoint, shapePagePoint)
|
||||||
|
|
||||||
|
@ -61,29 +61,22 @@ export class Pointing extends StateNode {
|
||||||
) {
|
) {
|
||||||
// If the end handle is too close to the previous end handle, we'll just extend the previous end handle
|
// If the end handle is too close to the previous end handle, we'll just extend the previous end handle
|
||||||
nextEndHandleIndex = endHandle.index
|
nextEndHandleIndex = endHandle.index
|
||||||
nextEndHandleId = endHandle.id
|
|
||||||
nextEndHandle = {
|
nextEndHandle = {
|
||||||
...endHandle,
|
|
||||||
x: nextPoint.x + 0.1,
|
x: nextPoint.x + 0.1,
|
||||||
y: nextPoint.y + 0.1,
|
y: nextPoint.y + 0.1,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, we'll create a new end handle
|
// Otherwise, we'll create a new end handle
|
||||||
nextEndHandleIndex = getIndexAbove(endHandle.index)
|
nextEndHandleIndex = getIndexAbove(endHandle.index)
|
||||||
nextEndHandleId = 'handle:' + nextEndHandleIndex
|
|
||||||
nextEndHandle = {
|
nextEndHandle = {
|
||||||
id: nextEndHandleId,
|
|
||||||
type: 'vertex',
|
|
||||||
index: nextEndHandleIndex,
|
|
||||||
x: nextPoint.x + 0.1,
|
x: nextPoint.x + 0.1,
|
||||||
y: nextPoint.y + 0.1,
|
y: nextPoint.y + 0.1,
|
||||||
canBind: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextHandles = structuredClone(this.shape.props.handles)
|
const nextHandles = structuredClone(this.shape.props.handles)
|
||||||
|
|
||||||
nextHandles[nextEndHandle.id] = nextEndHandle
|
nextHandles[nextEndHandleIndex] = nextEndHandle
|
||||||
|
|
||||||
this.editor.updateShapes([
|
this.editor.updateShapes([
|
||||||
{
|
{
|
||||||
|
|
|
@ -674,7 +674,7 @@ export const lineShapeProps: {
|
||||||
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
||||||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||||
spline: EnumStyleProp<"cubic" | "line">;
|
spline: EnumStyleProp<"cubic" | "line">;
|
||||||
handles: T.DictValidator<string, TLHandle>;
|
handles: T.DictValidator<IndexKey, VecModel>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
|
|
@ -2765,12 +2765,21 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<string, import(\"../misc/TLHandle\")."
|
"text": "<import(\"@tldraw/utils\")."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
"text": "TLHandle",
|
"text": "IndexKey",
|
||||||
"canonicalReference": "@tldraw/tlschema!TLHandle:interface"
|
"canonicalReference": "@tldraw/utils!IndexKey:type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ", import(\"../misc/geometry-types\")."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "VecModel",
|
||||||
|
"canonicalReference": "@tldraw/tlschema!VecModel:interface"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -2783,7 +2792,7 @@
|
||||||
"name": "lineShapeProps",
|
"name": "lineShapeProps",
|
||||||
"variableTypeTokenRange": {
|
"variableTypeTokenRange": {
|
||||||
"startIndex": 1,
|
"startIndex": 1,
|
||||||
"endIndex": 14
|
"endIndex": 16
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1844,6 +1844,99 @@ describe('Add duplicate props to instance', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Remove extra handle props', () => {
|
||||||
|
const { up, down } = lineShapeMigrations.migrators[lineShapeVersions.RemoveExtraHandleProps]
|
||||||
|
it('up works as expected', () => {
|
||||||
|
expect(
|
||||||
|
up({
|
||||||
|
props: {
|
||||||
|
handles: {
|
||||||
|
start: {
|
||||||
|
id: 'start',
|
||||||
|
type: 'vertex',
|
||||||
|
canBind: false,
|
||||||
|
canSnap: true,
|
||||||
|
index: 'a1',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
id: 'end',
|
||||||
|
type: 'vertex',
|
||||||
|
canBind: false,
|
||||||
|
canSnap: true,
|
||||||
|
index: 'a2',
|
||||||
|
x: 190,
|
||||||
|
y: -62,
|
||||||
|
},
|
||||||
|
'handle:a1V': {
|
||||||
|
id: 'handle:a1V',
|
||||||
|
type: 'vertex',
|
||||||
|
canBind: false,
|
||||||
|
index: 'a1V',
|
||||||
|
x: 76,
|
||||||
|
y: 60,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
props: {
|
||||||
|
handles: {
|
||||||
|
a1: { x: 0, y: 0 },
|
||||||
|
a1V: { x: 76, y: 60 },
|
||||||
|
a2: { x: 190, y: -62 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('down works as expected', () => {
|
||||||
|
expect(
|
||||||
|
down({
|
||||||
|
props: {
|
||||||
|
handles: {
|
||||||
|
a1: { x: 0, y: 0 },
|
||||||
|
a1V: { x: 76, y: 60 },
|
||||||
|
a2: { x: 190, y: -62 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
props: {
|
||||||
|
handles: {
|
||||||
|
start: {
|
||||||
|
id: 'start',
|
||||||
|
type: 'vertex',
|
||||||
|
canBind: false,
|
||||||
|
canSnap: true,
|
||||||
|
index: 'a1',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
id: 'end',
|
||||||
|
type: 'vertex',
|
||||||
|
canBind: false,
|
||||||
|
canSnap: true,
|
||||||
|
index: 'a2',
|
||||||
|
x: 190,
|
||||||
|
y: -62,
|
||||||
|
},
|
||||||
|
'handle:a1V': {
|
||||||
|
id: 'handle:a1V',
|
||||||
|
type: 'vertex',
|
||||||
|
canBind: false,
|
||||||
|
canSnap: true,
|
||||||
|
index: 'a1V',
|
||||||
|
x: 76,
|
||||||
|
y: 60,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
||||||
|
|
||||||
for (const migrator of allMigrators) {
|
for (const migrator of allMigrators) {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { IndexKey } from '@tldraw/utils'
|
import { IndexKey } from '@tldraw/utils'
|
||||||
import { T } from '@tldraw/validate'
|
|
||||||
import { SetValue } from '../util-types'
|
import { SetValue } from '../util-types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,14 +28,3 @@ export interface TLHandle {
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export const handleValidator: T.Validator<TLHandle> = T.object({
|
|
||||||
id: T.string,
|
|
||||||
type: T.setEnum(TL_HANDLE_TYPES),
|
|
||||||
canBind: T.boolean.optional(),
|
|
||||||
canSnap: T.boolean.optional(),
|
|
||||||
index: T.indexKey,
|
|
||||||
x: T.number,
|
|
||||||
y: T.number,
|
|
||||||
})
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { defineMigrations } from '@tldraw/store'
|
import { defineMigrations } from '@tldraw/store'
|
||||||
import { deepCopy } from '@tldraw/utils'
|
import { deepCopy, objectMapFromEntries, sortByIndex } from '@tldraw/utils'
|
||||||
import { T } from '@tldraw/validate'
|
import { T } from '@tldraw/validate'
|
||||||
import { handleValidator } from '../misc/TLHandle'
|
import { vecModelValidator } from '../misc/geometry-types'
|
||||||
import { StyleProp } from '../styles/StyleProp'
|
import { StyleProp } from '../styles/StyleProp'
|
||||||
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
||||||
import { DefaultDashStyle } from '../styles/TLDashStyle'
|
import { DefaultDashStyle } from '../styles/TLDashStyle'
|
||||||
|
@ -23,7 +23,7 @@ export const lineShapeProps = {
|
||||||
dash: DefaultDashStyle,
|
dash: DefaultDashStyle,
|
||||||
size: DefaultSizeStyle,
|
size: DefaultSizeStyle,
|
||||||
spline: LineShapeSplineStyle,
|
spline: LineShapeSplineStyle,
|
||||||
handles: T.dict(T.string, handleValidator),
|
handles: T.dict(T.indexKey, vecModelValidator),
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -35,11 +35,12 @@ export type TLLineShape = TLBaseShape<'line', TLLineShapeProps>
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const lineShapeVersions = {
|
export const lineShapeVersions = {
|
||||||
AddSnapHandles: 1,
|
AddSnapHandles: 1,
|
||||||
|
RemoveExtraHandleProps: 2,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const lineShapeMigrations = defineMigrations({
|
export const lineShapeMigrations = defineMigrations({
|
||||||
currentVersion: lineShapeVersions.AddSnapHandles,
|
currentVersion: lineShapeVersions.RemoveExtraHandleProps,
|
||||||
migrators: {
|
migrators: {
|
||||||
[lineShapeVersions.AddSnapHandles]: {
|
[lineShapeVersions.AddSnapHandles]: {
|
||||||
up: (record: any) => {
|
up: (record: any) => {
|
||||||
|
@ -57,5 +58,54 @@ export const lineShapeMigrations = defineMigrations({
|
||||||
return { ...record, props: { ...record.props, handles } }
|
return { ...record, props: { ...record.props, handles } }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[lineShapeVersions.RemoveExtraHandleProps]: {
|
||||||
|
up: (record: any) => {
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
props: {
|
||||||
|
...record.props,
|
||||||
|
handles: objectMapFromEntries(
|
||||||
|
Object.values(record.props.handles).map((handle: any) => [
|
||||||
|
handle.index,
|
||||||
|
{
|
||||||
|
x: handle.x,
|
||||||
|
y: handle.y,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
down: (record: any) => {
|
||||||
|
const handles = Object.entries(record.props.handles)
|
||||||
|
.map(([index, handle]: any) => ({ index, ...handle }))
|
||||||
|
.sort(sortByIndex)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
props: {
|
||||||
|
...record.props,
|
||||||
|
handles: Object.fromEntries(
|
||||||
|
handles.map((handle, i) => {
|
||||||
|
const id =
|
||||||
|
i === 0 ? 'start' : i === handles.length - 1 ? 'end' : `handle:${handle.index}`
|
||||||
|
return [
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
type: 'vertex',
|
||||||
|
canBind: false,
|
||||||
|
canSnap: true,
|
||||||
|
index: handle.index,
|
||||||
|
x: handle.x,
|
||||||
|
y: handle.y,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue