Add line IDs & fractional indexes (#2890)
In #2856, we moved changed line handles into an array of points. This introduced an issue where some concurrent operations wouldn't work because they array indexes change. We need some sort of stable way of referring to these points. Our existing fractional indexing system is a good fit. In this version, instead of making the points be a map from index to x/y, we make the points be a map from id (the index) to x/y/index/id(also index). This is "kinda silly" (steve's words) but might be more familiar to devs who are expecting maps to be keyed on IDs rather than anything else. ### Change Type - [x] `major` — Breaking change
This commit is contained in:
parent
dac814fd39
commit
fd4b5c6291
14 changed files with 274 additions and 166 deletions
|
@ -14,6 +14,7 @@ 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';
|
||||
|
@ -888,7 +889,12 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
||||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||
spline: EnumStyleProp<"cubic" | "line">;
|
||||
points: ArrayOfValidator<VecModel>;
|
||||
points: DictValidator<string, {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
index: IndexKey;
|
||||
}>;
|
||||
};
|
||||
// (undocumented)
|
||||
toSvg(shape: TLLineShape, ctx: SvgExportContext): SVGGElement;
|
||||
|
|
|
@ -10637,21 +10637,21 @@
|
|||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "ArrayOfValidator",
|
||||
"canonicalReference": "@tldraw/validate!ArrayOfValidator:class"
|
||||
"text": "DictValidator",
|
||||
"canonicalReference": "@tldraw/validate!DictValidator:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<import(\"@tldraw/editor\")."
|
||||
"text": "<string, {\n id: string;\n x: number;\n y: number;\n index: import(\"@tldraw/editor\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "VecModel",
|
||||
"canonicalReference": "@tldraw/tlschema!VecModel:interface"
|
||||
"text": "IndexKey",
|
||||
"canonicalReference": "@tldraw/utils!IndexKey:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">;\n }"
|
||||
"text": ";\n }>;\n }"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Group2d, TLShapeId } from '@tldraw/editor'
|
||||
import { Group2d, IndexKey, TLShapeId } from '@tldraw/editor'
|
||||
import { TestEditor } from '../../../test/TestEditor'
|
||||
import { TL } from '../../../test/test-jsx'
|
||||
|
||||
|
@ -17,10 +17,10 @@ describe('Handle snapping', () => {
|
|||
ref="line"
|
||||
x={0}
|
||||
y={0}
|
||||
points={[
|
||||
{ x: 200, y: 0 },
|
||||
{ x: 200, y: 100 },
|
||||
]}
|
||||
points={{
|
||||
a1: { id: 'a1', index: 'a1' as IndexKey, x: 200, y: 0 },
|
||||
a2: { id: 'a2', index: 'a2' as IndexKey, x: 200, y: 100 },
|
||||
}}
|
||||
/>,
|
||||
])
|
||||
})
|
||||
|
|
|
@ -65,10 +65,10 @@ describe('When dragging the line', () => {
|
|||
x: 0,
|
||||
y: 0,
|
||||
props: {
|
||||
points: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 10, y: 10 },
|
||||
],
|
||||
points: {
|
||||
a1: { id: 'a1', index: 'a1', x: 0, y: 0 },
|
||||
a2: { id: 'a2', index: 'a2', x: 10, y: 10 },
|
||||
},
|
||||
},
|
||||
})
|
||||
editor.expectToBeIn('select.dragging_handle')
|
||||
|
@ -128,7 +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<TLLineShape>(line, 'line'))
|
||||
expect(line.props.points.length).toBe(3)
|
||||
expect(Object.keys(line.props.points).length).toBe(3)
|
||||
})
|
||||
|
||||
it('extends a line after a click by shift-click dragging', () => {
|
||||
|
@ -144,7 +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<TLLineShape>(line, 'line'))
|
||||
expect(line.props.points.length).toBe(2)
|
||||
expect(Object.keys(line.props.points).length).toBe(2)
|
||||
})
|
||||
|
||||
it('extends a line by shift-click dragging', () => {
|
||||
|
@ -161,7 +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<TLLineShape>(line, 'line'))
|
||||
expect(line.props.points.length).toBe(3)
|
||||
expect(Object.keys(line.props.points).length).toBe(3)
|
||||
})
|
||||
|
||||
it('extends a line by shift-clicking even after canceling a pointerdown', () => {
|
||||
|
@ -180,7 +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<TLLineShape>(line, 'line'))
|
||||
expect(line.props.points.length).toBe(3)
|
||||
expect(Object.keys(line.props.points).length).toBe(3)
|
||||
})
|
||||
|
||||
it('extends a line by shift-clicking even after canceling a pointermove', () => {
|
||||
|
@ -201,7 +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<TLLineShape>(line, 'line'))
|
||||
expect(line.props.points.length).toBe(3)
|
||||
expect(Object.keys(line.props.points).length).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import { TLGeoShape, TLLineShape, createShapeId, deepCopy, sortByIndex } from '@tldraw/editor'
|
||||
import {
|
||||
IndexKey,
|
||||
TLGeoShape,
|
||||
TLLineShape,
|
||||
createShapeId,
|
||||
deepCopy,
|
||||
sortByIndex,
|
||||
} from '@tldraw/editor'
|
||||
import { TestEditor } from '../../../test/TestEditor'
|
||||
import { TL } from '../../../test/test-jsx'
|
||||
|
||||
|
@ -24,10 +31,10 @@ beforeEach(() => {
|
|||
x: 150,
|
||||
y: 150,
|
||||
props: {
|
||||
points: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 100, y: 100 },
|
||||
],
|
||||
points: {
|
||||
a1: { id: 'a1', index: 'a1' as IndexKey, x: 0, y: 0 },
|
||||
a2: { id: 'a2', index: 'a2' as IndexKey, x: 100, y: 100 },
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
@ -77,14 +84,14 @@ describe('Mid-point handles', () => {
|
|||
editor.pointerMove(349, 349).pointerMove(350, 350) // Move handle by 150, 150
|
||||
editor.pointerUp()
|
||||
|
||||
editor.expectShapeToMatch<TLLineShape>({
|
||||
editor.expectShapeToMatch({
|
||||
id: id,
|
||||
props: {
|
||||
points: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 200, y: 200 },
|
||||
{ x: 100, y: 100 },
|
||||
],
|
||||
points: {
|
||||
a1: { id: 'a1', index: 'a1', x: 0, y: 0 },
|
||||
a1V: { id: 'a1V', index: 'a1V', x: 200, y: 200 },
|
||||
a2: { id: 'a2', index: 'a2', x: 100, y: 100 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -105,10 +112,11 @@ describe('Mid-point handles', () => {
|
|||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
expect(editor.getShapeHandles(id)).toHaveLength(5) // 3 real + 2
|
||||
const points = editor.getShape<TLLineShape>(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 })
|
||||
expect(points).toStrictEqual({
|
||||
a1: { id: 'a1', index: 'a1', x: 0, y: 0 },
|
||||
a1V: { id: 'a1V', index: 'a1V', x: 50, y: 80 },
|
||||
a2: { id: 'a2', index: 'a2', x: 100, y: 100 },
|
||||
})
|
||||
})
|
||||
|
||||
it('allows snapping with created mid-point handles', () => {
|
||||
|
@ -145,10 +153,11 @@ describe('Mid-point handles', () => {
|
|||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
expect(editor.getShapeHandles(id)).toHaveLength(5) // 3 real + 2
|
||||
const points = editor.getShape<TLLineShape>(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 })
|
||||
expect(points).toStrictEqual({
|
||||
a1: { id: 'a1', index: 'a1', x: 0, y: 0 },
|
||||
a1V: { id: 'a1V', index: 'a1V', x: 50, y: 80 },
|
||||
a2: { id: 'a2', index: 'a2', x: 100, y: 100 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -158,12 +167,12 @@ describe('Snapping', () => {
|
|||
id: id,
|
||||
type: 'line',
|
||||
props: {
|
||||
points: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 100, y: 0 },
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 0, y: 100 },
|
||||
],
|
||||
points: {
|
||||
a1: { id: 'a1', index: 'a1', x: 0, y: 0 },
|
||||
a2: { id: 'a2', index: 'a2', x: 100, y: 0 },
|
||||
a3: { id: 'a3', index: 'a3', x: 100, y: 100 },
|
||||
a4: { id: 'a4', index: 'a4', x: 0, y: 100 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -176,15 +185,15 @@ describe('Snapping', () => {
|
|||
.pointerMove(50, 95, undefined, { ctrlKey: true })
|
||||
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
editor.expectShapeToMatch<TLLineShape>({
|
||||
editor.expectShapeToMatch({
|
||||
id: id,
|
||||
props: {
|
||||
points: [
|
||||
{ x: 50, y: 100 },
|
||||
{ x: 100, y: 0 },
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 0, y: 100 },
|
||||
],
|
||||
points: {
|
||||
a1: { id: 'a1', index: 'a1', x: 50, y: 100 },
|
||||
a2: { id: 'a2', index: 'a2', x: 100, y: 0 },
|
||||
a3: { id: 'a3', index: 'a3', x: 100, y: 100 },
|
||||
a4: { id: 'a4', index: 'a4', x: 0, y: 100 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -200,12 +209,12 @@ describe('Snapping', () => {
|
|||
editor.expectShapeToMatch({
|
||||
id: id,
|
||||
props: {
|
||||
points: [
|
||||
{ x: 0, y: 100 },
|
||||
{ x: 100, y: 0 },
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 0, y: 100 },
|
||||
],
|
||||
points: {
|
||||
a1: { id: 'a1', index: 'a1', x: 0, y: 100 },
|
||||
a2: { id: 'a2', index: 'a2', x: 100, y: 0 },
|
||||
a3: { id: 'a3', index: 'a3', x: 100, y: 100 },
|
||||
a4: { id: 'a4', index: 'a4', x: 0, y: 100 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -218,15 +227,15 @@ describe('Snapping', () => {
|
|||
.pointerMove(5, 2, undefined, { ctrlKey: true })
|
||||
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||
editor.expectShapeToMatch<TLLineShape>({
|
||||
editor.expectShapeToMatch({
|
||||
id: id,
|
||||
props: {
|
||||
points: [
|
||||
{ x: 5, y: 2 },
|
||||
{ x: 100, y: 0 },
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 0, y: 100 },
|
||||
],
|
||||
points: {
|
||||
a1: { id: 'a1', index: 'a1', x: 5, y: 2 },
|
||||
a2: { id: 'a2', index: 'a2', x: 100, y: 0 },
|
||||
a3: { id: 'a3', index: 'a3', x: 100, y: 100 },
|
||||
a4: { id: 'a4', index: 'a4', x: 0, y: 100 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
@ -236,10 +245,10 @@ describe('Snapping', () => {
|
|||
<TL.line
|
||||
x={150}
|
||||
y={150}
|
||||
points={[
|
||||
{ x: 200, y: 0 },
|
||||
{ x: 300, y: 0 },
|
||||
]}
|
||||
points={{
|
||||
a1: { id: 'a1', index: 'a1' as IndexKey, x: 200, y: 0 },
|
||||
a2: { id: 'a2', index: 'a2' as IndexKey, x: 300, y: 0 },
|
||||
}}
|
||||
/>,
|
||||
])
|
||||
|
||||
|
@ -251,15 +260,15 @@ describe('Snapping', () => {
|
|||
.pointerMove(205, 1, undefined, { ctrlKey: true })
|
||||
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
editor.expectShapeToMatch<TLLineShape>({
|
||||
editor.expectShapeToMatch({
|
||||
id: id,
|
||||
props: {
|
||||
points: [
|
||||
{ x: 200, y: 0 },
|
||||
{ x: 100, y: 0 },
|
||||
{ x: 100, y: 100 },
|
||||
{ x: 0, y: 100 },
|
||||
],
|
||||
points: {
|
||||
a1: { id: 'a1', index: 'a1', x: 200, y: 0 },
|
||||
a2: { id: 'a2', index: 'a2', x: 100, y: 0 },
|
||||
a3: { id: 'a3', index: 'a3', x: 100, y: 100 },
|
||||
a4: { id: 'a4', index: 'a4', x: 0, y: 100 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
@ -13,11 +13,12 @@ import {
|
|||
TLOnResizeHandler,
|
||||
Vec,
|
||||
WeakMapCache,
|
||||
ZERO_INDEX_KEY,
|
||||
getDefaultColorTheme,
|
||||
getIndexAbove,
|
||||
getIndexBetween,
|
||||
getIndices,
|
||||
lineShapeMigrations,
|
||||
lineShapeProps,
|
||||
mapObjectMapValues,
|
||||
sortByIndex,
|
||||
} from '@tldraw/editor'
|
||||
|
||||
|
@ -47,21 +48,16 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
override hideSelectionBoundsBg = () => true
|
||||
|
||||
override getDefaultProps(): TLLineShape['props'] {
|
||||
const [start, end] = getIndices(2)
|
||||
return {
|
||||
dash: 'draw',
|
||||
size: 'm',
|
||||
color: 'black',
|
||||
spline: 'line',
|
||||
points: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
x: 0.1,
|
||||
y: 0.1,
|
||||
},
|
||||
],
|
||||
points: {
|
||||
[start]: { id: start, index: start, x: 0, y: 0 },
|
||||
[end]: { id: end, index: end, x: 0.1, y: 0.1 },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,38 +70,26 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
return handlesCache.get(shape.props, () => {
|
||||
const spline = getGeometryForLineShape(shape)
|
||||
|
||||
const results: TLHandle[] = []
|
||||
const points = linePointsToArray(shape)
|
||||
const results: TLHandle[] = points.map((point) => ({
|
||||
...point,
|
||||
id: point.index,
|
||||
type: 'vertex',
|
||||
canSnap: true,
|
||||
}))
|
||||
|
||||
const { points } = shape.props
|
||||
|
||||
let index = ZERO_INDEX_KEY
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const handle = points[i]
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const index = getIndexBetween(points[i].index, points[i + 1].index)
|
||||
const segment = spline.segments[i]
|
||||
const point = segment.midPoint()
|
||||
results.push({
|
||||
...handle,
|
||||
id: index,
|
||||
type: 'create',
|
||||
index,
|
||||
type: 'vertex',
|
||||
canBind: false,
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
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)
|
||||
|
@ -119,36 +103,28 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
|
||||
return {
|
||||
props: {
|
||||
points: shape.props.points.map(({ x, y }) => {
|
||||
return {
|
||||
x: x * scaleX,
|
||||
y: y * scaleY,
|
||||
}
|
||||
}),
|
||||
points: mapObjectMapValues(shape.props.points, (_, { id, index, x, y }) => ({
|
||||
id,
|
||||
index,
|
||||
x: x * scaleX,
|
||||
y: y * scaleY,
|
||||
})),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
override onHandleDrag: TLOnHandleDragHandler<TLLineShape> = (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 }
|
||||
if (handle.type !== 'vertex') return
|
||||
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...shape.props,
|
||||
points,
|
||||
points: {
|
||||
...shape.props.points,
|
||||
[handle.id]: { id: handle.id, index: handle.index, x: handle.x, y: handle.y },
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -387,7 +363,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
}
|
||||
|
||||
override getHandleSnapGeometry(shape: TLLineShape): HandleSnapGeometry {
|
||||
const { points } = shape.props
|
||||
const points = linePointsToArray(shape)
|
||||
return {
|
||||
points,
|
||||
getSelfSnapPoints: (handle) => {
|
||||
|
@ -418,17 +394,20 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
|
|||
}
|
||||
}
|
||||
|
||||
function linePointsToArray(shape: TLLineShape) {
|
||||
return Object.values(shape.props.points).sort(sortByIndex)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function getGeometryForLineShape(shape: TLLineShape): CubicSpline2d | Polyline2d {
|
||||
const { spline, points } = shape.props
|
||||
const handlePoints = points.map(Vec.From)
|
||||
const points = linePointsToArray(shape).map(Vec.From)
|
||||
|
||||
switch (spline) {
|
||||
switch (shape.props.spline) {
|
||||
case 'cubic': {
|
||||
return new CubicSpline2d({ points: handlePoints })
|
||||
return new CubicSpline2d({ points })
|
||||
}
|
||||
case 'line': {
|
||||
return new Polyline2d({ points: handlePoints })
|
||||
return new Polyline2d({ points })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,16 +11,20 @@ exports[`Misc resizes: line shape after resize 1`] = `
|
|||
"props": {
|
||||
"color": "black",
|
||||
"dash": "draw",
|
||||
"points": [
|
||||
{
|
||||
"points": {
|
||||
"a1": {
|
||||
"id": "a1",
|
||||
"index": "a1",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
{
|
||||
"a2": {
|
||||
"id": "a2",
|
||||
"index": "a2",
|
||||
"x": 100,
|
||||
"y": 700,
|
||||
},
|
||||
],
|
||||
},
|
||||
"size": "m",
|
||||
"spline": "line",
|
||||
},
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
TLShapeId,
|
||||
Vec,
|
||||
createShapeId,
|
||||
getIndexAbove,
|
||||
last,
|
||||
sortByIndex,
|
||||
structuredClone,
|
||||
|
@ -56,10 +57,21 @@ export class Pointing extends StateNode {
|
|||
Vec.Dist(nextPoint, endHandle) < MINIMUM_DISTANCE_BETWEEN_SHIFT_CLICKED_HANDLES
|
||||
) {
|
||||
// Don't add a new point if the distance between the last two points is too small
|
||||
points[points.length - 1] = nextPoint.toJson()
|
||||
points[endHandle.id] = {
|
||||
id: endHandle.id,
|
||||
index: endHandle.index,
|
||||
x: nextPoint.x,
|
||||
y: nextPoint.y,
|
||||
}
|
||||
} else {
|
||||
// Add a new point
|
||||
points.push(nextPoint.toJson())
|
||||
const nextIndex = getIndexAbove(endHandle.index)
|
||||
points[nextIndex] = {
|
||||
id: nextIndex,
|
||||
index: nextIndex,
|
||||
x: nextPoint.x,
|
||||
y: nextPoint.y,
|
||||
}
|
||||
}
|
||||
|
||||
this.editor.updateShapes([
|
||||
|
|
|
@ -62,18 +62,17 @@ export class DraggingHandle extends StateNode {
|
|||
// 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,
|
||||
points: {
|
||||
...shape.props.points,
|
||||
[handle.index]: { id: handle.index, index: handle.index, x: handle.x, y: handle.y },
|
||||
},
|
||||
},
|
||||
})
|
||||
const handlesAfter = this.editor.getShapeHandles(shape)!
|
||||
const handleAfter = handlesAfter.find((h) => h.x === handle.x && h.y === handle.y)!
|
||||
const handleAfter = handlesAfter.find((h) => h.index === handle.index)!
|
||||
this.initialHandle = structuredClone(handleAfter)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
BaseBoxShapeUtil,
|
||||
IndexKey,
|
||||
Polyline2d,
|
||||
TLAnyShapeUtilConstructor,
|
||||
TLBaseShape,
|
||||
|
@ -238,10 +239,10 @@ describe('custom handle snapping', () => {
|
|||
ref="line"
|
||||
x={0}
|
||||
y={0}
|
||||
points={[
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 100, y: 100 },
|
||||
]}
|
||||
points={{
|
||||
a1: { id: 'a1', index: 'a1' as IndexKey, x: 0, y: 0 },
|
||||
a2: { id: 'a2', index: 'a2' as IndexKey, x: 100, y: 100 },
|
||||
}}
|
||||
/>,
|
||||
<TL.test ref="test" x={200} y={200} w={100} h={100} boundsSnapPoints={null} />,
|
||||
])
|
||||
|
|
|
@ -680,7 +680,12 @@ export const lineShapeProps: {
|
|||
dash: EnumStyleProp<"dashed" | "dotted" | "draw" | "solid">;
|
||||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||
spline: EnumStyleProp<"cubic" | "line">;
|
||||
points: T.ArrayOfValidator<VecModel>;
|
||||
points: T.DictValidator<string, {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
index: IndexKey;
|
||||
} & {}>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
|
|
@ -2787,21 +2787,21 @@
|
|||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "T.ArrayOfValidator",
|
||||
"canonicalReference": "@tldraw/validate!ArrayOfValidator:class"
|
||||
"text": "T.DictValidator",
|
||||
"canonicalReference": "@tldraw/validate!DictValidator:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<import(\"../misc/geometry-types\")."
|
||||
"text": "<string, {\n id: string;\n x: number;\n y: number;\n index: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "VecModel",
|
||||
"canonicalReference": "@tldraw/tlschema!VecModel:interface"
|
||||
"text": "IndexKey",
|
||||
"canonicalReference": "@tldraw/utils!IndexKey:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">;\n}"
|
||||
"text": ";\n } & {}>;\n}"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/tlschema/src/shapes/TLLineShape.ts",
|
||||
|
|
|
@ -1983,6 +1983,52 @@ describe('Restore some handle props', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Fractional indexing for line points', () => {
|
||||
const { up, down } = lineShapeMigrations.migrators[lineShapeVersions.PointIndexIds]
|
||||
it('up works as expected', () => {
|
||||
expect(
|
||||
up({
|
||||
props: {
|
||||
points: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 76, y: 60 },
|
||||
{ x: 190, y: -62 },
|
||||
],
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
props: {
|
||||
points: {
|
||||
a1: { id: 'a1', index: 'a1', x: 0, y: 0 },
|
||||
a2: { id: 'a2', index: 'a2', x: 76, y: 60 },
|
||||
a3: { id: 'a3', index: 'a3', x: 190, y: -62 },
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
it('down works as expected', () => {
|
||||
expect(
|
||||
down({
|
||||
props: {
|
||||
points: {
|
||||
a1: { id: 'a1', index: 'a1', x: 0, y: 0 },
|
||||
a3: { id: 'a3', index: 'a3', x: 190, y: -62 },
|
||||
a2: { id: 'a2', index: 'a2', x: 76, y: 60 },
|
||||
},
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
props: {
|
||||
points: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 76, y: 60 },
|
||||
{ x: 190, y: -62 },
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
||||
|
||||
for (const migrator of allMigrators) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { defineMigrations } from '@tldraw/store'
|
||||
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'
|
||||
import { DefaultColorStyle } from '../styles/TLColorStyle'
|
||||
import { DefaultDashStyle } from '../styles/TLDashStyle'
|
||||
|
@ -17,13 +16,20 @@ export const LineShapeSplineStyle = StyleProp.defineEnum('tldraw:spline', {
|
|||
/** @public */
|
||||
export type TLLineShapeSplineStyle = T.TypeOf<typeof LineShapeSplineStyle>
|
||||
|
||||
const lineShapePointValidator = T.object({
|
||||
id: T.string,
|
||||
index: T.indexKey,
|
||||
x: T.number,
|
||||
y: T.number,
|
||||
})
|
||||
|
||||
/** @public */
|
||||
export const lineShapeProps = {
|
||||
color: DefaultColorStyle,
|
||||
dash: DefaultDashStyle,
|
||||
size: DefaultSizeStyle,
|
||||
spline: LineShapeSplineStyle,
|
||||
points: T.arrayOf(vecModelValidator),
|
||||
points: T.dict(T.string, lineShapePointValidator),
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
@ -37,11 +43,12 @@ export const lineShapeVersions = {
|
|||
AddSnapHandles: 1,
|
||||
RemoveExtraHandleProps: 2,
|
||||
HandlesToPoints: 3,
|
||||
PointIndexIds: 4,
|
||||
} as const
|
||||
|
||||
/** @internal */
|
||||
export const lineShapeMigrations = defineMigrations({
|
||||
currentVersion: lineShapeVersions.HandlesToPoints,
|
||||
currentVersion: lineShapeVersions.PointIndexIds,
|
||||
migrators: {
|
||||
[lineShapeVersions.AddSnapHandles]: {
|
||||
up: (record: any) => {
|
||||
|
@ -148,5 +155,45 @@ export const lineShapeMigrations = defineMigrations({
|
|||
}
|
||||
},
|
||||
},
|
||||
[lineShapeVersions.PointIndexIds]: {
|
||||
up: (record: any) => {
|
||||
const { points, ...props } = record.props
|
||||
const indices = getIndices(points.length)
|
||||
|
||||
return {
|
||||
...record,
|
||||
props: {
|
||||
...props,
|
||||
points: Object.fromEntries(
|
||||
points.map((point: { x: number; y: number }, i: number) => {
|
||||
const id = indices[i]
|
||||
return [
|
||||
id,
|
||||
{
|
||||
id: id,
|
||||
index: id,
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
},
|
||||
]
|
||||
})
|
||||
),
|
||||
},
|
||||
}
|
||||
},
|
||||
down: (record: any) => {
|
||||
const sortedHandles = (
|
||||
Object.values(record.props.points) as { x: number; y: number; index: IndexKey }[]
|
||||
).sort(sortByIndex)
|
||||
|
||||
return {
|
||||
...record,
|
||||
props: {
|
||||
...record.props,
|
||||
points: sortedHandles.map(({ x, y }) => ({ x, y })),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue