feat: Precise hit testing (#9488)
This commit is contained in:
@@ -18,7 +18,6 @@ import {
|
||||
arrayToMap,
|
||||
getFontFamilyString,
|
||||
getShortcutKey,
|
||||
tupleToCoors,
|
||||
getLineHeight,
|
||||
isTransparent,
|
||||
reduceToCommonValue,
|
||||
@@ -28,9 +27,7 @@ import { canBecomePolygon, getNonDeletedElements } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
bindLinearElement,
|
||||
bindPointToSnapToElementOutline,
|
||||
calculateFixedPointForElbowArrowBinding,
|
||||
getHoveredElementForBinding,
|
||||
updateBoundElements,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
@@ -1661,63 +1658,16 @@ export const actionChangeArrowType = register({
|
||||
-1,
|
||||
elementsMap,
|
||||
);
|
||||
const startHoveredElement =
|
||||
!newElement.startBinding &&
|
||||
getHoveredElementForBinding(
|
||||
tupleToCoors(startGlobalPoint),
|
||||
elements,
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
const endHoveredElement =
|
||||
!newElement.endBinding &&
|
||||
getHoveredElementForBinding(
|
||||
tupleToCoors(endGlobalPoint),
|
||||
elements,
|
||||
elementsMap,
|
||||
appState.zoom,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
const startElement = startHoveredElement
|
||||
? startHoveredElement
|
||||
: newElement.startBinding &&
|
||||
(elementsMap.get(
|
||||
newElement.startBinding.elementId,
|
||||
) as ExcalidrawBindableElement);
|
||||
const endElement = endHoveredElement
|
||||
? endHoveredElement
|
||||
: newElement.endBinding &&
|
||||
(elementsMap.get(
|
||||
newElement.endBinding.elementId,
|
||||
) as ExcalidrawBindableElement);
|
||||
|
||||
const finalStartPoint = startHoveredElement
|
||||
? bindPointToSnapToElementOutline(
|
||||
newElement,
|
||||
startHoveredElement,
|
||||
"start",
|
||||
)
|
||||
: startGlobalPoint;
|
||||
const finalEndPoint = endHoveredElement
|
||||
? bindPointToSnapToElementOutline(
|
||||
newElement,
|
||||
endHoveredElement,
|
||||
"end",
|
||||
)
|
||||
: endGlobalPoint;
|
||||
|
||||
startHoveredElement &&
|
||||
bindLinearElement(
|
||||
newElement,
|
||||
startHoveredElement,
|
||||
"start",
|
||||
app.scene,
|
||||
);
|
||||
endHoveredElement &&
|
||||
bindLinearElement(newElement, endHoveredElement, "end", app.scene);
|
||||
const startElement =
|
||||
newElement.startBinding &&
|
||||
(elementsMap.get(
|
||||
newElement.startBinding.elementId,
|
||||
) as ExcalidrawBindableElement);
|
||||
const endElement =
|
||||
newElement.endBinding &&
|
||||
(elementsMap.get(
|
||||
newElement.endBinding.elementId,
|
||||
) as ExcalidrawBindableElement);
|
||||
|
||||
const startBinding =
|
||||
startElement && newElement.startBinding
|
||||
@@ -1728,6 +1678,7 @@ export const actionChangeArrowType = register({
|
||||
newElement,
|
||||
startElement,
|
||||
"start",
|
||||
elementsMap,
|
||||
),
|
||||
}
|
||||
: null;
|
||||
@@ -1740,6 +1691,7 @@ export const actionChangeArrowType = register({
|
||||
newElement,
|
||||
endElement,
|
||||
"end",
|
||||
elementsMap,
|
||||
),
|
||||
}
|
||||
: null;
|
||||
@@ -1749,7 +1701,7 @@ export const actionChangeArrowType = register({
|
||||
startBinding,
|
||||
endBinding,
|
||||
...updateElbowArrowPoints(newElement, elementsMap, {
|
||||
points: [finalStartPoint, finalEndPoint].map(
|
||||
points: [startGlobalPoint, endGlobalPoint].map(
|
||||
(p): LocalPoint =>
|
||||
pointFrom(p[0] - newElement.x, p[1] - newElement.y),
|
||||
),
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
STATS_PANELS,
|
||||
THEME,
|
||||
DEFAULT_GRID_STEP,
|
||||
isTestEnv,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import type { AppState, NormalizedZoomValue } from "./types";
|
||||
@@ -36,7 +37,7 @@ export const getDefaultAppState = (): Omit<
|
||||
currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness,
|
||||
currentItemStartArrowhead: null,
|
||||
currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor,
|
||||
currentItemRoundness: "round",
|
||||
currentItemRoundness: isTestEnv() ? "sharp" : "round",
|
||||
currentItemArrowType: ARROW_TYPE.round,
|
||||
currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle,
|
||||
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
|
||||
|
||||
@@ -17,8 +17,6 @@ import {
|
||||
vectorDot,
|
||||
vectorNormalize,
|
||||
} from "@excalidraw/math";
|
||||
import { isPointInShape } from "@excalidraw/utils/collision";
|
||||
import { getSelectionBoxShape } from "@excalidraw/utils/shape";
|
||||
|
||||
import {
|
||||
COLOR_PALETTE,
|
||||
@@ -104,9 +102,9 @@ import {
|
||||
Emitter,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getCommonBounds,
|
||||
getElementAbsoluteCoords,
|
||||
bindOrUnbindLinearElements,
|
||||
fixBindingsAfterDeletion,
|
||||
getHoveredElementForBinding,
|
||||
@@ -115,13 +113,8 @@ import {
|
||||
shouldEnableBindingForPointerEvent,
|
||||
updateBoundElements,
|
||||
getSuggestedBindingsForArrows,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { LinearElementEditor } from "@excalidraw/element";
|
||||
|
||||
import { newElementWith } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
LinearElementEditor,
|
||||
newElementWith,
|
||||
newFrameElement,
|
||||
newFreeDrawElement,
|
||||
newEmbeddableElement,
|
||||
@@ -133,11 +126,8 @@ import {
|
||||
newLinearElement,
|
||||
newTextElement,
|
||||
refreshTextDimensions,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { deepCopyElement, duplicateElements } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
deepCopyElement,
|
||||
duplicateElements,
|
||||
hasBoundTextElement,
|
||||
isArrowElement,
|
||||
isBindingElement,
|
||||
@@ -158,48 +148,27 @@ import {
|
||||
isFlowchartNodeElement,
|
||||
isBindableElement,
|
||||
isTextElement,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getLockedLinearCursorAlignSize,
|
||||
getNormalizedDimensions,
|
||||
isElementCompletelyInViewport,
|
||||
isElementInViewport,
|
||||
isInvisiblySmallElement,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getBoundTextShape,
|
||||
getCornerRadius,
|
||||
getElementShape,
|
||||
isPathALoop,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
createSrcDoc,
|
||||
embeddableURLValidator,
|
||||
maybeParseEmbedSrc,
|
||||
getEmbedLink,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getInitializedImageElements,
|
||||
loadHTMLImageElement,
|
||||
normalizeSVG,
|
||||
updateImageCache as _updateImageCache,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getBoundTextElement,
|
||||
getContainerCenter,
|
||||
getContainerElement,
|
||||
isValidTextContainer,
|
||||
redrawTextBoundingBox,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { shouldShowBoundingBox } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
shouldShowBoundingBox,
|
||||
getFrameChildren,
|
||||
isCursorInFrame,
|
||||
addElementsToFrame,
|
||||
@@ -214,29 +183,17 @@ import {
|
||||
getFrameLikeTitle,
|
||||
getElementsOverlappingFrame,
|
||||
filterElementsEligibleAsFrameChildren,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
hitElementBoundText,
|
||||
hitElementBoundingBoxOnly,
|
||||
hitElementItself,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { getVisibleSceneBounds } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getVisibleSceneBounds,
|
||||
FlowChartCreator,
|
||||
FlowChartNavigator,
|
||||
getLinkDirectionFromKey,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { cropElement } from "@excalidraw/element";
|
||||
|
||||
import { wrapText } from "@excalidraw/element";
|
||||
|
||||
import { isElementLink, parseElementLinkFromURL } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
cropElement,
|
||||
wrapText,
|
||||
isElementLink,
|
||||
parseElementLinkFromURL,
|
||||
isMeasureTextSupported,
|
||||
normalizeText,
|
||||
measureText,
|
||||
@@ -244,13 +201,8 @@ import {
|
||||
getApproxMinLineWidth,
|
||||
getApproxMinLineHeight,
|
||||
getMinTextElementWidth,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { ShapeCache } from "@excalidraw/element";
|
||||
|
||||
import { getRenderOpacity } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
ShapeCache,
|
||||
getRenderOpacity,
|
||||
editGroupForSelectedElement,
|
||||
getElementsInGroup,
|
||||
getSelectedGroupIdForElement,
|
||||
@@ -258,42 +210,28 @@ import {
|
||||
isElementInGroup,
|
||||
isSelectedViaGroup,
|
||||
selectGroupsForSelectedElements,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { syncInvalidIndices, syncMovedIndices } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
syncInvalidIndices,
|
||||
syncMovedIndices,
|
||||
excludeElementsInFramesFromSelection,
|
||||
getSelectionStateForElements,
|
||||
makeNextSelectedElementIds,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getResizeOffsetXY,
|
||||
getResizeArrowDirection,
|
||||
transformElements,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
getCursorForResizingElement,
|
||||
getElementWithTransformHandleType,
|
||||
getTransformHandleTypeFromCoords,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
dragNewElement,
|
||||
dragSelectedElements,
|
||||
getDragOffsetXY,
|
||||
isNonDeletedElement,
|
||||
Scene,
|
||||
Store,
|
||||
CaptureUpdateAction,
|
||||
type ElementUpdate,
|
||||
hitElementBoundingBox,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import { isNonDeletedElement } from "@excalidraw/element";
|
||||
|
||||
import { Scene } from "@excalidraw/element";
|
||||
|
||||
import { Store, CaptureUpdateAction } from "@excalidraw/element";
|
||||
|
||||
import type { ElementUpdate } from "@excalidraw/element";
|
||||
|
||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
@@ -5095,6 +5033,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return null;
|
||||
}
|
||||
|
||||
// NOTE: Hot path for hit testing, so avoid unnecessary computations
|
||||
private getElementAtPosition(
|
||||
x: number,
|
||||
y: number,
|
||||
@@ -5134,16 +5073,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
// If we're hitting element with highest z-index only on its bounding box
|
||||
// while also hitting other element figure, the latter should be considered.
|
||||
return hitElementItself({
|
||||
x,
|
||||
y,
|
||||
point: pointFrom(x, y),
|
||||
element: elementWithHighestZIndex,
|
||||
shape: getElementShape(
|
||||
elementWithHighestZIndex,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
// when overlapping, we would like to be more precise
|
||||
// this also avoids the need to update past tests
|
||||
threshold: this.getElementHitThreshold() / 2,
|
||||
threshold: this.getElementHitThreshold(elementWithHighestZIndex) / 2,
|
||||
elementsMap: this.scene.getNonDeletedElementsMap(),
|
||||
frameNameBound: isFrameLikeElement(elementWithHighestZIndex)
|
||||
? this.frameNameBoundsCache.get(elementWithHighestZIndex)
|
||||
: null,
|
||||
@@ -5158,6 +5093,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return null;
|
||||
}
|
||||
|
||||
// NOTE: Hot path for hit testing, so avoid unnecessary computations
|
||||
private getElementsAtPosition(
|
||||
x: number,
|
||||
y: number,
|
||||
@@ -5208,8 +5144,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return elements;
|
||||
}
|
||||
|
||||
getElementHitThreshold() {
|
||||
return DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value;
|
||||
getElementHitThreshold(element: ExcalidrawElement) {
|
||||
return Math.max(
|
||||
element.strokeWidth / 2 + 0.1,
|
||||
// NOTE: Here be dragons. Do not go under the 0.63 multiplier unless you're
|
||||
// willing to test extensively. The hit testing starts to become unreliable
|
||||
// due to FP imprecision under 0.63 in high zoom levels.
|
||||
0.85 * (DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value),
|
||||
);
|
||||
}
|
||||
|
||||
private hitElement(
|
||||
@@ -5224,35 +5166,35 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state.selectedElementIds[element.id] &&
|
||||
shouldShowBoundingBox([element], this.state)
|
||||
) {
|
||||
const selectionShape = getSelectionBoxShape(
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
isImageElement(element) ? 0 : this.getElementHitThreshold(),
|
||||
);
|
||||
|
||||
// if hitting the bounding box, return early
|
||||
// but if not, we should check for other cases as well (e.g. frame name)
|
||||
if (isPointInShape(pointFrom(x, y), selectionShape)) {
|
||||
if (
|
||||
hitElementBoundingBox(
|
||||
pointFrom(x, y),
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.getElementHitThreshold(element),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// take bound text element into consideration for hit collision as well
|
||||
const hitBoundTextOfElement = hitElementBoundText(
|
||||
x,
|
||||
y,
|
||||
getBoundTextShape(element, this.scene.getNonDeletedElementsMap()),
|
||||
pointFrom(x, y),
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (hitBoundTextOfElement) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return hitElementItself({
|
||||
x,
|
||||
y,
|
||||
point: pointFrom(x, y),
|
||||
element,
|
||||
shape: getElementShape(element, this.scene.getNonDeletedElementsMap()),
|
||||
threshold: this.getElementHitThreshold(),
|
||||
threshold: this.getElementHitThreshold(element),
|
||||
elementsMap: this.scene.getNonDeletedElementsMap(),
|
||||
frameNameBound: isFrameLikeElement(element)
|
||||
? this.frameNameBoundsCache.get(element)
|
||||
: null,
|
||||
@@ -5280,14 +5222,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (
|
||||
isArrowElement(elements[index]) &&
|
||||
hitElementItself({
|
||||
x,
|
||||
y,
|
||||
point: pointFrom(x, y),
|
||||
element: elements[index],
|
||||
shape: getElementShape(
|
||||
elements[index],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
threshold: this.getElementHitThreshold(),
|
||||
elementsMap: this.scene.getNonDeletedElementsMap(),
|
||||
threshold: this.getElementHitThreshold(elements[index]),
|
||||
})
|
||||
) {
|
||||
hitElement = elements[index];
|
||||
@@ -5632,14 +5570,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
hasBoundTextElement(container) ||
|
||||
!isTransparent(container.backgroundColor) ||
|
||||
hitElementItself({
|
||||
x: sceneX,
|
||||
y: sceneY,
|
||||
point: pointFrom(sceneX, sceneY),
|
||||
element: container,
|
||||
shape: getElementShape(
|
||||
container,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
threshold: this.getElementHitThreshold(),
|
||||
elementsMap: this.scene.getNonDeletedElementsMap(),
|
||||
threshold: this.getElementHitThreshold(container),
|
||||
})
|
||||
) {
|
||||
const midPoint = getContainerCenter(
|
||||
@@ -6329,13 +6263,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
let segmentMidPointHoveredCoords = null;
|
||||
if (
|
||||
hitElementItself({
|
||||
x: scenePointerX,
|
||||
y: scenePointerY,
|
||||
point: pointFrom(scenePointerX, scenePointerY),
|
||||
element,
|
||||
shape: getElementShape(
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
elementsMap,
|
||||
threshold: this.getElementHitThreshold(element),
|
||||
})
|
||||
) {
|
||||
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
|
||||
@@ -7505,7 +7436,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
// How many pixels off the shape boundary we still consider a hit
|
||||
const threshold = this.getElementHitThreshold();
|
||||
const threshold = Math.max(
|
||||
DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value,
|
||||
1,
|
||||
);
|
||||
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
||||
return (
|
||||
point.x > x1 - threshold &&
|
||||
@@ -9768,14 +9702,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||
((hitElement &&
|
||||
hitElementBoundingBoxOnly(
|
||||
{
|
||||
x: pointerDownState.origin.x,
|
||||
y: pointerDownState.origin.y,
|
||||
element: hitElement,
|
||||
shape: getElementShape(
|
||||
hitElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
point: pointFrom(
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
),
|
||||
threshold: this.getElementHitThreshold(),
|
||||
element: hitElement,
|
||||
elementsMap,
|
||||
threshold: this.getElementHitThreshold(hitElement),
|
||||
frameNameBound: isFrameLikeElement(hitElement)
|
||||
? this.frameNameBoundsCache.get(hitElement)
|
||||
: null,
|
||||
@@ -10882,6 +10815,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
croppingElement,
|
||||
cropElement(
|
||||
croppingElement,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
transformHandleType,
|
||||
image.naturalWidth,
|
||||
image.naturalHeight,
|
||||
|
||||
@@ -133,7 +133,6 @@ describe("binding with linear elements", () => {
|
||||
const inputX = UI.queryStatsProperty("X")?.querySelector(
|
||||
".drag-input",
|
||||
) as HTMLInputElement;
|
||||
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
expect(inputX).not.toBeNull();
|
||||
UI.updateInput(inputX, String("204"));
|
||||
@@ -657,6 +656,7 @@ describe("stats for multiple elements", () => {
|
||||
|
||||
mouse.reset();
|
||||
Keyboard.withModifierKeys({ shift: true }, () => {
|
||||
mouse.moveTo(10, 0);
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
|
||||
@@ -463,7 +463,7 @@ const shouldHideLinkPopup = (
|
||||
|
||||
const threshold = 15 / appState.zoom.value;
|
||||
// hitbox to prevent hiding when hovered in element bounding box
|
||||
if (hitElementBoundingBox(sceneX, sceneY, element, elementsMap)) {
|
||||
if (hitElementBoundingBox(pointFrom(sceneX, sceneY), element, elementsMap)) {
|
||||
return false;
|
||||
}
|
||||
const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
|
||||
@@ -92,7 +92,7 @@ export const isPointHittingLink = (
|
||||
if (
|
||||
!isMobile &&
|
||||
appState.viewModeEnabled &&
|
||||
hitElementBoundingBox(x, y, element, elementsMap)
|
||||
hitElementBoundingBox(pointFrom(x, y), element, elementsMap)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"startBinding": {
|
||||
"elementId": "diamond-1",
|
||||
"focus": 0,
|
||||
"gap": 4.545343408287929,
|
||||
"gap": 4.535423522449215,
|
||||
},
|
||||
"strokeColor": "#e67700",
|
||||
"strokeStyle": "solid",
|
||||
@@ -335,7 +335,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
"endBinding": {
|
||||
"elementId": "text-2",
|
||||
"focus": 0,
|
||||
"gap": 14,
|
||||
"gap": 16,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
@@ -1540,7 +1540,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
||||
"endBinding": {
|
||||
"elementId": "B",
|
||||
"focus": 0,
|
||||
"gap": 14,
|
||||
"gap": 32,
|
||||
},
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
|
||||
@@ -781,7 +781,7 @@ describe("Test Transform", () => {
|
||||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||
elementId: "rect-1",
|
||||
focus: -0,
|
||||
gap: 14,
|
||||
gap: 25,
|
||||
});
|
||||
expect(rect.boundElements).toStrictEqual([
|
||||
{
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
import { arrayToMap, easeOut, THEME } from "@excalidraw/common";
|
||||
import { getElementLineSegments } from "@excalidraw/element";
|
||||
import {
|
||||
lineSegment,
|
||||
lineSegmentIntersectionPoints,
|
||||
pointFrom,
|
||||
} from "@excalidraw/math";
|
||||
computeBoundTextPosition,
|
||||
getBoundTextElement,
|
||||
intersectElementWithLineSegment,
|
||||
isPointInElement,
|
||||
} from "@excalidraw/element";
|
||||
import { lineSegment, pointFrom } from "@excalidraw/math";
|
||||
|
||||
import { getElementsInGroup } from "@excalidraw/element";
|
||||
|
||||
import { getElementShape } from "@excalidraw/element";
|
||||
import { shouldTestInside } from "@excalidraw/element";
|
||||
import { isPointInShape } from "@excalidraw/utils/collision";
|
||||
import { hasBoundTextElement, isBoundToContainer } from "@excalidraw/element";
|
||||
import { getBoundTextElementId } from "@excalidraw/element";
|
||||
|
||||
import type { GeometricShape } from "@excalidraw/utils/shape";
|
||||
import type {
|
||||
ElementsSegmentsMap,
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
} from "@excalidraw/math/types";
|
||||
import type { GlobalPoint, LineSegment } from "@excalidraw/math/types";
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { AnimatedTrail } from "../animated-trail";
|
||||
@@ -28,15 +22,9 @@ import type { AnimationFrameHandler } from "../animation-frame-handler";
|
||||
|
||||
import type App from "../components/App";
|
||||
|
||||
// just enough to form a segment; this is sufficient for eraser
|
||||
const POINTS_ON_TRAIL = 2;
|
||||
|
||||
export class EraserTrail extends AnimatedTrail {
|
||||
private elementsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||
private groupsToErase: Set<ExcalidrawElement["id"]> = new Set();
|
||||
private segmentsCache: Map<string, LineSegment<GlobalPoint>[]> = new Map();
|
||||
private geometricShapesCache: Map<string, GeometricShape<GlobalPoint>> =
|
||||
new Map();
|
||||
|
||||
constructor(animationFrameHandler: AnimationFrameHandler, app: App) {
|
||||
super(animationFrameHandler, app, {
|
||||
@@ -79,14 +67,21 @@ export class EraserTrail extends AnimatedTrail {
|
||||
}
|
||||
|
||||
private updateElementsToBeErased(restoreToErase?: boolean) {
|
||||
let eraserPath: GlobalPoint[] =
|
||||
const eraserPath: GlobalPoint[] =
|
||||
super
|
||||
.getCurrentTrail()
|
||||
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1])) || [];
|
||||
|
||||
if (eraserPath.length < 2) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// for efficiency and avoid unnecessary calculations,
|
||||
// take only POINTS_ON_TRAIL points to form some number of segments
|
||||
eraserPath = eraserPath?.slice(eraserPath.length - POINTS_ON_TRAIL);
|
||||
const pathSegment = lineSegment<GlobalPoint>(
|
||||
eraserPath[eraserPath.length - 1],
|
||||
eraserPath[eraserPath.length - 2],
|
||||
);
|
||||
|
||||
const candidateElements = this.app.visibleElements.filter(
|
||||
(el) => !el.locked,
|
||||
@@ -94,28 +89,13 @@ export class EraserTrail extends AnimatedTrail {
|
||||
|
||||
const candidateElementsMap = arrayToMap(candidateElements);
|
||||
|
||||
const pathSegments = eraserPath.reduce((acc, point, index) => {
|
||||
if (index === 0) {
|
||||
return acc;
|
||||
}
|
||||
acc.push(lineSegment(eraserPath[index - 1], point));
|
||||
return acc;
|
||||
}, [] as LineSegment<GlobalPoint>[]);
|
||||
|
||||
if (pathSegments.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const element of candidateElements) {
|
||||
// restore only if already added to the to-be-erased set
|
||||
if (restoreToErase && this.elementsToErase.has(element.id)) {
|
||||
const intersects = eraserTest(
|
||||
pathSegments,
|
||||
pathSegment,
|
||||
element,
|
||||
this.segmentsCache,
|
||||
this.geometricShapesCache,
|
||||
candidateElementsMap,
|
||||
this.app,
|
||||
);
|
||||
|
||||
if (intersects) {
|
||||
@@ -148,12 +128,9 @@ export class EraserTrail extends AnimatedTrail {
|
||||
}
|
||||
} else if (!restoreToErase && !this.elementsToErase.has(element.id)) {
|
||||
const intersects = eraserTest(
|
||||
pathSegments,
|
||||
pathSegment,
|
||||
element,
|
||||
this.segmentsCache,
|
||||
this.geometricShapesCache,
|
||||
candidateElementsMap,
|
||||
this.app,
|
||||
);
|
||||
|
||||
if (intersects) {
|
||||
@@ -196,45 +173,37 @@ export class EraserTrail extends AnimatedTrail {
|
||||
super.clearTrails();
|
||||
this.elementsToErase.clear();
|
||||
this.groupsToErase.clear();
|
||||
this.segmentsCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const eraserTest = (
|
||||
pathSegments: LineSegment<GlobalPoint>[],
|
||||
pathSegment: LineSegment<GlobalPoint>,
|
||||
element: ExcalidrawElement,
|
||||
elementsSegments: ElementsSegmentsMap,
|
||||
shapesCache: Map<string, GeometricShape<GlobalPoint>>,
|
||||
elementsMap: ElementsMap,
|
||||
app: App,
|
||||
): boolean => {
|
||||
let shape = shapesCache.get(element.id);
|
||||
|
||||
if (!shape) {
|
||||
shape = getElementShape<GlobalPoint>(element, elementsMap);
|
||||
shapesCache.set(element.id, shape);
|
||||
}
|
||||
|
||||
const lastPoint = pathSegments[pathSegments.length - 1][1];
|
||||
if (shouldTestInside(element) && isPointInShape(lastPoint, shape)) {
|
||||
const lastPoint = pathSegment[1];
|
||||
if (
|
||||
shouldTestInside(element) &&
|
||||
isPointInElement(lastPoint, element, elementsMap)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let elementSegments = elementsSegments.get(element.id);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (!elementSegments) {
|
||||
elementSegments = getElementLineSegments(element, elementsMap);
|
||||
elementsSegments.set(element.id, elementSegments);
|
||||
}
|
||||
|
||||
return pathSegments.some((pathSegment) =>
|
||||
elementSegments?.some(
|
||||
(elementSegment) =>
|
||||
lineSegmentIntersectionPoints(
|
||||
pathSegment,
|
||||
elementSegment,
|
||||
app.getElementHitThreshold(),
|
||||
) !== null,
|
||||
),
|
||||
return (
|
||||
intersectElementWithLineSegment(element, elementsMap, pathSegment, 0, true)
|
||||
.length > 0 ||
|
||||
(!!boundTextElement &&
|
||||
intersectElementWithLineSegment(
|
||||
{
|
||||
...boundTextElement,
|
||||
...computeBoundTextPosition(element, boundTextElement, elementsMap),
|
||||
},
|
||||
elementsMap,
|
||||
pathSegment,
|
||||
0,
|
||||
true,
|
||||
).length > 0)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -199,6 +199,7 @@ export class LassoTrail extends AnimatedTrail {
|
||||
const { selectedElementIds } = getLassoSelectedElementIds({
|
||||
lassoPath,
|
||||
elements: this.app.visibleElements,
|
||||
elementsMap: this.app.scene.getNonDeletedElementsMap(),
|
||||
elementsSegments: this.elementsSegments,
|
||||
intersectedElements: this.intersectedElements,
|
||||
enclosedElements: this.enclosedElements,
|
||||
|
||||
@@ -3,20 +3,25 @@ import { simplify } from "points-on-curve";
|
||||
import {
|
||||
polygonFromPoints,
|
||||
lineSegment,
|
||||
lineSegmentIntersectionPoints,
|
||||
polygonIncludesPointNonZero,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
ElementsSegmentsMap,
|
||||
GlobalPoint,
|
||||
LineSegment,
|
||||
} from "@excalidraw/math/types";
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
import {
|
||||
type Bounds,
|
||||
computeBoundTextPosition,
|
||||
doBoundsIntersect,
|
||||
getBoundTextElement,
|
||||
getElementBounds,
|
||||
intersectElementWithLineSegment,
|
||||
} from "@excalidraw/element";
|
||||
|
||||
import type { ElementsSegmentsMap, GlobalPoint } from "@excalidraw/math/types";
|
||||
import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
export const getLassoSelectedElementIds = (input: {
|
||||
lassoPath: GlobalPoint[];
|
||||
elements: readonly ExcalidrawElement[];
|
||||
elementsMap: ElementsMap;
|
||||
elementsSegments: ElementsSegmentsMap;
|
||||
intersectedElements: Set<ExcalidrawElement["id"]>;
|
||||
enclosedElements: Set<ExcalidrawElement["id"]>;
|
||||
@@ -27,6 +32,7 @@ export const getLassoSelectedElementIds = (input: {
|
||||
const {
|
||||
lassoPath,
|
||||
elements,
|
||||
elementsMap,
|
||||
elementsSegments,
|
||||
intersectedElements,
|
||||
enclosedElements,
|
||||
@@ -40,8 +46,26 @@ export const getLassoSelectedElementIds = (input: {
|
||||
const unlockedElements = elements.filter((el) => !el.locked);
|
||||
// as the path might not enclose a shape anymore, clear before checking
|
||||
enclosedElements.clear();
|
||||
intersectedElements.clear();
|
||||
const lassoBounds = lassoPath.reduce(
|
||||
(acc, item) => {
|
||||
return [
|
||||
Math.min(acc[0], item[0]),
|
||||
Math.min(acc[1], item[1]),
|
||||
Math.max(acc[2], item[0]),
|
||||
Math.max(acc[3], item[1]),
|
||||
];
|
||||
},
|
||||
[Infinity, Infinity, -Infinity, -Infinity],
|
||||
) as Bounds;
|
||||
for (const element of unlockedElements) {
|
||||
// First check if the lasso segment intersects the element's axis-aligned
|
||||
// bounding box as it is much faster than checking intersection against
|
||||
// the element's shape
|
||||
const elementBounds = getElementBounds(element, elementsMap);
|
||||
|
||||
if (
|
||||
doBoundsIntersect(lassoBounds, elementBounds) &&
|
||||
!intersectedElements.has(element.id) &&
|
||||
!enclosedElements.has(element.id)
|
||||
) {
|
||||
@@ -49,7 +73,7 @@ export const getLassoSelectedElementIds = (input: {
|
||||
if (enclosed) {
|
||||
enclosedElements.add(element.id);
|
||||
} else {
|
||||
const intersects = intersectionTest(path, element, elementsSegments);
|
||||
const intersects = intersectionTest(path, element, elementsMap);
|
||||
if (intersects) {
|
||||
intersectedElements.add(element.id);
|
||||
}
|
||||
@@ -85,26 +109,34 @@ const enclosureTest = (
|
||||
const intersectionTest = (
|
||||
lassoPath: GlobalPoint[],
|
||||
element: ExcalidrawElement,
|
||||
elementsSegments: ElementsSegmentsMap,
|
||||
elementsMap: ElementsMap,
|
||||
): boolean => {
|
||||
const elementSegments = elementsSegments.get(element.id);
|
||||
if (!elementSegments) {
|
||||
return false;
|
||||
}
|
||||
const lassoSegments = lassoPath
|
||||
.slice(1)
|
||||
.map((point: GlobalPoint, index) => lineSegment(lassoPath[index], point))
|
||||
.concat([lineSegment(lassoPath[lassoPath.length - 1], lassoPath[0])]);
|
||||
|
||||
const lassoSegments = lassoPath.reduce((acc, point, index) => {
|
||||
if (index === 0) {
|
||||
return acc;
|
||||
}
|
||||
acc.push(lineSegment(lassoPath[index - 1], point));
|
||||
return acc;
|
||||
}, [] as LineSegment<GlobalPoint>[]);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
return lassoSegments.some((lassoSegment) =>
|
||||
elementSegments.some(
|
||||
(elementSegment) =>
|
||||
// introduce a bit of tolerance to account for roughness and simplification of paths
|
||||
lineSegmentIntersectionPoints(lassoSegment, elementSegment, 1) !== null,
|
||||
),
|
||||
return lassoSegments.some(
|
||||
(lassoSegment) =>
|
||||
intersectElementWithLineSegment(
|
||||
element,
|
||||
elementsMap,
|
||||
lassoSegment,
|
||||
0,
|
||||
true,
|
||||
).length > 0 ||
|
||||
(!!boundTextElement &&
|
||||
intersectElementWithLineSegment(
|
||||
{
|
||||
...boundTextElement,
|
||||
...computeBoundTextPosition(element, boundTextElement, elementsMap),
|
||||
},
|
||||
elementsMap,
|
||||
lassoSegment,
|
||||
0,
|
||||
true,
|
||||
).length > 0),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,17 +5,14 @@ import { getDiamondPoints } from "@excalidraw/element";
|
||||
import { getCornerRadius } from "@excalidraw/element";
|
||||
|
||||
import {
|
||||
bezierEquation,
|
||||
curve,
|
||||
curveTangent,
|
||||
curveCatmullRomCubicApproxPoints,
|
||||
curveCatmullRomQuadraticApproxPoints,
|
||||
curveOffsetPoints,
|
||||
type GlobalPoint,
|
||||
offsetPointsForQuadraticBezier,
|
||||
pointFrom,
|
||||
pointFromVector,
|
||||
pointRotateRads,
|
||||
vector,
|
||||
vectorNormal,
|
||||
vectorNormalize,
|
||||
vectorScale,
|
||||
} from "@excalidraw/math";
|
||||
|
||||
import type {
|
||||
@@ -102,25 +99,14 @@ export const bootstrapCanvas = ({
|
||||
function drawCatmullRomQuadraticApprox(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
points: GlobalPoint[],
|
||||
segments = 20,
|
||||
tension = 0.5,
|
||||
) {
|
||||
ctx.lineTo(points[0][0], points[0][1]);
|
||||
const pointSets = curveCatmullRomQuadraticApproxPoints(points, tension);
|
||||
if (pointSets) {
|
||||
for (let i = 0; i < pointSets.length - 1; i++) {
|
||||
const [[cpX, cpY], [p2X, p2Y]] = pointSets[i];
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i - 1 < 0 ? 0 : i - 1];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1];
|
||||
|
||||
for (let t = 0; t <= 1; t += 1 / segments) {
|
||||
const t2 = t * t;
|
||||
|
||||
const x =
|
||||
(1 - t) * (1 - t) * p0[0] + 2 * (1 - t) * t * p1[0] + t2 * p2[0];
|
||||
|
||||
const y =
|
||||
(1 - t) * (1 - t) * p0[1] + 2 * (1 - t) * t * p1[1] + t2 * p2[1];
|
||||
|
||||
ctx.lineTo(x, y);
|
||||
ctx.quadraticCurveTo(cpX, cpY, p2X, p2Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,35 +114,13 @@ function drawCatmullRomQuadraticApprox(
|
||||
function drawCatmullRomCubicApprox(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
points: GlobalPoint[],
|
||||
segments = 20,
|
||||
tension = 0.5,
|
||||
) {
|
||||
ctx.lineTo(points[0][0], points[0][1]);
|
||||
|
||||
for (let i = 0; i < points.length - 1; i++) {
|
||||
const p0 = points[i - 1 < 0 ? 0 : i - 1];
|
||||
const p1 = points[i];
|
||||
const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1];
|
||||
const p3 = points[i + 2 >= points.length ? points.length - 1 : i + 2];
|
||||
|
||||
for (let t = 0; t <= 1; t += 1 / segments) {
|
||||
const t2 = t * t;
|
||||
const t3 = t2 * t;
|
||||
|
||||
const x =
|
||||
0.5 *
|
||||
(2 * p1[0] +
|
||||
(-p0[0] + p2[0]) * t +
|
||||
(2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 +
|
||||
(-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3);
|
||||
|
||||
const y =
|
||||
0.5 *
|
||||
(2 * p1[1] +
|
||||
(-p0[1] + p2[1]) * t +
|
||||
(2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 +
|
||||
(-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3);
|
||||
|
||||
ctx.lineTo(x, y);
|
||||
const pointSets = curveCatmullRomCubicApproxPoints(points, tension);
|
||||
if (pointSets) {
|
||||
for (let i = 0; i < pointSets.length; i++) {
|
||||
const [[cp1x, cp1y], [cp2x, cp2y], [x, y]] = pointSets[i];
|
||||
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +132,10 @@ export const drawHighlightForRectWithRotation = (
|
||||
) => {
|
||||
const [x, y] = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x, element.y),
|
||||
elementCenterPoint(element),
|
||||
elementCenterPoint(
|
||||
element,
|
||||
window.h.app.scene.getElementsMapIncludingDeleted(),
|
||||
),
|
||||
element.angle,
|
||||
);
|
||||
|
||||
@@ -187,25 +154,25 @@ export const drawHighlightForRectWithRotation = (
|
||||
context.beginPath();
|
||||
|
||||
{
|
||||
const topLeftApprox = offsetQuadraticBezier(
|
||||
const topLeftApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(0, 0 + radius),
|
||||
pointFrom(0, 0),
|
||||
pointFrom(0 + radius, 0),
|
||||
padding,
|
||||
);
|
||||
const topRightApprox = offsetQuadraticBezier(
|
||||
const topRightApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(element.width - radius, 0),
|
||||
pointFrom(element.width, 0),
|
||||
pointFrom(element.width, radius),
|
||||
padding,
|
||||
);
|
||||
const bottomRightApprox = offsetQuadraticBezier(
|
||||
const bottomRightApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(element.width, element.height - radius),
|
||||
pointFrom(element.width, element.height),
|
||||
pointFrom(element.width - radius, element.height),
|
||||
padding,
|
||||
);
|
||||
const bottomLeftApprox = offsetQuadraticBezier(
|
||||
const bottomLeftApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(radius, element.height),
|
||||
pointFrom(0, element.height),
|
||||
pointFrom(0, element.height - radius),
|
||||
@@ -230,25 +197,25 @@ export const drawHighlightForRectWithRotation = (
|
||||
// mask" on a filled shape for the diamond highlight, because stroking creates
|
||||
// sharp inset edges on line joins < 90 degrees.
|
||||
{
|
||||
const topLeftApprox = offsetQuadraticBezier(
|
||||
const topLeftApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(0 + radius, 0),
|
||||
pointFrom(0, 0),
|
||||
pointFrom(0, 0 + radius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const topRightApprox = offsetQuadraticBezier(
|
||||
const topRightApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(element.width, radius),
|
||||
pointFrom(element.width, 0),
|
||||
pointFrom(element.width - radius, 0),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomRightApprox = offsetQuadraticBezier(
|
||||
const bottomRightApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(element.width - radius, element.height),
|
||||
pointFrom(element.width, element.height),
|
||||
pointFrom(element.width, element.height - radius),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomLeftApprox = offsetQuadraticBezier(
|
||||
const bottomLeftApprox = offsetPointsForQuadraticBezier(
|
||||
pointFrom(0, element.height - radius),
|
||||
pointFrom(0, element.height),
|
||||
pointFrom(radius, element.height),
|
||||
@@ -325,7 +292,10 @@ export const drawHighlightForDiamondWithRotation = (
|
||||
) => {
|
||||
const [x, y] = pointRotateRads(
|
||||
pointFrom<GlobalPoint>(element.x, element.y),
|
||||
elementCenterPoint(element),
|
||||
elementCenterPoint(
|
||||
element,
|
||||
window.h.app.scene.getElementsMapIncludingDeleted(),
|
||||
),
|
||||
element.angle,
|
||||
);
|
||||
context.save();
|
||||
@@ -343,32 +313,40 @@ export const drawHighlightForDiamondWithRotation = (
|
||||
const horizontalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||
: (rightY - topY) * 0.01;
|
||||
const topApprox = offsetCubicBezier(
|
||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||
const topApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||
),
|
||||
padding,
|
||||
);
|
||||
const rightApprox = offsetCubicBezier(
|
||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||
const rightApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||
),
|
||||
padding,
|
||||
);
|
||||
const bottomApprox = offsetCubicBezier(
|
||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||
const bottomApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||
),
|
||||
padding,
|
||||
);
|
||||
const leftApprox = offsetCubicBezier(
|
||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||
const leftApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||
),
|
||||
padding,
|
||||
);
|
||||
|
||||
@@ -376,13 +354,13 @@ export const drawHighlightForDiamondWithRotation = (
|
||||
topApprox[topApprox.length - 1][0],
|
||||
topApprox[topApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(rightApprox[0][0], rightApprox[0][1]);
|
||||
context.lineTo(rightApprox[1][0], rightApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, rightApprox);
|
||||
context.lineTo(bottomApprox[0][0], bottomApprox[0][1]);
|
||||
context.lineTo(bottomApprox[1][0], bottomApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, bottomApprox);
|
||||
context.lineTo(leftApprox[0][0], leftApprox[0][1]);
|
||||
context.lineTo(leftApprox[1][0], leftApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, leftApprox);
|
||||
context.lineTo(topApprox[0][0], topApprox[0][1]);
|
||||
context.lineTo(topApprox[1][0], topApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, topApprox);
|
||||
}
|
||||
|
||||
@@ -398,32 +376,40 @@ export const drawHighlightForDiamondWithRotation = (
|
||||
const horizontalRadius = element.roundness
|
||||
? getCornerRadius(Math.abs(rightY - topY), element)
|
||||
: (rightY - topY) * 0.01;
|
||||
const topApprox = offsetCubicBezier(
|
||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||
const topApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(topX + verticalRadius, topY + horizontalRadius),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX, topY),
|
||||
pointFrom(topX - verticalRadius, topY + horizontalRadius),
|
||||
),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const rightApprox = offsetCubicBezier(
|
||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||
const rightApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(rightX - verticalRadius, rightY + horizontalRadius),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX, rightY),
|
||||
pointFrom(rightX - verticalRadius, rightY - horizontalRadius),
|
||||
),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const bottomApprox = offsetCubicBezier(
|
||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||
const bottomApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX, bottomY),
|
||||
pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius),
|
||||
),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
const leftApprox = offsetCubicBezier(
|
||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||
const leftApprox = curveOffsetPoints(
|
||||
curve(
|
||||
pointFrom(leftX + verticalRadius, leftY - horizontalRadius),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX, leftY),
|
||||
pointFrom(leftX + verticalRadius, leftY + horizontalRadius),
|
||||
),
|
||||
-FIXED_BINDING_DISTANCE,
|
||||
);
|
||||
|
||||
@@ -431,66 +417,16 @@ export const drawHighlightForDiamondWithRotation = (
|
||||
topApprox[topApprox.length - 1][0],
|
||||
topApprox[topApprox.length - 1][1],
|
||||
);
|
||||
context.lineTo(leftApprox[0][0], leftApprox[0][1]);
|
||||
context.lineTo(leftApprox[1][0], leftApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, leftApprox);
|
||||
context.lineTo(bottomApprox[0][0], bottomApprox[0][1]);
|
||||
context.lineTo(bottomApprox[1][0], bottomApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, bottomApprox);
|
||||
context.lineTo(rightApprox[0][0], rightApprox[0][1]);
|
||||
context.lineTo(rightApprox[1][0], rightApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, rightApprox);
|
||||
context.lineTo(topApprox[0][0], topApprox[0][1]);
|
||||
context.lineTo(topApprox[1][0], topApprox[1][1]);
|
||||
drawCatmullRomCubicApprox(context, topApprox);
|
||||
}
|
||||
context.closePath();
|
||||
context.fill();
|
||||
context.restore();
|
||||
};
|
||||
|
||||
function offsetCubicBezier(
|
||||
p0: GlobalPoint,
|
||||
p1: GlobalPoint,
|
||||
p2: GlobalPoint,
|
||||
p3: GlobalPoint,
|
||||
offsetDist: number,
|
||||
steps = 20,
|
||||
) {
|
||||
const offsetPoints = [];
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const c = curve(p0, p1, p2, p3);
|
||||
const point = bezierEquation(c, t);
|
||||
const tangent = vectorNormalize(curveTangent(c, t));
|
||||
const normal = vectorNormal(tangent);
|
||||
|
||||
offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point));
|
||||
}
|
||||
|
||||
return offsetPoints;
|
||||
}
|
||||
|
||||
function offsetQuadraticBezier(
|
||||
p0: GlobalPoint,
|
||||
p1: GlobalPoint,
|
||||
p2: GlobalPoint,
|
||||
offsetDist: number,
|
||||
steps = 20,
|
||||
) {
|
||||
const offsetPoints = [];
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const t1 = 1 - t;
|
||||
const point = pointFrom<GlobalPoint>(
|
||||
t1 * t1 * p0[0] + 2 * t1 * t * p1[0] + t * t * p2[0],
|
||||
t1 * t1 * p0[1] + 2 * t1 * t * p1[1] + t * t * p2[1],
|
||||
);
|
||||
const tangentX = 2 * (1 - t) * (p1[0] - p0[0]) + 2 * t * (p2[0] - p1[0]);
|
||||
const tangentY = 2 * (1 - t) * (p1[1] - p0[1]) + 2 * t * (p2[1] - p1[1]);
|
||||
const tangent = vectorNormalize(vector(tangentX, tangentY));
|
||||
const normal = vectorNormal(tangent);
|
||||
|
||||
offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point));
|
||||
}
|
||||
|
||||
return offsetPoints;
|
||||
}
|
||||
|
||||
@@ -193,16 +193,10 @@ const renderBindingHighlightForBindableElement = (
|
||||
elementsMap: ElementsMap,
|
||||
zoom: InteractiveCanvasAppState["zoom"],
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const width = x2 - x1;
|
||||
const height = y2 - y1;
|
||||
|
||||
context.strokeStyle = "rgba(0,0,0,.05)";
|
||||
context.fillStyle = "rgba(0,0,0,.05)";
|
||||
|
||||
// To ensure the binding highlight doesn't overlap the element itself
|
||||
const padding = maxBindingGap(element, element.width, element.height, zoom);
|
||||
|
||||
context.fillStyle = "rgba(0,0,0,.05)";
|
||||
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "text":
|
||||
@@ -216,10 +210,13 @@ const renderBindingHighlightForBindableElement = (
|
||||
case "diamond":
|
||||
drawHighlightForDiamondWithRotation(context, padding, element);
|
||||
break;
|
||||
case "ellipse":
|
||||
context.lineWidth =
|
||||
maxBindingGap(element, element.width, element.height, zoom) -
|
||||
FIXED_BINDING_DISTANCE;
|
||||
case "ellipse": {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
const width = x2 - x1;
|
||||
const height = y2 - y1;
|
||||
|
||||
context.strokeStyle = "rgba(0,0,0,.05)";
|
||||
context.lineWidth = padding - FIXED_BINDING_DISTANCE;
|
||||
|
||||
strokeEllipseWithRotation(
|
||||
context,
|
||||
@@ -230,6 +227,7 @@ const renderBindingHighlightForBindableElement = (
|
||||
element.angle,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -898,7 +898,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -1016,9 +1016,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -1052,9 +1050,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -1101,7 +1097,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -1162,7 +1158,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||
"resizingElement": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
"scrolledOutside": true,
|
||||
"searchMatches": null,
|
||||
"selectedElementIds": {
|
||||
"id0": true,
|
||||
@@ -1205,7 +1201,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
@@ -1213,20 +1209,18 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1150084233,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1263,23 +1257,21 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"height": 10,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
@@ -1317,7 +1309,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -1427,9 +1419,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1014066025,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -1461,9 +1451,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -1518,9 +1506,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -1572,9 +1558,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -1650,7 +1634,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -1760,9 +1744,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1014066025,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -1794,9 +1776,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -1851,9 +1831,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -1905,9 +1883,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -1983,7 +1959,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -2044,7 +2020,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||
"resizingElement": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
"scrolledOutside": true,
|
||||
"searchMatches": null,
|
||||
"selectedElementIds": {
|
||||
"id0": true,
|
||||
@@ -2087,7 +2063,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
@@ -2095,20 +2071,18 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1150084233,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2145,23 +2119,21 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"height": 10,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
@@ -2199,7 +2171,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -2299,7 +2271,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"index": "a0",
|
||||
"isDeleted": true,
|
||||
@@ -2307,20 +2279,18 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 1014066025,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
"versionNonce": 1116226695,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2357,23 +2327,21 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"height": 10,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
@@ -2440,7 +2408,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -2542,7 +2510,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
@@ -2550,20 +2518,18 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1150084233,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2576,7 +2542,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"height": 10,
|
||||
"id": "id3",
|
||||
"index": "a1",
|
||||
"isDeleted": false,
|
||||
@@ -2584,9 +2550,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1014066025,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -2595,9 +2559,9 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"updated": 1,
|
||||
"version": 5,
|
||||
"versionNonce": 400692809,
|
||||
"width": 20,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2634,23 +2598,21 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"height": 10,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
@@ -2688,23 +2650,21 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"height": 10,
|
||||
"index": "a1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 20,
|
||||
"x": 0,
|
||||
"y": 10,
|
||||
"width": 10,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
@@ -2742,7 +2702,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -2859,9 +2819,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -2895,9 +2853,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1014066025,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -2952,9 +2908,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -3006,9 +2960,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -3114,7 +3066,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 60,
|
||||
"currentItemRoughness": 2,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#e03131",
|
||||
"currentItemStrokeStyle": "dotted",
|
||||
@@ -3226,9 +3178,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||
"locked": false,
|
||||
"opacity": 60,
|
||||
"roughness": 2,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "dotted",
|
||||
@@ -3236,7 +3186,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 941653321,
|
||||
"versionNonce": 908564423,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@@ -3260,17 +3210,15 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||
"locked": false,
|
||||
"opacity": 60,
|
||||
"roughness": 2,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 289600103,
|
||||
"roundness": null,
|
||||
"seed": 1315507081,
|
||||
"strokeColor": "#e03131",
|
||||
"strokeStyle": "dotted",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 9,
|
||||
"versionNonce": 640725609,
|
||||
"versionNonce": 406373543,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
@@ -3317,9 +3265,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -3371,9 +3317,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -3597,7 +3541,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -3707,17 +3651,15 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 1014066025,
|
||||
"roundness": null,
|
||||
"seed": 1150084233,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 23633383,
|
||||
"versionNonce": 1604849351,
|
||||
"width": 20,
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
@@ -3741,17 +3683,15 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1150084233,
|
||||
"versionNonce": 401146281,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
@@ -3798,9 +3738,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -3852,9 +3790,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -3922,7 +3858,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -4032,9 +3968,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1014066025,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -4066,9 +4000,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -4123,9 +4055,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -4177,9 +4107,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -4247,7 +4175,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -4360,9 +4288,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -4394,9 +4320,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 238820263,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -4451,9 +4375,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -4505,9 +4427,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -5528,7 +5448,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -5641,9 +5561,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 453191,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -5675,17 +5593,15 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 400692809,
|
||||
"roundness": null,
|
||||
"seed": 1604849351,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 23633383,
|
||||
"versionNonce": 493213705,
|
||||
"width": 10,
|
||||
"x": 12,
|
||||
"y": 0,
|
||||
@@ -5732,9 +5648,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -5786,9 +5700,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -6749,7 +6661,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -6866,9 +6778,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 449462985,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -6902,9 +6812,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 238820263,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -6959,9 +6867,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -7013,9 +6919,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
@@ -7684,7 +7588,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -8684,7 +8588,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -8745,7 +8649,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||
"resizingElement": null,
|
||||
"scrollX": 0,
|
||||
"scrollY": 0,
|
||||
"scrolledOutside": false,
|
||||
"scrolledOutside": true,
|
||||
"searchMatches": null,
|
||||
"selectedElementIds": {
|
||||
"id0": true,
|
||||
@@ -9675,7 +9579,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
||||
"currentItemFontSize": 20,
|
||||
"currentItemOpacity": 100,
|
||||
"currentItemRoughness": 1,
|
||||
"currentItemRoundness": "round",
|
||||
"currentItemRoundness": "sharp",
|
||||
"currentItemStartArrowhead": null,
|
||||
"currentItemStrokeColor": "#1e1e1e",
|
||||
"currentItemStrokeStyle": "solid",
|
||||
@@ -9780,7 +9684,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
@@ -9788,20 +9692,18 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1150084233,
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
"versionNonce": 2019559783,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -9822,9 +9724,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -9856,9 +9756,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -9912,23 +9810,21 @@ exports[`contextMenu element > shows context menu for element > [end of test] un
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 20,
|
||||
"height": 10,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"width": 20,
|
||||
"x": -10,
|
||||
"y": 0,
|
||||
"width": 10,
|
||||
"x": -20,
|
||||
"y": -10,
|
||||
},
|
||||
"inserted": {
|
||||
"isDeleted": true,
|
||||
|
||||
@@ -71,9 +71,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -107,9 +105,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -155,9 +151,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
||||
],
|
||||
"polygon": false,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"startArrowhead": null,
|
||||
"startBinding": null,
|
||||
@@ -193,9 +187,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -17,9 +17,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -51,9 +49,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1604849351,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -85,9 +81,7 @@ exports[`move element > rectangle 5`] = `
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -124,9 +118,7 @@ exports[`move element > rectangles with binding arrow 5`] = `
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -163,9 +155,7 @@ exports[`move element > rectangles with binding arrow 6`] = `
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1150084233,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -196,7 +186,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": "87.29887",
|
||||
"height": "81.40630",
|
||||
"id": "id6",
|
||||
"index": "a2",
|
||||
"isDeleted": false,
|
||||
@@ -210,8 +200,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
||||
0,
|
||||
],
|
||||
[
|
||||
"86.85786",
|
||||
"87.29887",
|
||||
"81.00000",
|
||||
"81.40630",
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
@@ -232,8 +222,8 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"versionNonce": 1996028265,
|
||||
"width": "86.85786",
|
||||
"x": "107.07107",
|
||||
"y": "47.07107",
|
||||
"width": "81.00000",
|
||||
"x": "110.00000",
|
||||
"y": 50,
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -95,9 +95,7 @@ exports[`multi point mode in linear elements > line 3`] = `
|
||||
],
|
||||
"polygon": false,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"startArrowhead": null,
|
||||
"startBinding": null,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -81,9 +81,7 @@ exports[`select single element on the scene > arrow escape 1`] = `
|
||||
],
|
||||
"polygon": false,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"startArrowhead": null,
|
||||
"startBinding": null,
|
||||
@@ -117,9 +115,7 @@ exports[`select single element on the scene > diamond 1`] = `
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -151,9 +147,7 @@ exports[`select single element on the scene > ellipse 1`] = `
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -185,9 +179,7 @@ exports[`select single element on the scene > rectangle 1`] = `
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
||||
@@ -110,8 +110,8 @@ describe("contextMenu element", () => {
|
||||
|
||||
it("shows context menu for element", () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(10, 10);
|
||||
mouse.up(20, 20);
|
||||
mouse.down(0, 0);
|
||||
mouse.up(10, 10);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
@@ -304,8 +304,8 @@ describe("contextMenu element", () => {
|
||||
|
||||
it("selecting 'Copy styles' in context menu copies styles", () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(10, 10);
|
||||
mouse.up(20, 20);
|
||||
mouse.down(0, 0);
|
||||
mouse.up(10, 10);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
@@ -389,8 +389,8 @@ describe("contextMenu element", () => {
|
||||
|
||||
it("selecting 'Delete' in context menu deletes element", () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(10, 10);
|
||||
mouse.up(20, 20);
|
||||
mouse.down(0, 0);
|
||||
mouse.up(10, 10);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
@@ -405,8 +405,8 @@ describe("contextMenu element", () => {
|
||||
|
||||
it("selecting 'Add to library' in context menu adds element to library", async () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(10, 10);
|
||||
mouse.up(20, 20);
|
||||
mouse.down(0, 0);
|
||||
mouse.up(10, 10);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
@@ -424,8 +424,8 @@ describe("contextMenu element", () => {
|
||||
|
||||
it("selecting 'Duplicate' in context menu duplicates element", () => {
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down(10, 10);
|
||||
mouse.up(20, 20);
|
||||
mouse.down(0, 0);
|
||||
mouse.up(10, 10);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
|
||||
@@ -31,9 +31,7 @@ exports[`restoreElements > should restore arrow element correctly 1`] = `
|
||||
],
|
||||
],
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": Any<Number>,
|
||||
"startArrowhead": null,
|
||||
"startBinding": null,
|
||||
@@ -193,9 +191,7 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = `
|
||||
],
|
||||
"pressures": [],
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": Any<Number>,
|
||||
"simulatePressure": true,
|
||||
"strokeColor": "#1e1e1e",
|
||||
@@ -242,9 +238,7 @@ exports[`restoreElements > should restore line and draw elements correctly 1`] =
|
||||
],
|
||||
"polygon": false,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": Any<Number>,
|
||||
"startArrowhead": null,
|
||||
"startBinding": null,
|
||||
@@ -292,9 +286,7 @@ exports[`restoreElements > should restore line and draw elements correctly 2`] =
|
||||
],
|
||||
"polygon": false,
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": Any<Number>,
|
||||
"startArrowhead": null,
|
||||
"startBinding": null,
|
||||
@@ -334,9 +326,7 @@ exports[`restoreElements > should restore text element correctly passing value f
|
||||
"opacity": 100,
|
||||
"originalText": "text",
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
@@ -378,9 +368,7 @@ exports[`restoreElements > should restore text element correctly with unknown fo
|
||||
"opacity": 100,
|
||||
"originalText": "",
|
||||
"roughness": 1,
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"roundness": null,
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
ExcalidrawTextContainer,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawImageElement,
|
||||
ElementsMap,
|
||||
} from "@excalidraw/element/types";
|
||||
|
||||
import { createTestHook } from "../../components/App";
|
||||
@@ -146,6 +147,7 @@ export class Keyboard {
|
||||
|
||||
const getElementPointForSelection = (
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
): GlobalPoint => {
|
||||
const { x, y, width, angle } = element;
|
||||
const target = pointFrom<GlobalPoint>(
|
||||
@@ -162,7 +164,7 @@ const getElementPointForSelection = (
|
||||
(bounds[1] + bounds[3]) / 2,
|
||||
);
|
||||
} else {
|
||||
center = elementCenterPoint(element);
|
||||
center = elementCenterPoint(element, elementsMap);
|
||||
}
|
||||
|
||||
if (isTextElement(element)) {
|
||||
@@ -299,7 +301,12 @@ export class Pointer {
|
||||
elements = Array.isArray(elements) ? elements : [elements];
|
||||
elements.forEach((element) => {
|
||||
this.reset();
|
||||
this.click(...getElementPointForSelection(element));
|
||||
this.click(
|
||||
...getElementPointForSelection(
|
||||
element,
|
||||
h.app.scene.getElementsMapIncludingDeleted(),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,13 +315,23 @@ export class Pointer {
|
||||
|
||||
clickOn(element: ExcalidrawElement) {
|
||||
this.reset();
|
||||
this.click(...getElementPointForSelection(element));
|
||||
this.click(
|
||||
...getElementPointForSelection(
|
||||
element,
|
||||
h.app.scene.getElementsMapIncludingDeleted(),
|
||||
),
|
||||
);
|
||||
this.reset();
|
||||
}
|
||||
|
||||
doubleClickOn(element: ExcalidrawElement) {
|
||||
this.reset();
|
||||
this.doubleClick(...getElementPointForSelection(element));
|
||||
this.doubleClick(
|
||||
...getElementPointForSelection(
|
||||
element,
|
||||
h.app.scene.getElementsMapIncludingDeleted(),
|
||||
),
|
||||
);
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
@@ -598,6 +615,7 @@ export class UI {
|
||||
|
||||
const mutations = cropElement(
|
||||
element,
|
||||
h.scene.getNonDeletedElementsMap(),
|
||||
handle,
|
||||
naturalWidth,
|
||||
naturalHeight,
|
||||
|
||||
@@ -70,6 +70,7 @@ const updatePath = (startPoint: GlobalPoint, points: LocalPoint[]) => {
|
||||
?.originalPoints?.map((p) => pointFrom<GlobalPoint>(p[0], p[1])) ??
|
||||
[],
|
||||
elements: h.elements,
|
||||
elementsMap: h.scene.getNonDeletedElementsMap(),
|
||||
elementsSegments,
|
||||
intersectedElements: new Set(),
|
||||
enclosedElements: new Set(),
|
||||
|
||||
@@ -124,8 +124,8 @@ describe("move element", () => {
|
||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||
expect([rectA.x, rectA.y]).toEqual([0, 0]);
|
||||
expect([rectB.x, rectB.y]).toEqual([201, 2]);
|
||||
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[107.07, 47.07]]);
|
||||
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[86.86, 87.3]]);
|
||||
expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[110, 50]]);
|
||||
expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[81, 81.4]]);
|
||||
|
||||
h.elements.forEach((element) => expect(element).toMatchSnapshot());
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ test("unselected bound arrow updates when rotating its target element", async ()
|
||||
expect(arrow.endBinding?.elementId).toEqual(rectangle.id);
|
||||
expect(arrow.x).toBeCloseTo(-80);
|
||||
expect(arrow.y).toBeCloseTo(50);
|
||||
expect(arrow.width).toBeCloseTo(116.7, 1);
|
||||
expect(arrow.width).toBeCloseTo(110.7, 1);
|
||||
expect(arrow.height).toBeCloseTo(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -682,7 +682,7 @@ describe("textWysiwyg", () => {
|
||||
expect(diamond.height).toBe(70);
|
||||
});
|
||||
|
||||
it("should bind text to container when double clicked on center of transparent container", async () => {
|
||||
it("should bind text to container when double clicked inside of the transparent container", async () => {
|
||||
const rectangle = API.createElement({
|
||||
type: "rectangle",
|
||||
x: 10,
|
||||
@@ -1500,9 +1500,7 @@ describe("textWysiwyg", () => {
|
||||
locked: false,
|
||||
opacity: 100,
|
||||
roughness: 1,
|
||||
roundness: {
|
||||
type: 3,
|
||||
},
|
||||
roundness: null,
|
||||
strokeColor: "#1e1e1e",
|
||||
strokeStyle: "solid",
|
||||
strokeWidth: 2,
|
||||
|
||||
Reference in New Issue
Block a user