feat: Focus indicator (#10613)
* feat: Focus indicator Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Snapshot update Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Move visualdebug to utils and introduce volume bindable volume visualization Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Move visualdebug to elements Due to dep circles Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Possible test timeout Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Incorrect hit test point input Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Add fallback when dragged outside of allowed area Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Elbow arrows don't need focus point mgmt Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: End bound indirect fix Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Show indicator when arrow endpoint dragging Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Update bound arrow endpoint at mid-point drag Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Refactor Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Curve endpoint intersection Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Outline focus point is reset on existing arrow drag Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Tests Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Fix lint Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Dragging focus point off Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Don't show the focus indicator when arrow endpoint is dragged Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Drag area for focus handles Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Focus point size unified Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Size bump for focus knob Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Cache hits and scene lookups Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove debug Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Consider hit threshold and inside override too Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Other shape switching Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * perf: Update tolerance params Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Focus know line width Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: knob offset Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Full overlap Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove Map caching Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: incorrect threshold Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: threshold setting Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Hit caching Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: cache override Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Snapshots Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Redesigned focus point handling Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Inside-inside mode Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * chore: Remove comment Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * feat: Allow focus knob outside the shape Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Arrow endpoint offset Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Focus knob element distance Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Increase iteration on curve intersection calc Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Handle disabled binding Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Alt mode Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Nested shape focus rewrite Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Alt + Ctrl + arrow endpoitn Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Hit ordering for focus points Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Focus point visibility Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * dry out renderFocusPointIndicator * do not higlight point when dragging & make focus point smaller * optimize retrieval of selectedLinearElement * move focus highlighting into renderFocusPointIndicator to DRY out and colocate * remove `disabled` state from focus highlight * make focus point stroke color less prominent * fix: No focus point for multi-point arrows Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * fix: Arrow edit mode drag focus point release Signed-off-by: Mark Tolmacs <mark@lazycat.hu> * DRY out arrow point-like drag * move `focus.ts` to `arrows/focus.ts` --------- Signed-off-by: Mark Tolmacs <mark@lazycat.hu> Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
@@ -250,6 +250,11 @@ import {
|
||||
maxBindingDistance_simple,
|
||||
convertToExcalidrawElements,
|
||||
type ExcalidrawElementSkeleton,
|
||||
handleFocusPointDrag,
|
||||
handleFocusPointHover,
|
||||
handleFocusPointPointerDown,
|
||||
handleFocusPointPointerUp,
|
||||
maybeHandleArrowPointlikeDrag,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
|
||||
@@ -4734,8 +4739,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
// Handle Alt key for bind mode
|
||||
if (event.key === KEYS.ALT && getFeatureFlag("COMPLEX_BINDINGS")) {
|
||||
this.handleSkipBindMode();
|
||||
if (event.key === KEYS.ALT) {
|
||||
if (getFeatureFlag("COMPLEX_BINDINGS")) {
|
||||
this.handleSkipBindMode();
|
||||
} else {
|
||||
maybeHandleArrowPointlikeDrag({ app: this, event });
|
||||
}
|
||||
}
|
||||
|
||||
if (this.actionManager.handleKeyDown(event)) {
|
||||
@@ -4751,7 +4760,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.resetDelayedBindMode();
|
||||
}
|
||||
|
||||
this.setState({ isBindingEnabled: false });
|
||||
flushSync(() => {
|
||||
this.setState({ isBindingEnabled: false });
|
||||
});
|
||||
|
||||
maybeHandleArrowPointlikeDrag({ app: this, event });
|
||||
}
|
||||
|
||||
if (isArrowKey(event.key)) {
|
||||
@@ -5024,6 +5037,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
isHoldingSpace = false;
|
||||
}
|
||||
|
||||
if (event.key === KEYS.ALT) {
|
||||
maybeHandleArrowPointlikeDrag({ app: this, event });
|
||||
}
|
||||
|
||||
if (
|
||||
(event.key === KEYS.ALT && this.state.bindMode === "skip") ||
|
||||
(!event[KEYS.CTRL_OR_CMD] && !isBindingEnabled(this.state))
|
||||
@@ -5034,7 +5052,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
|
||||
// Restart the timer if we're creating/editing a linear element and hovering over an element
|
||||
if (this.lastPointerMoveEvent) {
|
||||
if (this.lastPointerMoveEvent && getFeatureFlag("COMPLEX_BINDINGS")) {
|
||||
const scenePointer = viewportCoordsToSceneCoords(
|
||||
{
|
||||
clientX: this.lastPointerMoveEvent.clientX,
|
||||
@@ -5055,14 +5073,18 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
if (isBindingElement(element) && getFeatureFlag("COMPLEX_BINDINGS")) {
|
||||
if (isBindingElement(element)) {
|
||||
this.handleDelayedBindModeChange(element, hoveredElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!event[KEYS.CTRL_OR_CMD] && !this.state.isBindingEnabled) {
|
||||
this.setState({ isBindingEnabled: true });
|
||||
flushSync(() => {
|
||||
this.setState({ isBindingEnabled: true });
|
||||
});
|
||||
|
||||
maybeHandleArrowPointlikeDrag({ app: this, event });
|
||||
}
|
||||
if (isArrowKey(event.key)) {
|
||||
bindOrUnbindBindingElements(
|
||||
@@ -6379,7 +6401,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
(el) => maxBindingDistance_simple(this.state.zoom),
|
||||
maxBindingDistance_simple(this.state.zoom),
|
||||
);
|
||||
if (hoveredElement) {
|
||||
this.setState({
|
||||
@@ -6410,7 +6432,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
(el) => maxBindingDistance_simple(this.state.zoom),
|
||||
maxBindingDistance_simple(this.state.zoom),
|
||||
);
|
||||
if (hoveredElement) {
|
||||
this.actionManager.executeAction(actionFinalize, "ui", {
|
||||
@@ -6564,7 +6586,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointFrom<GlobalPoint>(scenePointerX, scenePointerY),
|
||||
this.scene.getNonDeletedElements(),
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
(el) => maxBindingDistance_simple(this.state.zoom),
|
||||
maxBindingDistance_simple(this.state.zoom),
|
||||
);
|
||||
if (
|
||||
hit &&
|
||||
@@ -6923,6 +6945,37 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check for focus point hover
|
||||
let hoveredFocusPointBinding: "start" | "end" | null = null;
|
||||
const arrow = element as any;
|
||||
if (arrow.startBinding || arrow.endBinding) {
|
||||
hoveredFocusPointBinding = handleFocusPointHover(
|
||||
element as ExcalidrawArrowElement,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
this.scene,
|
||||
this.state,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
this.state.selectedLinearElement.hoveredFocusPointBinding !==
|
||||
hoveredFocusPointBinding
|
||||
) {
|
||||
this.setState({
|
||||
selectedLinearElement: {
|
||||
...this.state.selectedLinearElement,
|
||||
isDragging: false,
|
||||
hoveredFocusPointBinding,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Set cursor to pointer when hovering over a focus point
|
||||
if (hoveredFocusPointBinding) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||
}
|
||||
} else {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||
}
|
||||
@@ -7844,6 +7897,37 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (ret.didAddPoint) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Also check at current pointer position if focus point is being hovered
|
||||
// (in case we're clicking directly without a prior move event)
|
||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||
const arrow = LinearElementEditor.getElement(
|
||||
linearElementEditor.elementId,
|
||||
elementsMap,
|
||||
) as any;
|
||||
|
||||
if (arrow && isBindingElement(arrow)) {
|
||||
const { hitFocusPoint, pointerOffset } =
|
||||
handleFocusPointPointerDown(
|
||||
arrow,
|
||||
pointerDownState,
|
||||
elementsMap,
|
||||
this.state,
|
||||
);
|
||||
|
||||
// If focus point is hit, update state and prevent element selection
|
||||
if (hitFocusPoint) {
|
||||
this.setState({
|
||||
selectedLinearElement: {
|
||||
...linearElementEditor,
|
||||
hoveredFocusPointBinding: hitFocusPoint,
|
||||
draggedFocusPointBinding: hitFocusPoint,
|
||||
pointerOffset,
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allHitElements = this.getElementsAtPosition(
|
||||
@@ -8991,6 +9075,31 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (this.state.selectedLinearElement) {
|
||||
const linearElementEditor = this.state.selectedLinearElement;
|
||||
|
||||
// Handle focus point dragging if needed
|
||||
if (linearElementEditor.draggedFocusPointBinding) {
|
||||
handleFocusPointDrag(
|
||||
linearElementEditor,
|
||||
elementsMap,
|
||||
pointerCoords,
|
||||
this.scene,
|
||||
this.state,
|
||||
this.getEffectiveGridSize(),
|
||||
event.altKey,
|
||||
);
|
||||
this.setState({
|
||||
selectedLinearElement: {
|
||||
...linearElementEditor,
|
||||
isDragging: false,
|
||||
selectedPointsIndices: [],
|
||||
initialState: {
|
||||
...linearElementEditor.initialState,
|
||||
lastClickedPoint: -1,
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
LinearElementEditor.shouldAddMidpoint(
|
||||
this.state.selectedLinearElement,
|
||||
@@ -9859,12 +9968,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// and sets binding element
|
||||
if (
|
||||
this.state.selectedLinearElement?.isEditing &&
|
||||
!this.state.newElement
|
||||
!this.state.newElement &&
|
||||
this.state.selectedLinearElement.draggedFocusPointBinding === null
|
||||
) {
|
||||
if (
|
||||
!pointerDownState.boxSelection.hasOccurred &&
|
||||
pointerDownState.hit?.element?.id !==
|
||||
this.state.selectedLinearElement.elementId
|
||||
this.state.selectedLinearElement.elementId &&
|
||||
this.state.selectedLinearElement.draggedFocusPointBinding === null
|
||||
) {
|
||||
this.actionManager.executeAction(actionFinalize);
|
||||
} else {
|
||||
@@ -9900,7 +10011,18 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
if (this.state.selectedLinearElement.draggedFocusPointBinding) {
|
||||
handleFocusPointPointerUp(
|
||||
this.state.selectedLinearElement,
|
||||
this.scene,
|
||||
);
|
||||
this.setState({
|
||||
selectedLinearElement: {
|
||||
...this.state.selectedLinearElement,
|
||||
draggedFocusPointBinding: null,
|
||||
},
|
||||
});
|
||||
} else if (
|
||||
pointerDownState.hit?.element?.id !==
|
||||
this.state.selectedLinearElement.elementId
|
||||
) {
|
||||
@@ -9910,6 +10032,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setState({ selectedLinearElement: null });
|
||||
}
|
||||
} else if (this.state.selectedLinearElement.isDragging) {
|
||||
this.setState({
|
||||
selectedLinearElement: {
|
||||
...this.state.selectedLinearElement,
|
||||
isDragging: false,
|
||||
},
|
||||
});
|
||||
this.actionManager.executeAction(actionFinalize, "ui", {
|
||||
event: childEvent,
|
||||
sceneCoords,
|
||||
|
||||
@@ -135,7 +135,8 @@ describe("binding with linear elements", () => {
|
||||
) as HTMLInputElement;
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
expect(inputX).not.toBeNull();
|
||||
UI.updateInput(inputX, String("186"));
|
||||
|
||||
UI.updateInput(inputX, String("184"));
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
});
|
||||
|
||||
|
||||
@@ -21,10 +21,13 @@ import {
|
||||
deconstructDiamondElement,
|
||||
deconstructRectanguloidElement,
|
||||
elementCenterPoint,
|
||||
FOCUS_POINT_SIZE,
|
||||
getOmitSidesForEditorInterface,
|
||||
getTransformHandles,
|
||||
getTransformHandlesFromCoords,
|
||||
hasBoundingBox,
|
||||
isArrowElement,
|
||||
isBindableElement,
|
||||
isElbowArrow,
|
||||
isFrameLikeElement,
|
||||
isImageElement,
|
||||
@@ -44,6 +47,10 @@ import {
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element";
|
||||
import {
|
||||
getGlobalFixedPointForBindableElement,
|
||||
isFocusPointVisible,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type {
|
||||
TransformHandles,
|
||||
@@ -52,6 +59,7 @@ import type {
|
||||
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
@@ -71,11 +79,6 @@ import {
|
||||
SCROLLBAR_WIDTH,
|
||||
} from "../scene/scrollbars";
|
||||
|
||||
import {
|
||||
type AppClassProperties,
|
||||
type InteractiveCanvasAppState,
|
||||
} from "../types";
|
||||
|
||||
import { getClientColor, renderRemoteCursors } from "../clients";
|
||||
|
||||
import {
|
||||
@@ -85,6 +88,8 @@ import {
|
||||
strokeRectWithRotation_simple,
|
||||
} from "./helpers";
|
||||
|
||||
import type { AppClassProperties, InteractiveCanvasAppState } from "../types";
|
||||
|
||||
import type {
|
||||
InteractiveCanvasRenderConfig,
|
||||
InteractiveSceneRenderConfig,
|
||||
@@ -123,6 +128,9 @@ const renderLinearElementPointHighlight = (
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (appState.selectedLinearElement?.isDragging) {
|
||||
return;
|
||||
}
|
||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||
|
||||
if (!element) {
|
||||
@@ -156,6 +164,19 @@ const highlightPoint = <Point extends LocalPoint | GlobalPoint>(
|
||||
);
|
||||
};
|
||||
|
||||
const renderFocusPointHighlight = (
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: InteractiveCanvasAppState,
|
||||
focusPoint: GlobalPoint,
|
||||
) => {
|
||||
context.save();
|
||||
context.translate(appState.scrollX, appState.scrollY);
|
||||
|
||||
highlightPoint(focusPoint, context, appState);
|
||||
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const renderSingleLinearPoint = <Point extends GlobalPoint | LocalPoint>(
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: InteractiveCanvasAppState,
|
||||
@@ -920,6 +941,145 @@ const renderLinearPointHandles = (
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const renderFocusPointConnectionLine = (
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: InteractiveCanvasAppState,
|
||||
fromPoint: GlobalPoint,
|
||||
toPoint: GlobalPoint,
|
||||
) => {
|
||||
context.save();
|
||||
context.translate(appState.scrollX, appState.scrollY);
|
||||
|
||||
context.strokeStyle = "rgba(134, 131, 226, 0.6)";
|
||||
context.lineWidth = 1 / appState.zoom.value;
|
||||
context.setLineDash([4 / appState.zoom.value, 4 / appState.zoom.value]);
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(fromPoint[0], fromPoint[1]);
|
||||
context.lineTo(toPoint[0], toPoint[1]);
|
||||
context.stroke();
|
||||
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const renderFocusPointCicle = (
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: InteractiveCanvasAppState,
|
||||
point: GlobalPoint,
|
||||
radius: number,
|
||||
isHovered: boolean,
|
||||
) => {
|
||||
context.save();
|
||||
context.translate(appState.scrollX, appState.scrollY);
|
||||
context.strokeStyle = "rgba(134, 131, 226, 0.6)";
|
||||
context.lineWidth = 1 / appState.zoom.value;
|
||||
context.setLineDash([]);
|
||||
context.fillStyle = isHovered
|
||||
? "rgba(134, 131, 226, 0.9)"
|
||||
: "rgba(255, 255, 255, 0.9)";
|
||||
|
||||
fillCircle(
|
||||
context,
|
||||
point[0],
|
||||
point[1],
|
||||
radius / appState.zoom.value,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
context.restore();
|
||||
};
|
||||
|
||||
const renderFocusPointIndicator = ({
|
||||
arrow,
|
||||
appState,
|
||||
type,
|
||||
context,
|
||||
elementsMap,
|
||||
}: {
|
||||
arrow: NonDeleted<ExcalidrawArrowElement>;
|
||||
appState: InteractiveCanvasAppState;
|
||||
context: CanvasRenderingContext2D;
|
||||
elementsMap: NonDeletedSceneElementsMap;
|
||||
type: "start" | "end";
|
||||
}) => {
|
||||
const binding = type === "start" ? arrow.startBinding : arrow.endBinding;
|
||||
const bindableElement =
|
||||
binding?.elementId && elementsMap.get(binding.elementId);
|
||||
|
||||
if (
|
||||
!bindableElement ||
|
||||
!isBindableElement(bindableElement) ||
|
||||
bindableElement.isDeleted
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const focusPoint = getGlobalFixedPointForBindableElement(
|
||||
binding.fixedPoint,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
// Only render if focus point is within the bindable element
|
||||
if (
|
||||
!isFocusPointVisible(
|
||||
focusPoint,
|
||||
arrow,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
appState,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const linearState = appState.selectedLinearElement;
|
||||
const isDragging = !!linearState?.isDragging;
|
||||
const pointIndex = type === "start" ? 0 : arrow.points.length - 1;
|
||||
const pointSelected =
|
||||
!!linearState?.selectedPointsIndices?.includes(pointIndex);
|
||||
|
||||
// render focus point highlight
|
||||
// ----------------------------
|
||||
|
||||
if (
|
||||
linearState?.hoveredFocusPointBinding === type &&
|
||||
!linearState.draggedFocusPointBinding
|
||||
) {
|
||||
renderFocusPointHighlight(context, appState, focusPoint);
|
||||
}
|
||||
|
||||
// render focus point
|
||||
// ----------------------------
|
||||
|
||||
if (!(pointSelected && isDragging)) {
|
||||
const focusPoint = getGlobalFixedPointForBindableElement(
|
||||
binding.fixedPoint,
|
||||
bindableElement,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
const isHovered = linearState?.hoveredFocusPointBinding === type;
|
||||
|
||||
// Render dashed line from arrow start point to focus point
|
||||
const arrowPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||
arrow,
|
||||
pointIndex,
|
||||
elementsMap,
|
||||
);
|
||||
|
||||
renderFocusPointConnectionLine(context, appState, arrowPoint, focusPoint);
|
||||
|
||||
renderFocusPointCicle(
|
||||
context,
|
||||
appState,
|
||||
focusPoint,
|
||||
FOCUS_POINT_SIZE / 1.5,
|
||||
isHovered,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderTransformHandles = (
|
||||
context: CanvasRenderingContext2D,
|
||||
renderConfig: InteractiveCanvasRenderConfig,
|
||||
@@ -1251,26 +1411,44 @@ const _renderInteractiveScene = ({
|
||||
);
|
||||
}
|
||||
|
||||
const linearState = appState.selectedLinearElement;
|
||||
const selectedLinearElement =
|
||||
linearState &&
|
||||
LinearElementEditor.getElement(linearState.elementId, allElementsMap);
|
||||
// Arrows have a different highlight behavior when
|
||||
// they are the only selected element
|
||||
if (appState.selectedLinearElement) {
|
||||
const editor = appState.selectedLinearElement;
|
||||
const firstSelectedLinear = selectedElements.find(
|
||||
(el) => el.id === editor.elementId, // Don't forget bound text elements!
|
||||
);
|
||||
|
||||
if (selectedLinearElement) {
|
||||
if (!appState.selectedLinearElement.isDragging) {
|
||||
if (editor.segmentMidPointHoveredCoords) {
|
||||
if (linearState.segmentMidPointHoveredCoords) {
|
||||
renderElbowArrowMidPointHighlight(context, appState);
|
||||
} else if (
|
||||
isElbowArrow(firstSelectedLinear)
|
||||
? editor.hoverPointIndex === 0 ||
|
||||
editor.hoverPointIndex === firstSelectedLinear.points.length - 1
|
||||
: editor.hoverPointIndex >= 0
|
||||
isElbowArrow(selectedLinearElement)
|
||||
? linearState.hoverPointIndex === 0 ||
|
||||
linearState.hoverPointIndex ===
|
||||
selectedLinearElement.points.length - 1
|
||||
: linearState.hoverPointIndex >= 0
|
||||
) {
|
||||
renderLinearElementPointHighlight(context, appState, elementsMap);
|
||||
}
|
||||
}
|
||||
|
||||
if (isArrowElement(selectedLinearElement)) {
|
||||
renderFocusPointIndicator({
|
||||
arrow: selectedLinearElement,
|
||||
elementsMap: allElementsMap,
|
||||
appState,
|
||||
context,
|
||||
type: "start",
|
||||
});
|
||||
|
||||
renderFocusPointIndicator({
|
||||
arrow: selectedLinearElement,
|
||||
elementsMap: allElementsMap,
|
||||
appState,
|
||||
context,
|
||||
type: "end",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Paint selected elements
|
||||
|
||||
@@ -2398,7 +2398,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "353.93938",
|
||||
"height": "390.11932",
|
||||
"id": "id4",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
@@ -2412,8 +2412,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
0,
|
||||
],
|
||||
[
|
||||
"478.03877",
|
||||
"-353.93938",
|
||||
"478.03878",
|
||||
"-390.11932",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -2424,7 +2424,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -2435,9 +2435,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 12,
|
||||
"width": "478.03877",
|
||||
"width": "478.03878",
|
||||
"x": 11,
|
||||
"y": "-45.14705",
|
||||
"y": "-8.96692",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2566,7 +2566,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "353.93938",
|
||||
"height": "390.11932",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
@@ -2578,8 +2578,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
0,
|
||||
],
|
||||
[
|
||||
"478.03877",
|
||||
"-353.93938",
|
||||
"478.03878",
|
||||
"-390.11932",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -2590,7 +2590,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
"fixedPoint": [
|
||||
"0.50010",
|
||||
1,
|
||||
"0.50010",
|
||||
],
|
||||
"mode": "orbit",
|
||||
@@ -2600,9 +2600,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"version": 12,
|
||||
"width": "478.03877",
|
||||
"width": "478.03878",
|
||||
"x": 11,
|
||||
"y": "-45.14705",
|
||||
"y": "-8.96692",
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
|
||||
@@ -8743,9 +8743,11 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
||||
"selectedGroupIds": {},
|
||||
"selectedLinearElement": LinearElementEditor {
|
||||
"customLineAngle": null,
|
||||
"draggedFocusPointBinding": null,
|
||||
"elbowed": false,
|
||||
"elementId": "id0",
|
||||
"hoverPointIndex": -1,
|
||||
"hoveredFocusPointBinding": null,
|
||||
"initialState": {
|
||||
"altFocusPoint": null,
|
||||
"arrowStartIsInside": false,
|
||||
@@ -8976,9 +8978,11 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
||||
"selectedGroupIds": {},
|
||||
"selectedLinearElement": LinearElementEditor {
|
||||
"customLineAngle": null,
|
||||
"draggedFocusPointBinding": null,
|
||||
"elbowed": false,
|
||||
"elementId": "id0",
|
||||
"hoverPointIndex": -1,
|
||||
"hoveredFocusPointBinding": null,
|
||||
"initialState": {
|
||||
"altFocusPoint": null,
|
||||
"arrowStartIsInside": false,
|
||||
@@ -9402,9 +9406,11 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
||||
"selectedGroupIds": {},
|
||||
"selectedLinearElement": LinearElementEditor {
|
||||
"customLineAngle": null,
|
||||
"draggedFocusPointBinding": null,
|
||||
"elbowed": false,
|
||||
"elementId": "id0",
|
||||
"hoverPointIndex": -1,
|
||||
"hoveredFocusPointBinding": null,
|
||||
"initialState": {
|
||||
"altFocusPoint": null,
|
||||
"arrowStartIsInside": false,
|
||||
@@ -9818,9 +9824,11 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
||||
"selectedGroupIds": {},
|
||||
"selectedLinearElement": LinearElementEditor {
|
||||
"customLineAngle": null,
|
||||
"draggedFocusPointBinding": null,
|
||||
"elbowed": false,
|
||||
"elementId": "id0",
|
||||
"hoverPointIndex": -1,
|
||||
"hoveredFocusPointBinding": null,
|
||||
"initialState": {
|
||||
"altFocusPoint": null,
|
||||
"arrowStartIsInside": false,
|
||||
|
||||
@@ -5051,7 +5051,7 @@ describe("history", () => {
|
||||
id: arrowId,
|
||||
startBinding: expect.objectContaining({
|
||||
elementId: rect1.id,
|
||||
fixedPoint: expect.arrayContaining([0.5001, 0.5001]),
|
||||
fixedPoint: expect.arrayContaining([1, 0.5001]),
|
||||
}),
|
||||
endBinding: expect.objectContaining({
|
||||
elementId: rect2.id,
|
||||
|
||||
@@ -63,6 +63,8 @@ import type { isOverScrollBars } from "./scene/scrollbars";
|
||||
import type React from "react";
|
||||
import type { JSX } from "react";
|
||||
|
||||
export type { App };
|
||||
|
||||
export type SocketId = string & { _brand: "SocketId" };
|
||||
|
||||
export type Collaborator = Readonly<{
|
||||
|
||||
Reference in New Issue
Block a user