Start grid point arrow align

This commit is contained in:
Mark Tolmacs 2025-03-07 13:03:19 +01:00
parent 40f25180ea
commit 5947af5b50
2 changed files with 148 additions and 117 deletions

View file

@ -238,14 +238,15 @@ export class LinearElementEditor {
}); });
} }
static getOutlineAvoidingPointOrNull( static getOutlineAvoidingPoint(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
coords: { x: number; y: number }, coords: GlobalPoint,
pointIndex: number, pointIndex: number,
app: AppClassProperties, app: AppClassProperties,
) { fallback?: GlobalPoint,
): GlobalPoint {
const hoveredElement = getHoveredElementForBinding( const hoveredElement = getHoveredElementForBinding(
coords, { x: coords[0], y: coords[1] },
app.scene.getNonDeletedElements(), app.scene.getNonDeletedElements(),
app.scene.getNonDeletedElementsMap(), app.scene.getNonDeletedElementsMap(),
app.state.zoom, app.state.zoom,
@ -254,11 +255,10 @@ export class LinearElementEditor {
); );
if (hoveredElement) { if (hoveredElement) {
const p = pointFrom<GlobalPoint>(coords.x, coords.y);
const newPoints = Array.from(element.points); const newPoints = Array.from(element.points);
newPoints[pointIndex] = pointFrom<LocalPoint>( newPoints[pointIndex] = pointFrom<LocalPoint>(
p[0] - element.x, coords[0] - element.x,
p[1] - element.y, coords[1] - element.y,
); );
return bindPointToSnapToElementOutline( return bindPointToSnapToElementOutline(
@ -272,27 +272,7 @@ export class LinearElementEditor {
); );
} }
return null; return fallback ?? coords;
}
static getOutlineAvoidingPoint(
element: NonDeleted<ExcalidrawLinearElement>,
coords: { x: number; y: number },
pointIndex: number,
app: AppClassProperties,
): GlobalPoint {
const p = LinearElementEditor.getOutlineAvoidingPointOrNull(
element,
coords,
pointIndex,
app,
);
if (p) {
return p;
}
return pointFrom<GlobalPoint>(coords.x, coords.y);
} }
/** /**
@ -411,10 +391,10 @@ export class LinearElementEditor {
globalNewPointPosition = globalNewPointPosition =
LinearElementEditor.getOutlineAvoidingPoint( LinearElementEditor.getOutlineAvoidingPoint(
element, element,
{ pointFrom<GlobalPoint>(
x: element.x + element.points[pointIndex][0] + deltaX, element.x + element.points[pointIndex][0] + deltaX,
y: element.y + element.points[pointIndex][1] + deltaY, element.y + element.points[pointIndex][1] + deltaY,
}, ),
pointIndex, pointIndex,
app, app,
); );

View file

@ -5966,32 +5966,26 @@ class App extends React.Component<AppProps, AppState> {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
} }
const outlineGlobalPoint =
LinearElementEditor.getOutlineAvoidingPointOrNull(
multiElement,
{
x: scenePointerX,
y: scenePointerY,
},
multiElement.points.length - 1,
this,
);
const nextPoint = outlineGlobalPoint
? pointFrom<LocalPoint>(
outlineGlobalPoint[0] - rx,
outlineGlobalPoint[1] - ry,
)
: pointFrom<LocalPoint>(
lastCommittedX + dxFromLastCommitted,
lastCommittedY + dyFromLastCommitted,
);
// update last uncommitted point // update last uncommitted point
mutateElement( mutateElement(
multiElement, multiElement,
{ {
points: [...points.slice(0, -1), nextPoint], points: [
...points.slice(0, -1),
pointTranslate<GlobalPoint, LocalPoint>(
LinearElementEditor.getOutlineAvoidingPoint(
multiElement,
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
multiElement.points.length - 1,
this,
pointFrom<GlobalPoint>(
multiElement.x + lastCommittedX + dxFromLastCommitted,
multiElement.y + lastCommittedY + dyFromLastCommitted,
),
),
vector(-multiElement.x, -multiElement.y),
),
],
}, },
false, false,
{ {
@ -7802,53 +7796,93 @@ class App extends React.Component<AppProps, AppState> {
? [currentItemStartArrowhead, currentItemEndArrowhead] ? [currentItemStartArrowhead, currentItemEndArrowhead]
: [null, null]; : [null, null];
const element = let element: NonDeleted<ExcalidrawLinearElement>;
elementType === "arrow" if (elementType === "arrow") {
? newArrowElement({ const arrow: Mutable<NonDeleted<ExcalidrawArrowElement>> =
type: elementType, newArrowElement({
x: gridX, type: "arrow",
y: gridY, x: gridX,
strokeColor: this.state.currentItemStrokeColor, y: gridY,
backgroundColor: this.state.currentItemBackgroundColor, strokeColor: this.state.currentItemStrokeColor,
fillStyle: this.state.currentItemFillStyle, backgroundColor: this.state.currentItemBackgroundColor,
strokeWidth: this.state.currentItemStrokeWidth, fillStyle: this.state.currentItemFillStyle,
strokeStyle: this.state.currentItemStrokeStyle, strokeWidth: this.state.currentItemStrokeWidth,
roughness: this.state.currentItemRoughness, strokeStyle: this.state.currentItemStrokeStyle,
opacity: this.state.currentItemOpacity, roughness: this.state.currentItemRoughness,
roundness: opacity: this.state.currentItemOpacity,
this.state.currentItemArrowType === ARROW_TYPE.round roundness:
? { type: ROUNDNESS.PROPORTIONAL_RADIUS } this.state.currentItemArrowType === ARROW_TYPE.round
: // note, roundness doesn't have any effect for elbow arrows, ? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
// but it's best to set it to null as well : // note, roundness doesn't have any effect for elbow arrows,
null, // but it's best to set it to null as well
startArrowhead, null,
endArrowhead, startArrowhead,
locked: false, endArrowhead,
frameId: topLayerFrame ? topLayerFrame.id : null, locked: false,
elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow, frameId: topLayerFrame ? topLayerFrame.id : null,
fixedSegments: elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
this.state.currentItemArrowType === ARROW_TYPE.elbow fixedSegments:
? [] this.state.currentItemArrowType === ARROW_TYPE.elbow ? [] : null,
: null, });
})
: newLinearElement({ const hoveredElement = getHoveredElementForBinding(
type: elementType, { x: gridX, y: gridY },
x: gridX, this.scene.getNonDeletedElements(),
y: gridY, this.scene.getNonDeletedElementsMap(),
strokeColor: this.state.currentItemStrokeColor, this.state.zoom,
backgroundColor: this.state.currentItemBackgroundColor, true,
fillStyle: this.state.currentItemFillStyle, this.state.currentItemArrowType === ARROW_TYPE.elbow,
strokeWidth: this.state.currentItemStrokeWidth, );
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness, if (hoveredElement) {
opacity: this.state.currentItemOpacity, [arrow.x, arrow.y] =
roundness: intersectElementWithLineSegment(
this.state.currentItemRoundness === "round" hoveredElement,
? { type: ROUNDNESS.PROPORTIONAL_RADIUS } lineSegment(
: null, pointFrom<GlobalPoint>(gridX, gridY),
locked: false, pointFrom<GlobalPoint>(
frameId: topLayerFrame ? topLayerFrame.id : null, gridX,
}); hoveredElement.y + hoveredElement.height / 2,
),
),
2 * FIXED_BINDING_DISTANCE,
)[0] ??
intersectElementWithLineSegment(
hoveredElement,
lineSegment(
pointFrom<GlobalPoint>(gridX, gridY),
pointFrom<GlobalPoint>(
hoveredElement.x + hoveredElement.width / 2,
gridY,
),
),
2 * FIXED_BINDING_DISTANCE,
)[0] ??
pointFrom<GlobalPoint>(gridX, gridY);
}
element = arrow;
} else {
element = newLinearElement({
type: elementType,
x: gridX,
y: gridY,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
roundness:
this.state.currentItemRoundness === "round"
? { type: ROUNDNESS.PROPORTIONAL_RADIUS }
: null,
locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null,
});
}
this.setState((prevState) => { this.setState((prevState) => {
const nextSelectedElementIds = { const nextSelectedElementIds = {
...prevState.selectedElementIds, ...prevState.selectedElementIds,
@ -8163,12 +8197,6 @@ class App extends React.Component<AppProps, AppState> {
this.laserTrails.addPointToPath(pointerCoords.x, pointerCoords.y); this.laserTrails.addPointToPath(pointerCoords.x, pointerCoords.y);
} }
const [gridX, gridY] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
);
// for arrows/lines, don't start dragging until a given threshold // for arrows/lines, don't start dragging until a given threshold
// to ensure we don't create a 2-point arrow by mistake when // to ensure we don't create a 2-point arrow by mistake when
// user clicks mouse in a way that it moves a tiny bit (thus // user clicks mouse in a way that it moves a tiny bit (thus
@ -8610,6 +8638,11 @@ class App extends React.Component<AppProps, AppState> {
} else if (isLinearElement(newElement)) { } else if (isLinearElement(newElement)) {
pointerDownState.drag.hasOccurred = true; pointerDownState.drag.hasOccurred = true;
const points = newElement.points; const points = newElement.points;
const [gridX, gridY] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize(),
);
let dx = gridX - newElement.x; let dx = gridX - newElement.x;
let dy = gridY - newElement.y; let dy = gridY - newElement.y;
@ -8626,7 +8659,22 @@ class App extends React.Component<AppProps, AppState> {
mutateElement( mutateElement(
newElement, newElement,
{ {
points: [...points, pointFrom<LocalPoint>(dx, dy)], points: [
...points,
pointTranslate<GlobalPoint, LocalPoint>(
LinearElementEditor.getOutlineAvoidingPoint(
newElement,
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
newElement.points.length - 1,
this,
pointFrom<GlobalPoint>(
newElement.x + dx,
newElement.y + dy,
),
),
vector(-newElement.x, -newElement.y),
),
],
}, },
false, false,
); );
@ -8634,20 +8682,23 @@ class App extends React.Component<AppProps, AppState> {
points.length === 2 || points.length === 2 ||
(points.length > 1 && isElbowArrow(newElement)) (points.length > 1 && isElbowArrow(newElement))
) { ) {
const globalPoint = LinearElementEditor.getOutlineAvoidingPoint(
newElement,
{ x: newElement.x + dx, y: newElement.y + dy },
1,
this,
);
mutateElement( mutateElement(
newElement, newElement,
{ {
points: [ points: [
...points.slice(0, -1), ...points.slice(0, -1),
pointFrom<LocalPoint>( pointTranslate<GlobalPoint, LocalPoint>(
globalPoint[0] - newElement.x, LinearElementEditor.getOutlineAvoidingPoint(
globalPoint[1] - newElement.y, newElement,
pointFrom<GlobalPoint>(pointerCoords.x, pointerCoords.y),
newElement.points.length - 1,
this,
pointFrom<GlobalPoint>(
newElement.x + dx,
newElement.y + dy,
),
),
vector(-newElement.x, -newElement.y),
), ),
], ],
}, },