diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 447f8783a..59585c644 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -14,7 +14,6 @@ import { Box } from '@tldraw/editor'; import { Circle2d } from '@tldraw/editor'; import { ComponentType } from 'react'; import { CubicSpline2d } from '@tldraw/editor'; -import { DictValidator } from '@tldraw/editor'; import { Editor } from '@tldraw/editor'; import { EMBED_DEFINITIONS } from '@tldraw/editor'; import { EmbedDefinition } from '@tldraw/editor'; @@ -893,7 +892,7 @@ export class LineShapeUtil extends ShapeUtil { dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">; size: EnumStyleProp<"l" | "m" | "s" | "xl">; spline: EnumStyleProp<"cubic" | "line">; - handles: DictValidator; + points: ArrayOfValidator; }; // (undocumented) toSvg(shape: TLLineShape, ctx: SvgExportContext): SVGGElement; diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index c72832809..be8f1b537 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -10695,26 +10695,17 @@ }, { "kind": "Content", - "text": "<\"cubic\" | \"line\">;\n handles: import(\"@tldraw/editor\")." + "text": "<\"cubic\" | \"line\">;\n points: import(\"@tldraw/editor\")." }, { "kind": "Reference", - "text": "DictValidator", - "canonicalReference": "@tldraw/validate!DictValidator:class" + "text": "ArrayOfValidator", + "canonicalReference": "@tldraw/validate!ArrayOfValidator:class" }, { "kind": "Content", "text": " { ref="line" x={0} y={0} - handles={{ ['a1' as IndexKey]: { x: 200, y: 0 }, ['a2' as IndexKey]: { x: 200, y: 100 } }} + points={[ + { x: 200, y: 0 }, + { x: 200, y: 100 }, + ]} />, ]) }) diff --git a/packages/tldraw/src/lib/shapes/line/LineShapeTool.test.ts b/packages/tldraw/src/lib/shapes/line/LineShapeTool.test.ts index 93b881801..89ea548b8 100644 --- a/packages/tldraw/src/lib/shapes/line/LineShapeTool.test.ts +++ b/packages/tldraw/src/lib/shapes/line/LineShapeTool.test.ts @@ -65,10 +65,10 @@ describe('When dragging the line', () => { x: 0, y: 0, props: { - handles: { - a1: { x: 0, y: 0 }, - a2: { x: 10, y: 10 }, - }, + points: [ + { x: 0, y: 0 }, + { x: 10, y: 10 }, + ], }, }) editor.expectToBeIn('select.dragging_handle') @@ -128,8 +128,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => { const line = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1] assert(editor.isShapeOfType(line, 'line')) - const handles = Object.values(line.props.handles) - expect(handles.length).toBe(3) + expect(line.props.points.length).toBe(3) }) it('extends a line after a click by shift-click dragging', () => { @@ -145,8 +144,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => { const line = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1] assert(editor.isShapeOfType(line, 'line')) - const handles = Object.values(line.props.handles) - expect(handles.length).toBe(2) + expect(line.props.points.length).toBe(2) }) it('extends a line by shift-click dragging', () => { @@ -163,8 +161,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => { const line = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1] assert(editor.isShapeOfType(line, 'line')) - const handles = Object.values(line.props.handles) - expect(handles.length).toBe(3) + expect(line.props.points.length).toBe(3) }) it('extends a line by shift-clicking even after canceling a pointerdown', () => { @@ -183,8 +180,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => { const line = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1] assert(editor.isShapeOfType(line, 'line')) - const handles = Object.values(line.props.handles) - expect(handles.length).toBe(3) + expect(line.props.points.length).toBe(3) }) it('extends a line by shift-clicking even after canceling a pointermove', () => { @@ -205,8 +201,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => { const line = editor.getCurrentPageShapes()[editor.getCurrentPageShapes().length - 1] assert(editor.isShapeOfType(line, 'line')) - const handles = Object.values(line.props.handles) - expect(handles.length).toBe(3) + expect(line.props.points.length).toBe(3) }) }) diff --git a/packages/tldraw/src/lib/shapes/line/LineShapeUtil.test.tsx b/packages/tldraw/src/lib/shapes/line/LineShapeUtil.test.tsx index 082dd69e1..88efee2e7 100644 --- a/packages/tldraw/src/lib/shapes/line/LineShapeUtil.test.tsx +++ b/packages/tldraw/src/lib/shapes/line/LineShapeUtil.test.tsx @@ -1,7 +1,6 @@ -import { IndexKey, TLGeoShape, TLLineShape, createShapeId, deepCopy } from '@tldraw/editor' +import { TLGeoShape, TLLineShape, createShapeId, deepCopy, sortByIndex } from '@tldraw/editor' import { TestEditor } from '../../../test/TestEditor' import { TL } from '../../../test/test-jsx' -import { LineShapeUtil } from './LineShapeUtil' jest.mock('nanoid', () => { let i = 0 @@ -18,37 +17,31 @@ beforeEach(() => { editor .selectAll() .deleteShapes(editor.getSelectedShapeIds()) - .createShapes([ + .createShapes([ { id: id, type: 'line', x: 150, y: 150, props: { - handles: { - a1: { - x: 0, - y: 0, - }, - a2: { - x: 100, - y: 100, - }, - }, + points: [ + { x: 0, y: 0 }, + { x: 100, y: 100 }, + ], }, }, ]) }) const getShape = () => editor.getShape(id)! -const getHandles = () => (editor.getShapeUtil('line') as LineShapeUtil).getHandles(getShape()) +const getHandles = () => editor.getShapeHandles(id)! describe('Translating', () => { it('updates the line', () => { editor.select(id) editor.pointerDown(25, 25, { target: 'shape', shape: getShape() }) editor.pointerMove(50, 50) // Move shape by 25, 25 - editor.expectShapeToMatch({ + editor.expectShapeToMatch({ id: id, x: 175, y: 175, @@ -64,7 +57,7 @@ describe('Translating', () => { editor.pointerDown(250, 250, { target: 'shape', shape: shape }) editor.pointerMove(300, 400) // Move shape by 50, 150 - editor.expectShapeToMatch({ + editor.expectShapeToMatch({ id: id, x: 200, y: 300, @@ -79,23 +72,19 @@ describe('Mid-point handles', () => { editor.pointerDown(200, 200, { target: 'handle', shape: getShape(), - handle: { - id: 'mid-0', - type: 'create', - index: 'a1V' as IndexKey, - x: 50, - y: 50, - }, + handle: getHandles()[1], }) editor.pointerMove(349, 349).pointerMove(350, 350) // Move handle by 150, 150 editor.pointerUp() - editor.expectShapeToMatch({ + editor.expectShapeToMatch({ id: id, props: { - handles: { - a1V: { x: 200, y: 200 }, - }, + points: [ + { x: 0, y: 0 }, + { x: 200, y: 200 }, + { x: 100, y: 100 }, + ], }, }) }) @@ -109,33 +98,35 @@ describe('Mid-point handles', () => { .pointerDown(200, 200, { target: 'handle', shape: getShape(), - handle: getHandles().find((h) => h.id === 'mid-0')!, + handle: getHandles()[1], }) .pointerMove(198, 230, undefined, { ctrlKey: true }) expect(editor.snaps.getIndicators()).toHaveLength(1) - editor.expectShapeToMatch({ - id: id, - props: { - handles: { - a1V: { x: 50, y: 80 }, - }, - }, - }) + expect(editor.getShapeHandles(id)).toHaveLength(5) // 3 real + 2 + const points = editor.getShape(id)!.props.points + expect(points).toHaveLength(3) + expect(points[0]).toMatchObject({ x: 0, y: 0 }) + expect(points[1]).toMatchObject({ x: 50, y: 80 }) + expect(points[2]).toMatchObject({ x: 100, y: 100 }) }) it('allows snapping with created mid-point handles', () => { editor.createShapesFromJsx([]) - editor.select(id) + + // 2 actual points, plus 1 mid-points: + expect(getHandles()).toHaveLength(3) // use a mid-point handle to create a new handle editor + .select(id) .pointerDown(200, 200, { target: 'handle', shape: getShape(), - handle: getHandles().find((h) => h.id === 'mid-0')!, + handle: getHandles().sort(sortByIndex)[1]!, }) .pointerMove(230, 200) + .pointerMove(240, 200) .pointerMove(200, 200) .pointerUp() @@ -147,19 +138,17 @@ describe('Mid-point handles', () => { .pointerDown(200, 200, { target: 'handle', shape: getShape(), - handle: getHandles().find((h) => h.id === 'a1V')!, + handle: getHandles().sort(sortByIndex)[2], }) .pointerMove(198, 230, undefined, { ctrlKey: true }) expect(editor.snaps.getIndicators()).toHaveLength(1) - editor.expectShapeToMatch({ - id: id, - props: { - handles: { - a1V: { x: 50, y: 80 }, - }, - }, - }) + expect(editor.getShapeHandles(id)).toHaveLength(5) // 3 real + 2 + const points = editor.getShape(id)!.props.points + expect(points).toHaveLength(3) + expect(points[0]).toMatchObject({ x: 0, y: 0 }) + expect(points[1]).toMatchObject({ x: 50, y: 80 }) + expect(points[2]).toMatchObject({ x: 100, y: 100 }) }) }) @@ -169,12 +158,12 @@ describe('Snapping', () => { id: id, type: 'line', props: { - handles: { - a1: { x: 0, y: 0 }, - a2: { x: 100, y: 0 }, - a3: { x: 100, y: 100 }, - a4: { x: 0, y: 100 }, - }, + points: [ + { x: 0, y: 0 }, + { x: 100, y: 0 }, + { x: 100, y: 100 }, + { x: 0, y: 100 }, + ], }, }) }) @@ -187,12 +176,15 @@ describe('Snapping', () => { .pointerMove(50, 95, undefined, { ctrlKey: true }) expect(editor.snaps.getIndicators()).toHaveLength(1) - editor.expectShapeToMatch({ + editor.expectShapeToMatch({ id: id, props: { - handles: { - a1: { x: 50, y: 100 }, - }, + points: [ + { x: 50, y: 100 }, + { x: 100, y: 0 }, + { x: 100, y: 100 }, + { x: 0, y: 100 }, + ], }, }) }) @@ -205,12 +197,15 @@ describe('Snapping', () => { .pointerMove(5, 2, undefined, { ctrlKey: true }) expect(editor.snaps.getIndicators()).toHaveLength(0) - editor.expectShapeToMatch({ + editor.expectShapeToMatch({ id: id, props: { - handles: { - a1: { x: 5, y: 2 }, - }, + points: [ + { x: 5, y: 2 }, + { x: 100, y: 0 }, + { x: 100, y: 100 }, + { x: 0, y: 100 }, + ], }, }) }) @@ -220,20 +215,31 @@ describe('Snapping', () => { , ]) editor.select(id) + const handle = getHandles()[0] editor - .pointerDown(0, 0, { target: 'handle', shape: getShape(), handle: getHandles()[0] }) + .pointerDown(handle.x, handle.y, { target: 'handle', shape: getShape(), handle }) .pointerMove(205, 1, undefined, { ctrlKey: true }) expect(editor.snaps.getIndicators()).toHaveLength(1) - editor.expectShapeToMatch({ + editor.expectShapeToMatch({ id: id, - props: { handles: { a1: { x: 200, y: 0 } } }, + props: { + points: [ + { x: 200, y: 0 }, + { x: 100, y: 0 }, + { x: 100, y: 100 }, + { x: 0, y: 100 }, + ], + }, }) }) }) @@ -242,7 +248,7 @@ describe('Misc', () => { it('preserves handle positions on spline type change', () => { editor.select(id) const shape = getShape() - const prevHandles = deepCopy(shape.props.handles) + const prevPoints = deepCopy(shape.props.points) editor.updateShapes([ { @@ -253,11 +259,11 @@ describe('Misc', () => { }, ]) - editor.expectShapeToMatch({ + editor.expectShapeToMatch({ id, props: { spline: 'cubic', - handles: prevHandles, + points: prevPoints, }, }) }) @@ -277,7 +283,7 @@ describe('Misc', () => { editor.select(id) editor.nudgeShapes(editor.getSelectedShapeIds(), { x: 1, y: 0 }) - editor.expectShapeToMatch({ + editor.expectShapeToMatch({ id: id, x: 151, y: 150, @@ -285,7 +291,7 @@ describe('Misc', () => { editor.nudgeShapes(editor.getSelectedShapeIds(), { x: 0, y: 10 }) - editor.expectShapeToMatch({ + editor.expectShapeToMatch({ id: id, x: 151, y: 160, diff --git a/packages/tldraw/src/lib/shapes/line/LineShapeUtil.tsx b/packages/tldraw/src/lib/shapes/line/LineShapeUtil.tsx index 509e26833..4e9ba62e7 100644 --- a/packages/tldraw/src/lib/shapes/line/LineShapeUtil.tsx +++ b/packages/tldraw/src/lib/shapes/line/LineShapeUtil.tsx @@ -11,13 +11,11 @@ import { TLOnResizeHandler, Vec, WeakMapCache, - deepCopy, + ZERO_INDEX_KEY, getDefaultColorTheme, - getIndexBetween, - getIndices, + getIndexAbove, lineShapeMigrations, lineShapeProps, - objectMapEntries, sortByIndex, } from '@tldraw/editor' @@ -47,22 +45,21 @@ export class LineShapeUtil extends ShapeUtil { override hideSelectionBoundsBg = () => true override getDefaultProps(): TLLineShape['props'] { - const [startIndex, endIndex] = getIndices(2) return { dash: 'draw', size: 'm', color: 'black', spline: 'line', - handles: { - [startIndex]: { + points: [ + { x: 0, y: 0, }, - [endIndex]: { + { x: 0.1, y: 0.1, }, - }, + ], } } @@ -73,39 +70,40 @@ export class LineShapeUtil extends ShapeUtil { override getHandles(shape: TLLineShape) { return handlesCache.get(shape.props, () => { - const handles = shape.props.handles - const spline = getGeometryForLineShape(shape) - 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: TLHandle[] = [] - // Add "create" handles between each vertex handle - for (let i = 0; i < spline.segments.length; i++) { - const segment = spline.segments[i] - const point = segment.midPoint() - const index = getIndexBetween(sortedHandles[i].index, sortedHandles[i + 1].index) + const { points } = shape.props + let index = ZERO_INDEX_KEY + + for (let i = 0; i < points.length; i++) { + const handle = points[i] results.push({ - id: `mid-${i}`, - type: 'create', + ...handle, + id: index, index, - x: point.x, - y: point.y, - canSnap: true, + type: 'vertex', canBind: false, + canSnap: true, }) + index = getIndexAbove(index) + + if (i < points.length - 1) { + const segment = spline.segments[i] + const point = segment.midPoint() + results.push({ + id: index, + type: 'create', + index, + x: point.x, + y: point.y, + canSnap: true, + canBind: false, + }) + index = getIndexAbove(index) + } } return results.sort(sortByIndex) @@ -122,29 +120,38 @@ export class LineShapeUtil extends ShapeUtil { override onResize: TLOnResizeHandler = (shape, info) => { const { scaleX, scaleY } = info - const handles = deepCopy(shape.props.handles) - - objectMapEntries(shape.props.handles).forEach(([index, { x, y }]) => { - handles[index].x = x * scaleX - handles[index].y = y * scaleY - }) - return { props: { - handles, + points: shape.props.points.map(({ x, y }) => { + return { + x: x * scaleX, + y: y * scaleY, + } + }), }, } } override onHandleDrag: TLOnHandleDragHandler = (shape, { handle }) => { + // we should only ever be dragging vertex handles + if (handle.type !== 'vertex') { + return shape + } + + // get the index of the point to which the vertex handle corresponds + const index = this.getHandles(shape) + .filter((h) => h.type === 'vertex') + .findIndex((h) => h.id === handle.id)! + + // splice in the new point + const points = [...shape.props.points] + points[index] = { x: handle.x, y: handle.y } + return { ...shape, props: { ...shape.props, - handles: { - ...shape.props.handles, - [handle.index]: { x: handle.x, y: handle.y }, - }, + points, }, } } @@ -384,18 +391,15 @@ export class LineShapeUtil extends ShapeUtil { override getHandleSnapGeometry(shape: TLLineShape) { return { - points: Object.values(shape.props.handles), + points: shape.props.points, } } } /** @public */ export function getGeometryForLineShape(shape: TLLineShape): CubicSpline2d | Polyline2d { - const { spline, handles } = shape.props - const handlePoints = objectMapEntries(handles) - .map(([index, position]) => ({ index, ...position })) - .sort(sortByIndex) - .map(Vec.From) + const { spline, points } = shape.props + const handlePoints = points.map(Vec.From) switch (spline) { case 'cubic': { diff --git a/packages/tldraw/src/lib/shapes/line/__snapshots__/LineShapeUtil.test.tsx.snap b/packages/tldraw/src/lib/shapes/line/__snapshots__/LineShapeUtil.test.tsx.snap index d1583df63..4111578e1 100644 --- a/packages/tldraw/src/lib/shapes/line/__snapshots__/LineShapeUtil.test.tsx.snap +++ b/packages/tldraw/src/lib/shapes/line/__snapshots__/LineShapeUtil.test.tsx.snap @@ -11,16 +11,16 @@ exports[`Misc resizes: line shape after resize 1`] = ` "props": { "color": "black", "dash": "draw", - "handles": { - "a1": { + "points": [ + { "x": 0, "y": 0, }, - "a2": { + { "x": 100, "y": 700, }, - }, + ], "size": "m", "spline": "line", }, diff --git a/packages/tldraw/src/lib/shapes/line/toolStates/Pointing.ts b/packages/tldraw/src/lib/shapes/line/toolStates/Pointing.ts index f026e837c..87b6729d6 100644 --- a/packages/tldraw/src/lib/shapes/line/toolStates/Pointing.ts +++ b/packages/tldraw/src/lib/shapes/line/toolStates/Pointing.ts @@ -1,5 +1,4 @@ import { - IndexKey, Mat, StateNode, TLEventHandlers, @@ -7,9 +6,7 @@ import { TLLineShape, TLShapeId, Vec, - VecModel, createShapeId, - getIndexAbove, last, sortByIndex, structuredClone, @@ -51,39 +48,26 @@ export class Pointing extends StateNode { new Vec(this.shape.x, this.shape.y) ) - let nextEndHandleIndex: IndexKey, nextEndHandle: VecModel - - const nextPoint = Vec.Sub(currentPagePoint, shapePagePoint) + const nextPoint = Vec.Sub(currentPagePoint, shapePagePoint).addXY(0.1, 0.1) + const points = structuredClone(this.shape.props.points) if ( Vec.Dist(endHandle, prevEndHandle) < MINIMUM_DISTANCE_BETWEEN_SHIFT_CLICKED_HANDLES || Vec.Dist(nextPoint, endHandle) < MINIMUM_DISTANCE_BETWEEN_SHIFT_CLICKED_HANDLES ) { - // If the end handle is too close to the previous end handle, we'll just extend the previous end handle - nextEndHandleIndex = endHandle.index - nextEndHandle = { - x: nextPoint.x + 0.1, - y: nextPoint.y + 0.1, - } + // Don't add a new point if the distance between the last two points is too small + points[points.length - 1] = nextPoint.toJson() } else { - // Otherwise, we'll create a new end handle - nextEndHandleIndex = getIndexAbove(endHandle.index) - nextEndHandle = { - x: nextPoint.x + 0.1, - y: nextPoint.y + 0.1, - } + // Add a new point + points.push(nextPoint.toJson()) } - const nextHandles = structuredClone(this.shape.props.handles) - - nextHandles[nextEndHandleIndex] = nextEndHandle - this.editor.updateShapes([ { id: this.shape.id, type: this.shape.type, props: { - handles: nextHandles, + points, }, }, ]) diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx b/packages/tldraw/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx index 9a052ed8a..c0f2fcdc8 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx @@ -8,13 +8,14 @@ import { TLEventHandlers, TLHandle, TLKeyboardEvent, + TLLineShape, TLPointerEventInfo, TLShapeId, TLShapePartial, Vec, - deepCopy, snapAngle, sortByIndex, + structuredClone, } from '@tldraw/editor' export class DraggingHandle extends StateNode { @@ -30,7 +31,7 @@ export class DraggingHandle extends StateNode { initialPageRotation: any info = {} as TLPointerEventInfo & { - shape: TLArrowShape + shape: TLArrowShape | TLLineShape target: 'handle' onInteractionEnd?: string isCreating: boolean @@ -42,7 +43,7 @@ export class DraggingHandle extends StateNode { override onEnter: TLEnterEventHandler = ( info: TLPointerEventInfo & { - shape: TLArrowShape + shape: TLArrowShape | TLLineShape target: 'handle' onInteractionEnd?: string isCreating: boolean @@ -54,7 +55,30 @@ export class DraggingHandle extends StateNode { this.shapeId = shape.id this.markId = isCreating ? `creating:${shape.id}` : 'dragging handle' if (!isCreating) this.editor.mark(this.markId) - this.initialHandle = deepCopy(handle) + + this.initialHandle = structuredClone(handle) + + if (this.editor.isShapeOfType(shape, 'line')) { + // For line shapes, if we're dragging a "create" handle, then + // create a new vertex handle at that point; and make this handle + // the handle that we're dragging. + if (this.initialHandle.type === 'create') { + const handles = this.editor.getShapeHandles(shape)! + const index = handles.indexOf(handle) + const points = structuredClone(shape.props.points) + points.splice(Math.ceil(index / 2), 0, { x: handle.x, y: handle.y }) + this.editor.updateShape({ + ...shape, + props: { + points, + }, + }) + const handlesAfter = this.editor.getShapeHandles(shape)! + const handleAfter = handlesAfter.find((h) => h.x === handle.x && h.y === handle.y)! + this.initialHandle = structuredClone(handleAfter) + } + } + this.initialPageTransform = this.editor.getShapePageTransform(shape)! this.initialPageRotation = this.initialPageTransform.rotation() this.initialPagePoint = this.editor.inputs.originPagePoint.clone() @@ -64,7 +88,6 @@ export class DraggingHandle extends StateNode { { ephemeral: true } ) - // @@ -216,6 +242,7 @@ export class DraggingHandle extends StateNode { } = editor const initial = this.info.shape + const shape = editor.getShape(shapeId) if (!shape) return const util = editor.getShapeUtil(shape) diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/PointingHandle.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/PointingHandle.ts index 765420f07..c199dc9e8 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/PointingHandle.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/PointingHandle.ts @@ -8,10 +8,13 @@ export class PointingHandle extends StateNode { override onEnter = (info: TLPointerEventInfo & { target: 'handle' }) => { this.info = info - const initialTerminal = (info.shape as TLArrowShape).props[info.handle.id as 'start' | 'end'] + const { shape } = info + if (this.editor.isShapeOfType(shape, 'arrow')) { + const initialTerminal = shape.props[info.handle.id as 'start' | 'end'] - if (initialTerminal?.type === 'binding') { - this.editor.setHintingShapes([initialTerminal.boundShapeId]) + if (initialTerminal?.type === 'binding') { + this.editor.setHintingShapes([initialTerminal.boundShapeId]) + } } this.editor.updateInstanceState( diff --git a/packages/tldraw/src/test/TestEditor.ts b/packages/tldraw/src/test/TestEditor.ts index c676e5453..e103951a1 100644 --- a/packages/tldraw/src/test/TestEditor.ts +++ b/packages/tldraw/src/test/TestEditor.ts @@ -207,7 +207,9 @@ export class TestEditor extends Editor { }).toCloselyMatchObject({ x, y, z }) } - expectShapeToMatch = (...model: RequiredKeys[]) => { + expectShapeToMatch = ( + ...model: RequiredKeys, 'id'>[] + ) => { model.forEach((model) => { const shape = this.getShape(model.id)! const next = { ...shape, ...model } diff --git a/packages/tldraw/src/test/customSnapping.test.tsx b/packages/tldraw/src/test/customSnapping.test.tsx index 3be28ef03..a4c8253e7 100644 --- a/packages/tldraw/src/test/customSnapping.test.tsx +++ b/packages/tldraw/src/test/customSnapping.test.tsx @@ -1,6 +1,5 @@ import { BaseBoxShapeUtil, - IndexKey, Polyline2d, TLAnyShapeUtilConstructor, TLBaseShape, @@ -204,10 +203,10 @@ describe('custom handle snapping', () => { ref="line" x={0} y={0} - handles={{ - ['a1' as IndexKey]: { x: 0, y: 0 }, - ['a2' as IndexKey]: { x: 100, y: 100 }, - }} + points={[ + { x: 0, y: 0 }, + { x: 100, y: 100 }, + ]} />, , ]) diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index 798070c64..459662af1 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -674,7 +674,7 @@ export const lineShapeProps: { dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">; size: EnumStyleProp<"l" | "m" | "s" | "xl">; spline: EnumStyleProp<"cubic" | "line">; - handles: T.DictValidator; + points: T.ArrayOfValidator; }; // @public (undocumented) diff --git a/packages/tlschema/api/api.json b/packages/tlschema/api/api.json index 42fca9cd5..49718853b 100644 --- a/packages/tlschema/api/api.json +++ b/packages/tlschema/api/api.json @@ -2756,25 +2756,16 @@ }, { "kind": "Content", - "text": "<\"cubic\" | \"line\">;\n handles: " + "text": "<\"cubic\" | \"line\">;\n points: " }, { "kind": "Reference", - "text": "T.DictValidator", - "canonicalReference": "@tldraw/validate!DictValidator:class" + "text": "T.ArrayOfValidator", + "canonicalReference": "@tldraw/validate!ArrayOfValidator:class" }, { "kind": "Content", - "text": " { }) }) +describe('Restore some handle props', () => { + const { up, down } = lineShapeMigrations.migrators[lineShapeVersions.HandlesToPoints] + it('up works as expected', () => { + expect( + up({ + props: { + handles: { + a1: { x: 0, y: 0 }, + a1V: { x: 76, y: 60 }, + a2: { x: 190, y: -62 }, + }, + }, + }) + ).toEqual({ + props: { + points: [ + { x: 0, y: 0 }, + { x: 76, y: 60 }, + { x: 190, y: -62 }, + ], + }, + }) + }) + it('down works as expected', () => { + expect( + down({ + props: { + points: [ + { x: 0, y: 0 }, + { x: 76, y: 60 }, + { x: 190, y: -62 }, + ], + }, + }) + ).toEqual({ + props: { + handles: { + a1: { x: 0, y: 0 }, + a2: { x: 76, y: 60 }, + a3: { x: 190, y: -62 }, + }, + }, + }) + }) +}) + /* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */ for (const migrator of allMigrators) { diff --git a/packages/tlschema/src/shapes/TLLineShape.ts b/packages/tlschema/src/shapes/TLLineShape.ts index 535a1834b..5810fc349 100644 --- a/packages/tlschema/src/shapes/TLLineShape.ts +++ b/packages/tlschema/src/shapes/TLLineShape.ts @@ -1,5 +1,5 @@ import { defineMigrations } from '@tldraw/store' -import { deepCopy, objectMapFromEntries, sortByIndex } from '@tldraw/utils' +import { IndexKey, deepCopy, getIndices, objectMapFromEntries, sortByIndex } from '@tldraw/utils' import { T } from '@tldraw/validate' import { vecModelValidator } from '../misc/geometry-types' import { StyleProp } from '../styles/StyleProp' @@ -23,7 +23,7 @@ export const lineShapeProps = { dash: DefaultDashStyle, size: DefaultSizeStyle, spline: LineShapeSplineStyle, - handles: T.dict(T.indexKey, vecModelValidator), + points: T.arrayOf(vecModelValidator), } /** @public */ @@ -36,11 +36,12 @@ export type TLLineShape = TLBaseShape<'line', TLLineShapeProps> export const lineShapeVersions = { AddSnapHandles: 1, RemoveExtraHandleProps: 2, + HandlesToPoints: 3, } as const /** @internal */ export const lineShapeMigrations = defineMigrations({ - currentVersion: lineShapeVersions.RemoveExtraHandleProps, + currentVersion: lineShapeVersions.HandlesToPoints, migrators: { [lineShapeVersions.AddSnapHandles]: { up: (record: any) => { @@ -107,5 +108,45 @@ export const lineShapeMigrations = defineMigrations({ } }, }, + [lineShapeVersions.HandlesToPoints]: { + up: (record: any) => { + const { handles, ...props } = record.props + + const sortedHandles = (Object.entries(handles) as [IndexKey, { x: number; y: number }][]) + .map(([index, { x, y }]) => ({ x, y, index })) + .sort(sortByIndex) + + return { + ...record, + props: { + ...props, + points: sortedHandles.map(({ x, y }) => ({ x, y })), + }, + } + }, + down: (record: any) => { + const { points, ...props } = record.props + const indices = getIndices(points.length) + + return { + ...record, + props: { + ...props, + handles: Object.fromEntries( + points.map((handle: { x: number; y: number }, i: number) => { + const index = indices[i] + return [ + index, + { + x: handle.x, + y: handle.y, + }, + ] + }) + ), + }, + } + }, + }, }, })