kopia lustrzana https://github.com/Tldraw/Tldraw
[Snapping 5/5] Better handle snapping for geo shapes (#2845)
Currently, geo shapes have slightly janky handle-snapping: they snap to label geometry (even though its invisible) and because they extend from `BaseBoxShapeUtil` they snap to the corners of their bounding box (even if that's not where the actual shape is). With this PR, we no longer snap to labels, and we snap to the actual vertices of the geo shape rather than its bounding points. 1. #2827 2. #2831 3. #2793 4. #2841 5. #2845 (you are here) ### Change Type - [x] `minor` — New feature ### Test Plan - [x] Unit Tests ### Release Notes - You can now snap the handles of lines to the corners of rectangles, stars, triangles, etc.pull/2843/head^2
rodzic
89881397b5
commit
31a2b2115f
|
@ -21,6 +21,7 @@ import { EmbedDefinition } from '@tldraw/editor';
|
|||
import { EnumStyleProp } from '@tldraw/editor';
|
||||
import { Geometry2d } from '@tldraw/editor';
|
||||
import { Group2d } from '@tldraw/editor';
|
||||
import { HandleSnapGeometry } from '@tldraw/editor';
|
||||
import { IndexKey } from '@tldraw/editor';
|
||||
import { JsonObject } from '@tldraw/editor';
|
||||
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
||||
|
@ -607,7 +608,9 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
// (undocumented)
|
||||
getDefaultProps(): TLGeoShape['props'];
|
||||
// (undocumented)
|
||||
getGeometry(shape: TLGeoShape): Geometry2d;
|
||||
getGeometry(shape: TLGeoShape): Group2d;
|
||||
// (undocumented)
|
||||
getHandleSnapGeometry(shape: TLGeoShape): HandleSnapGeometry;
|
||||
// (undocumented)
|
||||
indicator(shape: TLGeoShape): JSX_2.Element;
|
||||
// (undocumented)
|
||||
|
|
|
@ -7580,8 +7580,8 @@
|
|||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Geometry2d",
|
||||
"canonicalReference": "@tldraw/editor!Geometry2d:class"
|
||||
"text": "Group2d",
|
||||
"canonicalReference": "@tldraw/editor!Group2d:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -7610,6 +7610,56 @@
|
|||
"isAbstract": false,
|
||||
"name": "getGeometry"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/tldraw!GeoShapeUtil#getHandleSnapGeometry:member(1)",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getHandleSnapGeometry(shape: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLGeoShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLGeoShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "HandleSnapGeometry",
|
||||
"canonicalReference": "@tldraw/editor!HandleSnapGeometry:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "shape",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "getHandleSnapGeometry"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/tldraw!GeoShapeUtil#indicator:member(1)",
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import { Group2d, IndexKey, TLShapeId } from '@tldraw/editor'
|
||||
import { TestEditor } from '../../../test/TestEditor'
|
||||
import { TL } from '../../../test/test-jsx'
|
||||
|
||||
let editor: TestEditor
|
||||
let ids: Record<string, TLShapeId>
|
||||
|
||||
beforeEach(() => {
|
||||
editor = new TestEditor()
|
||||
})
|
||||
|
||||
describe('Handle snapping', () => {
|
||||
beforeEach(() => {
|
||||
ids = editor.createShapesFromJsx([
|
||||
<TL.geo ref="geo" x={0} y={0} geo="rectangle" w={100} h={100} />,
|
||||
<TL.line
|
||||
ref="line"
|
||||
x={0}
|
||||
y={0}
|
||||
handles={{ ['a1' as IndexKey]: { x: 200, y: 0 }, ['a2' as IndexKey]: { x: 200, y: 100 } }}
|
||||
/>,
|
||||
])
|
||||
})
|
||||
|
||||
const geoShape = () => editor.getShape(ids.geo)!
|
||||
const lineShape = () => editor.getShape(ids.line)!
|
||||
const lineHandles = () => editor.getShapeUtil('line').getHandles!(lineShape())!
|
||||
|
||||
function startDraggingHandle() {
|
||||
editor
|
||||
.select(ids.line)
|
||||
.pointerDown(200, 0, { target: 'handle', shape: lineShape(), handle: lineHandles()[0] })
|
||||
}
|
||||
|
||||
test('handles snap to the edges of the shape', () => {
|
||||
startDraggingHandle()
|
||||
editor.pointerMove(50, 5, undefined, { ctrlKey: true })
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
expect(lineHandles()[0]).toMatchObject({ x: 50, y: 0 })
|
||||
})
|
||||
|
||||
test('handles snap to the corner of the shape', () => {
|
||||
startDraggingHandle()
|
||||
editor.pointerMove(0, 5, undefined, { ctrlKey: true })
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
expect(lineHandles()[0]).toMatchObject({ x: 0, y: 0 })
|
||||
})
|
||||
|
||||
test('handles snap to the center of the shape', () => {
|
||||
startDraggingHandle()
|
||||
editor.pointerMove(51, 45, undefined, { ctrlKey: true })
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(1)
|
||||
expect(lineHandles()[0]).toMatchObject({ x: 50, y: 50 })
|
||||
})
|
||||
|
||||
test('does not snap to the label of the shape', () => {
|
||||
startDraggingHandle()
|
||||
const geometry = editor.getShapeUtil('geo').getGeometry(geoShape()) as Group2d
|
||||
const label = geometry.children.find((c) => c.isLabel)!
|
||||
const labelVertex = label.vertices[0]
|
||||
editor.pointerMove(labelVertex.x + 2, labelVertex.y + 2, undefined, { ctrlKey: true })
|
||||
expect(editor.snaps.getIndicators()).toHaveLength(0)
|
||||
expect(lineHandles()[0]).toMatchObject({ x: labelVertex.x + 2, y: labelVertex.y + 2 })
|
||||
})
|
||||
})
|
|
@ -7,6 +7,7 @@ import {
|
|||
Group2d,
|
||||
HALF_PI,
|
||||
HTMLContainer,
|
||||
HandleSnapGeometry,
|
||||
PI2,
|
||||
Polygon2d,
|
||||
Polyline2d,
|
||||
|
@ -21,6 +22,7 @@ import {
|
|||
TLShapeUtilCanvasSvgDef,
|
||||
Vec,
|
||||
VecLike,
|
||||
exhaustiveSwitchError,
|
||||
geoShapeMigrations,
|
||||
geoShapeProps,
|
||||
getDefaultColorTheme,
|
||||
|
@ -89,7 +91,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
}
|
||||
}
|
||||
|
||||
override getGeometry(shape: TLGeoShape): Geometry2d {
|
||||
override getGeometry(shape: TLGeoShape) {
|
||||
const w = Math.max(1, shape.props.w)
|
||||
const h = Math.max(1, shape.props.h + shape.props.growY)
|
||||
const cx = w / 2
|
||||
|
@ -339,6 +341,39 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
})
|
||||
}
|
||||
|
||||
override getHandleSnapGeometry(shape: TLGeoShape): HandleSnapGeometry {
|
||||
const geometry = this.getGeometry(shape)
|
||||
// we only want to snap handles to the outline of the shape - not to its label etc.
|
||||
const outline = geometry.children[0]
|
||||
switch (shape.props.geo) {
|
||||
case 'arrow-down':
|
||||
case 'arrow-left':
|
||||
case 'arrow-right':
|
||||
case 'arrow-up':
|
||||
case 'check-box':
|
||||
case 'diamond':
|
||||
case 'hexagon':
|
||||
case 'octagon':
|
||||
case 'pentagon':
|
||||
case 'rectangle':
|
||||
case 'rhombus':
|
||||
case 'rhombus-2':
|
||||
case 'star':
|
||||
case 'trapezoid':
|
||||
case 'triangle':
|
||||
case 'x-box':
|
||||
// poly-line type shapes hand snap points for each vertex & the center
|
||||
return { outline: outline, points: [...outline.getVertices(), geometry.bounds.center] }
|
||||
case 'cloud':
|
||||
case 'ellipse':
|
||||
case 'oval':
|
||||
// blobby shapes only have a snap point in their center
|
||||
return { outline: outline, points: [geometry.bounds.center] }
|
||||
default:
|
||||
exhaustiveSwitchError(shape.props.geo)
|
||||
}
|
||||
}
|
||||
|
||||
override onEditEnd: TLOnEditEndHandler<TLGeoShape> = (shape) => {
|
||||
const {
|
||||
id,
|
||||
|
|
Ładowanie…
Reference in New Issue