Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 521896cccf | |||
| fe318126bd | |||
| 8ca4cf3260 | |||
| f3f0ab7c83 | |||
| 44a1c8d857 | |||
| e0a22edfbd | |||
| c07f5a0c80 |
+13
-1
@@ -126,6 +126,8 @@ import DebugCanvas, {
|
|||||||
loadSavedDebugState,
|
loadSavedDebugState,
|
||||||
} from "./components/DebugCanvas";
|
} from "./components/DebugCanvas";
|
||||||
import { AIComponents } from "./components/AI";
|
import { AIComponents } from "./components/AI";
|
||||||
|
import type { SaveWarningRef } from "./components/SaveWarning";
|
||||||
|
import { SaveWarning } from "./components/SaveWarning";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@@ -331,6 +333,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
|
|
||||||
const [langCode, setLangCode] = useAppLangCode();
|
const [langCode, setLangCode] = useAppLangCode();
|
||||||
|
|
||||||
|
const activityRef = useRef<SaveWarningRef | null>(null);
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -615,6 +619,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
collabAPI.syncElements(elements);
|
collabAPI.syncElements(elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activityRef.current?.activity();
|
||||||
|
|
||||||
// this check is redundant, but since this is a hot path, it's best
|
// this check is redundant, but since this is a hot path, it's best
|
||||||
// not to evaludate the nested expression every time
|
// not to evaludate the nested expression every time
|
||||||
if (!LocalData.isSavePaused()) {
|
if (!LocalData.isSavePaused()) {
|
||||||
@@ -649,7 +655,12 @@ const ExcalidrawWrapper = () => {
|
|||||||
|
|
||||||
// Render the debug scene if the debug canvas is available
|
// Render the debug scene if the debug canvas is available
|
||||||
if (debugCanvasRef.current && excalidrawAPI) {
|
if (debugCanvasRef.current && excalidrawAPI) {
|
||||||
debugRenderer(debugCanvasRef.current, appState, window.devicePixelRatio);
|
debugRenderer(
|
||||||
|
debugCanvasRef.current,
|
||||||
|
appState,
|
||||||
|
window.devicePixelRatio,
|
||||||
|
() => forceRefresh((prev) => !prev),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -851,6 +862,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
setTheme={(theme) => setAppTheme(theme)}
|
setTheme={(theme) => setAppTheme(theme)}
|
||||||
refresh={() => forceRefresh((prev) => !prev)}
|
refresh={() => forceRefresh((prev) => !prev)}
|
||||||
/>
|
/>
|
||||||
|
<SaveWarning ref={activityRef} />
|
||||||
<AppWelcomeScreen
|
<AppWelcomeScreen
|
||||||
onCollabDialogOpen={onCollabDialogOpen}
|
onCollabDialogOpen={onCollabDialogOpen}
|
||||||
isCollabEnabled={!isCollabDisabled}
|
isCollabEnabled={!isCollabDisabled}
|
||||||
|
|||||||
@@ -68,12 +68,17 @@ const _debugRenderer = (
|
|||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
scale: number,
|
scale: number,
|
||||||
|
refresh: () => void,
|
||||||
) => {
|
) => {
|
||||||
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
||||||
canvas,
|
canvas,
|
||||||
scale,
|
scale,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (appState.height !== canvas.height || appState.width !== canvas.width) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
const context = bootstrapCanvas({
|
const context = bootstrapCanvas({
|
||||||
canvas,
|
canvas,
|
||||||
scale,
|
scale,
|
||||||
@@ -138,8 +143,13 @@ export const saveDebugState = (debug: { enabled: boolean }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const debugRenderer = throttleRAF(
|
export const debugRenderer = throttleRAF(
|
||||||
(canvas: HTMLCanvasElement, appState: AppState, scale: number) => {
|
(
|
||||||
_debugRenderer(canvas, appState, scale);
|
canvas: HTMLCanvasElement,
|
||||||
|
appState: AppState,
|
||||||
|
scale: number,
|
||||||
|
refresh: () => void,
|
||||||
|
) => {
|
||||||
|
_debugRenderer(canvas, appState, scale, refresh);
|
||||||
},
|
},
|
||||||
{ trailing: true },
|
{ trailing: true },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||||
|
import { t } from "../../packages/excalidraw/i18n";
|
||||||
|
import { getShortcutKey } from "../../packages/excalidraw/utils";
|
||||||
|
|
||||||
|
export type SaveWarningRef = {
|
||||||
|
activity: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SaveWarning = forwardRef<SaveWarningRef, {}>((props, ref) => {
|
||||||
|
const dialogRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
/**
|
||||||
|
* Call this API method via the ref to hide warning message
|
||||||
|
* and start an idle timer again.
|
||||||
|
*/
|
||||||
|
activity: async () => {
|
||||||
|
if (timerRef.current != null) {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
dialogRef.current?.classList.remove("animate");
|
||||||
|
}
|
||||||
|
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
timerRef.current = null;
|
||||||
|
dialogRef.current?.classList.add("animate");
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={dialogRef} className="alert-save">
|
||||||
|
<div className="dialog">
|
||||||
|
{t("alerts.saveYourContent", {
|
||||||
|
shortcut: getShortcutKey("CtrlOrCmd + S"),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -18,6 +18,43 @@
|
|||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert-save {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 10vh;
|
||||||
|
margin-inline: auto;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0s;
|
||||||
|
|
||||||
|
&.animate {
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 0.2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
margin-inline: 10px;
|
||||||
|
padding: 1rem;
|
||||||
|
padding-inline: 1.25rem;
|
||||||
|
|
||||||
|
resize: none;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
background-color: var(--color-warning);
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
border: 1px solid var(--dialog-border-color);
|
||||||
|
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--color-text-warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.encrypted-icon {
|
.encrypted-icon {
|
||||||
border-radius: var(--space-factor);
|
border-radius: var(--space-factor);
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Excalidraw } from "../index";
|
||||||
|
import { render } from "../tests/test-utils";
|
||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
import { point } from "../../math";
|
||||||
|
import { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
describe("flipping re-centers selection", () => {
|
||||||
|
it("elbow arrow touches group selection side yet it remains in place after multiple moves", async () => {
|
||||||
|
const elements = [
|
||||||
|
API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "rec1",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
boundElements: [{ id: "arr", type: "arrow" }],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "rec2",
|
||||||
|
x: 220,
|
||||||
|
y: 250,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
boundElements: [{ id: "arr", type: "arrow" }],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arr",
|
||||||
|
x: 149.9,
|
||||||
|
y: 95,
|
||||||
|
width: 156,
|
||||||
|
height: 239.9,
|
||||||
|
startBinding: {
|
||||||
|
elementId: "rec1",
|
||||||
|
focus: 0,
|
||||||
|
gap: 5,
|
||||||
|
fixedPoint: [0.49, -0.05],
|
||||||
|
},
|
||||||
|
endBinding: {
|
||||||
|
elementId: "rec2",
|
||||||
|
focus: 0,
|
||||||
|
gap: 5,
|
||||||
|
fixedPoint: [-0.05, 0.49],
|
||||||
|
},
|
||||||
|
startArrowhead: null,
|
||||||
|
endArrowhead: "arrow",
|
||||||
|
points: [
|
||||||
|
point(0, 0),
|
||||||
|
point(0, -35),
|
||||||
|
point(-90.9, -35),
|
||||||
|
point(-90.9, 204.9),
|
||||||
|
point(65.1, 204.9),
|
||||||
|
],
|
||||||
|
elbowed: true,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
await render(<Excalidraw initialData={{ elements }} />);
|
||||||
|
|
||||||
|
API.setSelectedElements(elements);
|
||||||
|
|
||||||
|
expect(Object.keys(h.state.selectedElementIds).length).toBe(3);
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
|
||||||
|
const rec1 = h.elements.find((el) => el.id === "rec1");
|
||||||
|
expect(rec1?.x).toBeCloseTo(100);
|
||||||
|
expect(rec1?.y).toBeCloseTo(100);
|
||||||
|
|
||||||
|
const rec2 = h.elements.find((el) => el.id === "rec2");
|
||||||
|
expect(rec2?.x).toBeCloseTo(220);
|
||||||
|
expect(rec2?.y).toBeCloseTo(250);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("flipping arrowheads", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flipping bound arrow should flip arrowheads only", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ type: "arrow", id: "arrow1" }],
|
||||||
|
});
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
startArrowhead: "arrow",
|
||||||
|
endArrowhead: null,
|
||||||
|
endBinding: {
|
||||||
|
elementId: rect.id,
|
||||||
|
focus: 0.5,
|
||||||
|
gap: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rect, arrow]);
|
||||||
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe(null);
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe(null);
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("arrow");
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe(null);
|
||||||
|
|
||||||
|
API.executeAction(actionFlipVertical);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe(null);
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("arrow");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flipping bound arrow should flip arrowheads only 2", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ type: "arrow", id: "arrow1" }],
|
||||||
|
});
|
||||||
|
const rect2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ type: "arrow", id: "arrow1" }],
|
||||||
|
});
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
startArrowhead: "arrow",
|
||||||
|
endArrowhead: "circle",
|
||||||
|
startBinding: {
|
||||||
|
elementId: rect.id,
|
||||||
|
focus: 0.5,
|
||||||
|
gap: 5,
|
||||||
|
},
|
||||||
|
endBinding: {
|
||||||
|
elementId: rect2.id,
|
||||||
|
focus: 0.5,
|
||||||
|
gap: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rect, rect2, arrow]);
|
||||||
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("circle");
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("circle");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("arrow");
|
||||||
|
|
||||||
|
API.executeAction(actionFlipVertical);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("circle");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flipping unbound arrow shouldn't flip arrowheads", () => {
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
startArrowhead: "arrow",
|
||||||
|
endArrowhead: "circle",
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([arrow]);
|
||||||
|
API.setSelectedElements([arrow]);
|
||||||
|
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("circle");
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe("circle");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flipping bound arrow shouldn't flip arrowheads if selected alongside non-arrow eleemnt", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ type: "arrow", id: "arrow1" }],
|
||||||
|
});
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
id: "arrow1",
|
||||||
|
startArrowhead: "arrow",
|
||||||
|
endArrowhead: null,
|
||||||
|
endBinding: {
|
||||||
|
elementId: rect.id,
|
||||||
|
focus: 0.5,
|
||||||
|
gap: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([rect, arrow]);
|
||||||
|
API.setSelectedElements([rect, arrow]);
|
||||||
|
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe(null);
|
||||||
|
|
||||||
|
API.executeAction(actionFlipHorizontal);
|
||||||
|
expect(API.getElement(arrow).startArrowhead).toBe("arrow");
|
||||||
|
expect(API.getElement(arrow).endArrowhead).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,8 @@ import { register } from "./register";
|
|||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import type {
|
import type {
|
||||||
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
@@ -18,7 +20,13 @@ import {
|
|||||||
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
import { flipHorizontal, flipVertical } from "../components/icons";
|
import { flipHorizontal, flipVertical } from "../components/icons";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import {
|
||||||
|
isArrowElement,
|
||||||
|
isElbowArrow,
|
||||||
|
isLinearElement,
|
||||||
|
} from "../element/typeChecks";
|
||||||
|
import { mutateElbowArrow } from "../element/routing";
|
||||||
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
|
|
||||||
export const actionFlipHorizontal = register({
|
export const actionFlipHorizontal = register({
|
||||||
name: "flipHorizontal",
|
name: "flipHorizontal",
|
||||||
@@ -109,7 +117,23 @@ const flipElements = (
|
|||||||
flipDirection: "horizontal" | "vertical",
|
flipDirection: "horizontal" | "vertical",
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
const { minX, minY, maxX, maxY } = getCommonBoundingBox(selectedElements);
|
if (
|
||||||
|
selectedElements.every(
|
||||||
|
(element) =>
|
||||||
|
isArrowElement(element) && (element.startBinding || element.endBinding),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return selectedElements.map((element) => {
|
||||||
|
const _element = element as ExcalidrawArrowElement;
|
||||||
|
return newElementWith(_element, {
|
||||||
|
startArrowhead: _element.endArrowhead,
|
||||||
|
endArrowhead: _element.startArrowhead,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { minX, minY, maxX, maxY, midX, midY } =
|
||||||
|
getCommonBoundingBox(selectedElements);
|
||||||
|
|
||||||
resizeMultipleElements(
|
resizeMultipleElements(
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -131,5 +155,48 @@ const flipElements = (
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// flipping arrow elements (and potentially other) makes the selection group
|
||||||
|
// "move" across the canvas because of how arrows can bump against the "wall"
|
||||||
|
// of the selection, so we need to center the group back to the original
|
||||||
|
// position so that repeated flips don't accumulate the offset
|
||||||
|
|
||||||
|
const { elbowArrows, otherElements } = selectedElements.reduce(
|
||||||
|
(
|
||||||
|
acc: {
|
||||||
|
elbowArrows: ExcalidrawElbowArrowElement[];
|
||||||
|
otherElements: ExcalidrawElement[];
|
||||||
|
},
|
||||||
|
element,
|
||||||
|
) =>
|
||||||
|
isElbowArrow(element)
|
||||||
|
? { ...acc, elbowArrows: acc.elbowArrows.concat(element) }
|
||||||
|
: { ...acc, otherElements: acc.otherElements.concat(element) },
|
||||||
|
{ elbowArrows: [], otherElements: [] },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { midX: newMidX, midY: newMidY } =
|
||||||
|
getCommonBoundingBox(selectedElements);
|
||||||
|
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
|
||||||
|
otherElements.forEach((element) =>
|
||||||
|
mutateElement(element, {
|
||||||
|
x: element.x + diffX,
|
||||||
|
y: element.y + diffY,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
elbowArrows.forEach((element) =>
|
||||||
|
mutateElbowArrow(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
element.points,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
informMutation: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
return selectedElements;
|
return selectedElements;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1685,19 +1685,6 @@ export const actionChangeArrowType = register({
|
|||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
mutateElement(
|
|
||||||
newElement,
|
|
||||||
{
|
|
||||||
startBinding: newElement.startBinding
|
|
||||||
? { ...newElement.startBinding, fixedPoint: null }
|
|
||||||
: null,
|
|
||||||
endBinding: newElement.endBinding
|
|
||||||
? { ...newElement.endBinding, fixedPoint: null }
|
|
||||||
: null,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return newElement;
|
return newElement;
|
||||||
|
|||||||
@@ -1,90 +1,5 @@
|
|||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import type { Merge } from "./utility-types";
|
import type { Merge } from "./utility-types";
|
||||||
import { clamp } from "../math/utils";
|
|
||||||
import tinycolor from "tinycolor2";
|
|
||||||
import { degreesToRadians } from "../math/angle";
|
|
||||||
import type { Degrees } from "../math/types";
|
|
||||||
|
|
||||||
function cssHueRotate(
|
|
||||||
red: number,
|
|
||||||
green: number,
|
|
||||||
blue: number,
|
|
||||||
degrees: Degrees,
|
|
||||||
): { r: number; g: number; b: number } {
|
|
||||||
// normalize
|
|
||||||
const r = red / 255;
|
|
||||||
const g = green / 255;
|
|
||||||
const b = blue / 255;
|
|
||||||
|
|
||||||
// Convert degrees to radians
|
|
||||||
const a = degreesToRadians(degrees);
|
|
||||||
|
|
||||||
const c = Math.cos(a);
|
|
||||||
const s = Math.sin(a);
|
|
||||||
|
|
||||||
// rotation matrix
|
|
||||||
const matrix = [
|
|
||||||
0.213 + c * 0.787 - s * 0.213,
|
|
||||||
0.715 - c * 0.715 - s * 0.715,
|
|
||||||
0.072 - c * 0.072 + s * 0.928,
|
|
||||||
0.213 - c * 0.213 + s * 0.143,
|
|
||||||
0.715 + c * 0.285 + s * 0.14,
|
|
||||||
0.072 - c * 0.072 - s * 0.283,
|
|
||||||
0.213 - c * 0.213 - s * 0.787,
|
|
||||||
0.715 - c * 0.715 + s * 0.715,
|
|
||||||
0.072 + c * 0.928 + s * 0.072,
|
|
||||||
];
|
|
||||||
|
|
||||||
// transform
|
|
||||||
const newR = r * matrix[0] + g * matrix[1] + b * matrix[2];
|
|
||||||
const newG = r * matrix[3] + g * matrix[4] + b * matrix[5];
|
|
||||||
const newB = r * matrix[6] + g * matrix[7] + b * matrix[8];
|
|
||||||
|
|
||||||
// clamp the values to [0, 1] range and convert back to [0, 255]
|
|
||||||
return {
|
|
||||||
r: Math.round(Math.max(0, Math.min(1, newR)) * 255),
|
|
||||||
g: Math.round(Math.max(0, Math.min(1, newG)) * 255),
|
|
||||||
b: Math.round(Math.max(0, Math.min(1, newB)) * 255),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const cssInvert = (
|
|
||||||
r: number,
|
|
||||||
g: number,
|
|
||||||
b: number,
|
|
||||||
percent: number,
|
|
||||||
): { r: number; g: number; b: number } => {
|
|
||||||
const p = clamp(percent, 0, 100) / 100;
|
|
||||||
|
|
||||||
// Function to invert a single color component
|
|
||||||
const invertComponent = (color: number): number => {
|
|
||||||
// Apply the invert formula
|
|
||||||
const inverted = color * (1 - p) + (255 - color) * p;
|
|
||||||
// Round to the nearest integer and clamp to [0, 255]
|
|
||||||
return Math.round(clamp(inverted, 0, 255));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate the inverted RGB components
|
|
||||||
const invertedR = invertComponent(r);
|
|
||||||
const invertedG = invertComponent(g);
|
|
||||||
const invertedB = invertComponent(b);
|
|
||||||
|
|
||||||
return { r: invertedR, g: invertedG, b: invertedB };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const applyDarkModeFilter = (color: string) => {
|
|
||||||
let tc = tinycolor(color);
|
|
||||||
|
|
||||||
const _alpha = tc._a;
|
|
||||||
|
|
||||||
// order of operations matters
|
|
||||||
// (corresponds to "filter: invert(invertPercent) hue-rotate(hueDegrees)" in css)
|
|
||||||
tc = tinycolor(cssInvert(tc._r, tc._g, tc._b, 93));
|
|
||||||
tc = tinycolor(cssHueRotate(tc._r, tc._g, tc._b, 180 as Degrees));
|
|
||||||
tc.setAlpha(_alpha);
|
|
||||||
|
|
||||||
return tc.toHex8String();
|
|
||||||
};
|
|
||||||
|
|
||||||
// FIXME can't put to utils.ts rn because of circular dependency
|
// FIXME can't put to utils.ts rn because of circular dependency
|
||||||
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ import type {
|
|||||||
MagicGenerationData,
|
MagicGenerationData,
|
||||||
ExcalidrawNonSelectionElement,
|
ExcalidrawNonSelectionElement,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getCenter, getDistance } from "../gesture";
|
import { getCenter, getDistance } from "../gesture";
|
||||||
import {
|
import {
|
||||||
@@ -287,6 +288,7 @@ import {
|
|||||||
getDateTime,
|
getDateTime,
|
||||||
isShallowEqual,
|
isShallowEqual,
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
|
toBrandedType,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import {
|
import {
|
||||||
createSrcDoc,
|
createSrcDoc,
|
||||||
@@ -435,7 +437,7 @@ import { actionTextAutoResize } from "../actions/actionTextAutoResize";
|
|||||||
import { getVisibleSceneBounds } from "../element/bounds";
|
import { getVisibleSceneBounds } from "../element/bounds";
|
||||||
import { isMaybeMermaidDefinition } from "../mermaid";
|
import { isMaybeMermaidDefinition } from "../mermaid";
|
||||||
import NewElementCanvas from "./canvases/NewElementCanvas";
|
import NewElementCanvas from "./canvases/NewElementCanvas";
|
||||||
import { mutateElbowArrow } from "../element/routing";
|
import { mutateElbowArrow, updateElbowArrow } from "../element/routing";
|
||||||
import {
|
import {
|
||||||
FlowChartCreator,
|
FlowChartCreator,
|
||||||
FlowChartNavigator,
|
FlowChartNavigator,
|
||||||
@@ -1700,7 +1702,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elementsPendingErasure: this.elementsPendingErasure,
|
elementsPendingErasure: this.elementsPendingErasure,
|
||||||
pendingFlowchartNodes:
|
pendingFlowchartNodes:
|
||||||
this.flowChartCreator.pendingNodes,
|
this.flowChartCreator.pendingNodes,
|
||||||
theme: this.state.theme,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{this.state.newElement && (
|
{this.state.newElement && (
|
||||||
@@ -1721,7 +1722,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elementsPendingErasure:
|
elementsPendingErasure:
|
||||||
this.elementsPendingErasure,
|
this.elementsPendingErasure,
|
||||||
pendingFlowchartNodes: null,
|
pendingFlowchartNodes: null,
|
||||||
theme: this.state.theme,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -2697,11 +2697,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (prevState.theme !== this.state.theme) {
|
|
||||||
this.scene
|
|
||||||
.getElementsIncludingDeleted()
|
|
||||||
.forEach((element) => ShapeCache.delete(element));
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
this.state.activeTool.type === "eraser" &&
|
this.state.activeTool.type === "eraser" &&
|
||||||
prevState.theme !== this.state.theme
|
prevState.theme !== this.state.theme
|
||||||
@@ -3116,7 +3111,45 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
retainSeed?: boolean;
|
retainSeed?: boolean;
|
||||||
fitToContent?: boolean;
|
fitToContent?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const elements = restoreElements(opts.elements, null, undefined);
|
let elements = opts.elements.map((el, _, elements) => {
|
||||||
|
if (isElbowArrow(el)) {
|
||||||
|
const startEndElements = [
|
||||||
|
el.startBinding &&
|
||||||
|
elements.find((l) => l.id === el.startBinding?.elementId),
|
||||||
|
el.endBinding &&
|
||||||
|
elements.find((l) => l.id === el.endBinding?.elementId),
|
||||||
|
];
|
||||||
|
const startBinding = startEndElements[0] ? el.startBinding : null;
|
||||||
|
const endBinding = startEndElements[1] ? el.endBinding : null;
|
||||||
|
return {
|
||||||
|
...el,
|
||||||
|
...updateElbowArrow(
|
||||||
|
{
|
||||||
|
...el,
|
||||||
|
startBinding,
|
||||||
|
endBinding,
|
||||||
|
},
|
||||||
|
toBrandedType<NonDeletedSceneElementsMap>(
|
||||||
|
new Map(
|
||||||
|
startEndElements
|
||||||
|
.filter((x) => x != null)
|
||||||
|
.map(
|
||||||
|
(el) =>
|
||||||
|
[el!.id, el] as [
|
||||||
|
string,
|
||||||
|
Ordered<NonDeletedExcalidrawElement>,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[el.points[0], el.points[el.points.length - 1]],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return el;
|
||||||
|
});
|
||||||
|
elements = restoreElements(elements, null, undefined);
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
|
|
||||||
const elementsCenterX = distance(minX, maxX) / 2;
|
const elementsCenterX = distance(minX, maxX) / 2;
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ body.excalidraw-cursor-resize * {
|
|||||||
// recommends surface color of #121212, 93% yields #111111 for #FFF
|
// recommends surface color of #121212, 93% yields #111111 for #FFF
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
// filter: var(--theme-filter);
|
filter: var(--theme-filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -189,11 +189,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$theme-filter: "invert(93%) hue-rotate(180deg)"; // prod
|
$theme-filter: "invert(93%) hue-rotate(180deg)";
|
||||||
// $theme-filter: "invert(93%)"; // prod
|
|
||||||
// $theme-filter: "hue-rotate(180deg)"; // prod
|
|
||||||
// $theme-filter: "hue-rotate(180deg) invert(93%)";
|
|
||||||
|
|
||||||
$right-sidebar-width: "302px";
|
$right-sidebar-width: "302px";
|
||||||
|
|
||||||
:export {
|
:export {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawSelectionElement,
|
ExcalidrawSelectionElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
|
FixedPointBinding,
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
PointBinding,
|
PointBinding,
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
|
isFixedPointBinding,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
isUsingAdaptiveRadius,
|
isUsingAdaptiveRadius,
|
||||||
@@ -101,8 +103,8 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
|
|||||||
|
|
||||||
const repairBinding = (
|
const repairBinding = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
binding: PointBinding | null,
|
binding: PointBinding | FixedPointBinding | null,
|
||||||
): PointBinding | null => {
|
): PointBinding | FixedPointBinding | null => {
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -110,9 +112,11 @@ const repairBinding = (
|
|||||||
return {
|
return {
|
||||||
...binding,
|
...binding,
|
||||||
focus: binding.focus || 0,
|
focus: binding.focus || 0,
|
||||||
fixedPoint: isElbowArrow(element)
|
...(isElbowArrow(element) && isFixedPointBinding(binding)
|
||||||
? normalizeFixedPoint(binding.fixedPoint ?? [0, 0])
|
? {
|
||||||
: null,
|
fixedPoint: normalizeFixedPoint(binding.fixedPoint ?? [0, 0]),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
isBindingElement,
|
isBindingElement,
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
|
isFixedPointBinding,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isRectangularElement,
|
isRectangularElement,
|
||||||
@@ -797,7 +798,7 @@ export const bindPointToSnapToElementOutline = (
|
|||||||
isVertical
|
isVertical
|
||||||
? Math.abs(p[1] - i[1]) < 0.1
|
? Math.abs(p[1] - i[1]) < 0.1
|
||||||
: Math.abs(p[0] - i[0]) < 0.1,
|
: Math.abs(p[0] - i[0]) < 0.1,
|
||||||
)[0] ?? point;
|
)[0] ?? p;
|
||||||
}
|
}
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
@@ -1013,7 +1014,7 @@ const updateBoundPoint = (
|
|||||||
const direction = startOrEnd === "startBinding" ? -1 : 1;
|
const direction = startOrEnd === "startBinding" ? -1 : 1;
|
||||||
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
|
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
|
||||||
|
|
||||||
if (isElbowArrow(linearElement)) {
|
if (isElbowArrow(linearElement) && isFixedPointBinding(binding)) {
|
||||||
const fixedPoint =
|
const fixedPoint =
|
||||||
normalizeFixedPoint(binding.fixedPoint) ??
|
normalizeFixedPoint(binding.fixedPoint) ??
|
||||||
calculateFixedPointForElbowArrowBinding(
|
calculateFixedPointForElbowArrowBinding(
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export const dragSelectedElements = (
|
|||||||
) => {
|
) => {
|
||||||
if (
|
if (
|
||||||
_selectedElements.length === 1 &&
|
_selectedElements.length === 1 &&
|
||||||
isArrowElement(_selectedElements[0]) &&
|
|
||||||
isElbowArrow(_selectedElements[0]) &&
|
isElbowArrow(_selectedElements[0]) &&
|
||||||
(_selectedElements[0].startBinding || _selectedElements[0].endBinding)
|
(_selectedElements[0].startBinding || _selectedElements[0].endBinding)
|
||||||
) {
|
) {
|
||||||
@@ -43,13 +42,7 @@ export const dragSelectedElements = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedElements = _selectedElements.filter(
|
const selectedElements = _selectedElements.filter(
|
||||||
(el) =>
|
(el) => !(isElbowArrow(el) && el.startBinding && el.endBinding),
|
||||||
!(
|
|
||||||
isArrowElement(el) &&
|
|
||||||
isElbowArrow(el) &&
|
|
||||||
el.startBinding &&
|
|
||||||
el.endBinding
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// we do not want a frame and its elements to be selected at the same time
|
// we do not want a frame and its elements to be selected at the same time
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export class LinearElementEditor {
|
|||||||
public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
|
public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
|
||||||
public readonly hoverPointIndex: number;
|
public readonly hoverPointIndex: number;
|
||||||
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
|
||||||
|
public readonly elbowed: boolean;
|
||||||
|
|
||||||
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
|
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
|
||||||
this.elementId = element.id as string & {
|
this.elementId = element.id as string & {
|
||||||
@@ -131,6 +132,7 @@ export class LinearElementEditor {
|
|||||||
};
|
};
|
||||||
this.hoverPointIndex = -1;
|
this.hoverPointIndex = -1;
|
||||||
this.segmentMidPointHoveredCoords = null;
|
this.segmentMidPointHoveredCoords = null;
|
||||||
|
this.elbowed = isElbowArrow(element) && element.elbowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1477,7 +1479,9 @@ export class LinearElementEditor {
|
|||||||
nextPoints,
|
nextPoints,
|
||||||
vector(offsetX, offsetY),
|
vector(offsetX, offsetY),
|
||||||
bindings,
|
bindings,
|
||||||
options,
|
{
|
||||||
|
isDragging: options?.isDragging,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
const nextCoords = getElementPointsCoords(element, nextPoints);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
ExcalidrawArrowElement,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -909,6 +910,8 @@ export const resizeMultipleElements = (
|
|||||||
fontSize?: ExcalidrawTextElement["fontSize"];
|
fontSize?: ExcalidrawTextElement["fontSize"];
|
||||||
scale?: ExcalidrawImageElement["scale"];
|
scale?: ExcalidrawImageElement["scale"];
|
||||||
boundTextFontSize?: ExcalidrawTextElement["fontSize"];
|
boundTextFontSize?: ExcalidrawTextElement["fontSize"];
|
||||||
|
startBinding?: ExcalidrawArrowElement["startBinding"];
|
||||||
|
endBinding?: ExcalidrawArrowElement["endBinding"];
|
||||||
};
|
};
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
@@ -993,19 +996,6 @@ export const resizeMultipleElements = (
|
|||||||
|
|
||||||
mutateElement(element, update, false);
|
mutateElement(element, update, false);
|
||||||
|
|
||||||
if (isArrowElement(element) && isElbowArrow(element)) {
|
|
||||||
mutateElbowArrow(
|
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
element.points,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
informMutation: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBoundElements(element, elementsMap, {
|
updateBoundElements(element, elementsMap, {
|
||||||
simultaneouslyUpdated: elementsToUpdate,
|
simultaneouslyUpdated: elementsToUpdate,
|
||||||
oldSize: { width: oldWidth, height: oldHeight },
|
oldSize: { width: oldWidth, height: oldHeight },
|
||||||
@@ -1059,7 +1049,7 @@ const rotateMultipleElements = (
|
|||||||
(centerAngle + origAngle - element.angle) as Radians,
|
(centerAngle + origAngle - element.angle) as Radians,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isArrowElement(element) && isElbowArrow(element)) {
|
if (isElbowArrow(element)) {
|
||||||
const points = getArrowLocalFixedPoints(element, elementsMap);
|
const points = getArrowLocalFixedPoints(element, elementsMap);
|
||||||
mutateElbowArrow(element, elementsMap, points);
|
mutateElbowArrow(element, elementsMap, points);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -94,7 +94,16 @@ describe("elbow arrow routing", () => {
|
|||||||
|
|
||||||
describe("elbow arrow ui", () => {
|
describe("elbow arrow ui", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
localStorage.clear();
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
|
||||||
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 1,
|
||||||
|
clientY: 1,
|
||||||
|
});
|
||||||
|
const contextMenu = UI.queryContextMenu();
|
||||||
|
fireEvent.click(queryByTestId(contextMenu!, "stats")!);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can follow bound shapes", async () => {
|
it("can follow bound shapes", async () => {
|
||||||
@@ -130,8 +139,8 @@ describe("elbow arrow ui", () => {
|
|||||||
expect(arrow.elbowed).toBe(true);
|
expect(arrow.elbowed).toBe(true);
|
||||||
expect(arrow.points).toEqual([
|
expect(arrow.points).toEqual([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[35, 0],
|
[45, 0],
|
||||||
[35, 200],
|
[45, 200],
|
||||||
[90, 200],
|
[90, 200],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -163,14 +172,6 @@ describe("elbow arrow ui", () => {
|
|||||||
h.state,
|
h.state,
|
||||||
)[0] as ExcalidrawArrowElement;
|
)[0] as ExcalidrawArrowElement;
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
|
||||||
button: 2,
|
|
||||||
clientX: 1,
|
|
||||||
clientY: 1,
|
|
||||||
});
|
|
||||||
const contextMenu = UI.queryContextMenu();
|
|
||||||
fireEvent.click(queryByTestId(contextMenu!, "stats")!);
|
|
||||||
|
|
||||||
mouse.click(51, 51);
|
mouse.click(51, 51);
|
||||||
|
|
||||||
const inputAngle = UI.queryStatsProperty("A")?.querySelector(
|
const inputAngle = UI.queryStatsProperty("A")?.querySelector(
|
||||||
@@ -182,8 +183,8 @@ describe("elbow arrow ui", () => {
|
|||||||
[0, 0],
|
[0, 0],
|
||||||
[35, 0],
|
[35, 0],
|
||||||
[35, 90],
|
[35, 90],
|
||||||
[25, 90],
|
[35, 90], // Note that coordinates are rounded above!
|
||||||
[25, 165],
|
[35, 165],
|
||||||
[103, 165],
|
[103, 165],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ import {
|
|||||||
HEADING_UP,
|
HEADING_UP,
|
||||||
vectorToHeading,
|
vectorToHeading,
|
||||||
} from "./heading";
|
} from "./heading";
|
||||||
|
import type { ElementUpdate } from "./mutateElement";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { isBindableElement, isRectanguloidElement } from "./typeChecks";
|
import { isBindableElement, isRectanguloidElement } from "./typeChecks";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
FixedPointBinding,
|
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -72,16 +72,48 @@ export const mutateElbowArrow = (
|
|||||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
nextPoints: readonly LocalPoint[],
|
nextPoints: readonly LocalPoint[],
|
||||||
offset?: Vector,
|
offset?: Vector,
|
||||||
otherUpdates?: {
|
otherUpdates?: Omit<
|
||||||
startBinding?: FixedPointBinding | null;
|
ElementUpdate<ExcalidrawElbowArrowElement>,
|
||||||
endBinding?: FixedPointBinding | null;
|
"angle" | "x" | "y" | "width" | "height" | "elbowed" | "points"
|
||||||
|
>,
|
||||||
|
options?: {
|
||||||
|
isDragging?: boolean;
|
||||||
|
informMutation?: boolean;
|
||||||
},
|
},
|
||||||
|
) => {
|
||||||
|
const update = updateElbowArrow(
|
||||||
|
arrow,
|
||||||
|
elementsMap,
|
||||||
|
nextPoints,
|
||||||
|
offset,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
if (update) {
|
||||||
|
mutateElement(
|
||||||
|
arrow,
|
||||||
|
{
|
||||||
|
...otherUpdates,
|
||||||
|
...update,
|
||||||
|
angle: 0 as Radians,
|
||||||
|
},
|
||||||
|
options?.informMutation,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error("Elbow arrow cannot find a route");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateElbowArrow = (
|
||||||
|
arrow: ExcalidrawElbowArrowElement,
|
||||||
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
|
nextPoints: readonly LocalPoint[],
|
||||||
|
offset?: Vector,
|
||||||
options?: {
|
options?: {
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
disableBinding?: boolean;
|
disableBinding?: boolean;
|
||||||
informMutation?: boolean;
|
informMutation?: boolean;
|
||||||
},
|
},
|
||||||
) => {
|
): ElementUpdate<ExcalidrawElbowArrowElement> | null => {
|
||||||
const origStartGlobalPoint: GlobalPoint = pointTranslate(
|
const origStartGlobalPoint: GlobalPoint = pointTranslate(
|
||||||
pointTranslate<LocalPoint, GlobalPoint>(
|
pointTranslate<LocalPoint, GlobalPoint>(
|
||||||
nextPoints[0],
|
nextPoints[0],
|
||||||
@@ -235,6 +267,8 @@ export const mutateElbowArrow = (
|
|||||||
BASE_PADDING,
|
BASE_PADDING,
|
||||||
),
|
),
|
||||||
boundsOverlap,
|
boundsOverlap,
|
||||||
|
hoveredStartElement && aabbForElement(hoveredStartElement),
|
||||||
|
hoveredEndElement && aabbForElement(hoveredEndElement),
|
||||||
);
|
);
|
||||||
const startDonglePosition = getDonglePosition(
|
const startDonglePosition = getDonglePosition(
|
||||||
dynamicAABBs[0],
|
dynamicAABBs[0],
|
||||||
@@ -295,18 +329,10 @@ export const mutateElbowArrow = (
|
|||||||
startDongle && points.unshift(startGlobalPoint);
|
startDongle && points.unshift(startGlobalPoint);
|
||||||
endDongle && points.push(endGlobalPoint);
|
endDongle && points.push(endGlobalPoint);
|
||||||
|
|
||||||
mutateElement(
|
return normalizedArrowElementUpdate(simplifyElbowArrowPoints(points), 0, 0);
|
||||||
arrow,
|
|
||||||
{
|
|
||||||
...otherUpdates,
|
|
||||||
...normalizedArrowElementUpdate(simplifyElbowArrowPoints(points), 0, 0),
|
|
||||||
angle: 0 as Radians,
|
|
||||||
},
|
|
||||||
options?.informMutation,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error("Elbow arrow cannot find a route");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const offsetFromHeading = (
|
const offsetFromHeading = (
|
||||||
@@ -475,7 +501,11 @@ const generateDynamicAABBs = (
|
|||||||
startDifference?: [number, number, number, number],
|
startDifference?: [number, number, number, number],
|
||||||
endDifference?: [number, number, number, number],
|
endDifference?: [number, number, number, number],
|
||||||
disableSideHack?: boolean,
|
disableSideHack?: boolean,
|
||||||
|
startElementBounds?: Bounds | null,
|
||||||
|
endElementBounds?: Bounds | null,
|
||||||
): Bounds[] => {
|
): Bounds[] => {
|
||||||
|
const startEl = startElementBounds ?? a;
|
||||||
|
const endEl = endElementBounds ?? b;
|
||||||
const [startUp, startRight, startDown, startLeft] = startDifference ?? [
|
const [startUp, startRight, startDown, startLeft] = startDifference ?? [
|
||||||
0, 0, 0, 0,
|
0, 0, 0, 0,
|
||||||
];
|
];
|
||||||
@@ -484,29 +514,29 @@ const generateDynamicAABBs = (
|
|||||||
const first = [
|
const first = [
|
||||||
a[0] > b[2]
|
a[0] > b[2]
|
||||||
? a[1] > b[3] || a[3] < b[1]
|
? a[1] > b[3] || a[3] < b[1]
|
||||||
? Math.min((a[0] + b[2]) / 2, a[0] - startLeft)
|
? Math.min((startEl[0] + endEl[2]) / 2, a[0] - startLeft)
|
||||||
: (a[0] + b[2]) / 2
|
: (startEl[0] + endEl[2]) / 2
|
||||||
: a[0] > b[0]
|
: a[0] > b[0]
|
||||||
? a[0] - startLeft
|
? a[0] - startLeft
|
||||||
: common[0] - startLeft,
|
: common[0] - startLeft,
|
||||||
a[1] > b[3]
|
a[1] > b[3]
|
||||||
? a[0] > b[2] || a[2] < b[0]
|
? a[0] > b[2] || a[2] < b[0]
|
||||||
? Math.min((a[1] + b[3]) / 2, a[1] - startUp)
|
? Math.min((startEl[1] + endEl[3]) / 2, a[1] - startUp)
|
||||||
: (a[1] + b[3]) / 2
|
: (startEl[1] + endEl[3]) / 2
|
||||||
: a[1] > b[1]
|
: a[1] > b[1]
|
||||||
? a[1] - startUp
|
? a[1] - startUp
|
||||||
: common[1] - startUp,
|
: common[1] - startUp,
|
||||||
a[2] < b[0]
|
a[2] < b[0]
|
||||||
? a[1] > b[3] || a[3] < b[1]
|
? a[1] > b[3] || a[3] < b[1]
|
||||||
? Math.max((a[2] + b[0]) / 2, a[2] + startRight)
|
? Math.max((startEl[2] + endEl[0]) / 2, a[2] + startRight)
|
||||||
: (a[2] + b[0]) / 2
|
: (startEl[2] + endEl[0]) / 2
|
||||||
: a[2] < b[2]
|
: a[2] < b[2]
|
||||||
? a[2] + startRight
|
? a[2] + startRight
|
||||||
: common[2] + startRight,
|
: common[2] + startRight,
|
||||||
a[3] < b[1]
|
a[3] < b[1]
|
||||||
? a[0] > b[2] || a[2] < b[0]
|
? a[0] > b[2] || a[2] < b[0]
|
||||||
? Math.max((a[3] + b[1]) / 2, a[3] + startDown)
|
? Math.max((startEl[3] + endEl[1]) / 2, a[3] + startDown)
|
||||||
: (a[3] + b[1]) / 2
|
: (startEl[3] + endEl[1]) / 2
|
||||||
: a[3] < b[3]
|
: a[3] < b[3]
|
||||||
? a[3] + startDown
|
? a[3] + startDown
|
||||||
: common[3] + startDown,
|
: common[3] + startDown,
|
||||||
@@ -514,29 +544,29 @@ const generateDynamicAABBs = (
|
|||||||
const second = [
|
const second = [
|
||||||
b[0] > a[2]
|
b[0] > a[2]
|
||||||
? b[1] > a[3] || b[3] < a[1]
|
? b[1] > a[3] || b[3] < a[1]
|
||||||
? Math.min((b[0] + a[2]) / 2, b[0] - endLeft)
|
? Math.min((endEl[0] + startEl[2]) / 2, b[0] - endLeft)
|
||||||
: (b[0] + a[2]) / 2
|
: (endEl[0] + startEl[2]) / 2
|
||||||
: b[0] > a[0]
|
: b[0] > a[0]
|
||||||
? b[0] - endLeft
|
? b[0] - endLeft
|
||||||
: common[0] - endLeft,
|
: common[0] - endLeft,
|
||||||
b[1] > a[3]
|
b[1] > a[3]
|
||||||
? b[0] > a[2] || b[2] < a[0]
|
? b[0] > a[2] || b[2] < a[0]
|
||||||
? Math.min((b[1] + a[3]) / 2, b[1] - endUp)
|
? Math.min((endEl[1] + startEl[3]) / 2, b[1] - endUp)
|
||||||
: (b[1] + a[3]) / 2
|
: (endEl[1] + startEl[3]) / 2
|
||||||
: b[1] > a[1]
|
: b[1] > a[1]
|
||||||
? b[1] - endUp
|
? b[1] - endUp
|
||||||
: common[1] - endUp,
|
: common[1] - endUp,
|
||||||
b[2] < a[0]
|
b[2] < a[0]
|
||||||
? b[1] > a[3] || b[3] < a[1]
|
? b[1] > a[3] || b[3] < a[1]
|
||||||
? Math.max((b[2] + a[0]) / 2, b[2] + endRight)
|
? Math.max((endEl[2] + startEl[0]) / 2, b[2] + endRight)
|
||||||
: (b[2] + a[0]) / 2
|
: (endEl[2] + startEl[0]) / 2
|
||||||
: b[2] < a[2]
|
: b[2] < a[2]
|
||||||
? b[2] + endRight
|
? b[2] + endRight
|
||||||
: common[2] + endRight,
|
: common[2] + endRight,
|
||||||
b[3] < a[1]
|
b[3] < a[1]
|
||||||
? b[0] > a[2] || b[2] < a[0]
|
? b[0] > a[2] || b[2] < a[0]
|
||||||
? Math.max((b[3] + a[1]) / 2, b[3] + endDown)
|
? Math.max((endEl[3] + startEl[1]) / 2, b[3] + endDown)
|
||||||
: (b[3] + a[1]) / 2
|
: (endEl[3] + startEl[1]) / 2
|
||||||
: b[3] < a[3]
|
: b[3] < a[3]
|
||||||
? b[3] + endDown
|
? b[3] + endDown
|
||||||
: common[3] + endDown,
|
: common[3] + endDown,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { CLASSES, isSafari, POINTER_BUTTON, THEME } from "../constants";
|
import { CLASSES, isSafari, POINTER_BUTTON } from "../constants";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@@ -50,7 +50,6 @@ import {
|
|||||||
originalContainerCache,
|
originalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./containerCache";
|
} from "./containerCache";
|
||||||
import { applyDarkModeFilter } from "../colors";
|
|
||||||
|
|
||||||
const getTransform = (
|
const getTransform = (
|
||||||
width: number,
|
width: number,
|
||||||
@@ -274,15 +273,10 @@ export const textWysiwyg = ({
|
|||||||
textAlign,
|
textAlign,
|
||||||
verticalAlign,
|
verticalAlign,
|
||||||
color: updatedTextElement.strokeColor,
|
color: updatedTextElement.strokeColor,
|
||||||
// color:
|
|
||||||
// appState.theme === THEME.DARK
|
|
||||||
// ? applyDarkModeFilter(updatedTextElement.strokeColor)
|
|
||||||
// : updatedTextElement.strokeColor,
|
|
||||||
opacity: updatedTextElement.opacity / 100,
|
opacity: updatedTextElement.opacity / 100,
|
||||||
filter: "var(--theme-filter)",
|
filter: "var(--theme-filter)",
|
||||||
maxHeight: `${editorMaxHeight}px`,
|
maxHeight: `${editorMaxHeight}px`,
|
||||||
});
|
});
|
||||||
// console.log("...", updatedTextElement.strokeColor);
|
|
||||||
editable.scrollTop = 0;
|
editable.scrollTop = 0;
|
||||||
// For some reason updating font attribute doesn't set font family
|
// For some reason updating font attribute doesn't set font family
|
||||||
// hence updating font family explicitly for test environment
|
// hence updating font family explicitly for test environment
|
||||||
|
|||||||
@@ -320,9 +320,12 @@ export const getDefaultRoundnessTypeForElement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const isFixedPointBinding = (
|
export const isFixedPointBinding = (
|
||||||
binding: PointBinding,
|
binding: PointBinding | FixedPointBinding,
|
||||||
): binding is FixedPointBinding => {
|
): binding is FixedPointBinding => {
|
||||||
return binding.fixedPoint != null;
|
return (
|
||||||
|
Object.hasOwn(binding, "fixedPoint") &&
|
||||||
|
(binding as FixedPointBinding).fixedPoint != null
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Move this to @excalidraw/math
|
// TODO: Move this to @excalidraw/math
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ export type ExcalidrawElement =
|
|||||||
| ExcalidrawGenericElement
|
| ExcalidrawGenericElement
|
||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
| ExcalidrawLinearElement
|
| ExcalidrawLinearElement
|
||||||
|
| ExcalidrawArrowElement
|
||||||
| ExcalidrawFreeDrawElement
|
| ExcalidrawFreeDrawElement
|
||||||
| ExcalidrawImageElement
|
| ExcalidrawImageElement
|
||||||
| ExcalidrawFrameElement
|
| ExcalidrawFrameElement
|
||||||
@@ -268,15 +269,19 @@ export type PointBinding = {
|
|||||||
elementId: ExcalidrawBindableElement["id"];
|
elementId: ExcalidrawBindableElement["id"];
|
||||||
focus: number;
|
focus: number;
|
||||||
gap: number;
|
gap: number;
|
||||||
// Represents the fixed point binding information in form of a vertical and
|
|
||||||
// horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio
|
|
||||||
// gives the user selected fixed point by multiplying the bound element width
|
|
||||||
// with fixedPoint[0] and the bound element height with fixedPoint[1] to get the
|
|
||||||
// bound element-local point coordinate.
|
|
||||||
fixedPoint: FixedPoint | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FixedPointBinding = Merge<PointBinding, { fixedPoint: FixedPoint }>;
|
export type FixedPointBinding = Merge<
|
||||||
|
PointBinding,
|
||||||
|
{
|
||||||
|
// Represents the fixed point binding information in form of a vertical and
|
||||||
|
// horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio
|
||||||
|
// gives the user selected fixed point by multiplying the bound element width
|
||||||
|
// with fixedPoint[0] and the bound element height with fixedPoint[1] to get the
|
||||||
|
// bound element-local point coordinate.
|
||||||
|
fixedPoint: FixedPoint;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
export type Arrowhead =
|
export type Arrowhead =
|
||||||
| "arrow"
|
| "arrow"
|
||||||
|
|||||||
Vendored
-10
@@ -104,13 +104,3 @@ declare namespace jest {
|
|||||||
toBeNonNaNNumber(): void;
|
toBeNonNaNNumber(): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare namespace tinycolor {
|
|
||||||
interface Instance {
|
|
||||||
_r: number;
|
|
||||||
_g: number;
|
|
||||||
_b: number;
|
|
||||||
_a: number;
|
|
||||||
_ok: boolean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -230,7 +230,8 @@
|
|||||||
"resetLibrary": "This will clear your library. Are you sure?",
|
"resetLibrary": "This will clear your library. Are you sure?",
|
||||||
"removeItemsFromsLibrary": "Delete {{count}} item(s) from library?",
|
"removeItemsFromsLibrary": "Delete {{count}} item(s) from library?",
|
||||||
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.",
|
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.",
|
||||||
"collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!"
|
"collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!",
|
||||||
|
"saveYourContent": "Don't forget to save your content ({{shortcut}})!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Unsupported file type.",
|
"unsupportedFileType": "Unsupported file type.",
|
||||||
|
|||||||
@@ -63,7 +63,6 @@
|
|||||||
"@radix-ui/react-popover": "1.0.3",
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
"@radix-ui/react-tabs": "1.0.2",
|
"@radix-ui/react-tabs": "1.0.2",
|
||||||
"@tldraw/vec": "1.7.1",
|
"@tldraw/vec": "1.7.1",
|
||||||
"@types/tinycolor2": "1.4.6",
|
|
||||||
"browser-fs-access": "0.29.1",
|
"browser-fs-access": "0.29.1",
|
||||||
"canvas-roundrect-polyfill": "0.0.1",
|
"canvas-roundrect-polyfill": "0.0.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
@@ -85,7 +84,6 @@
|
|||||||
"pwacompat": "2.0.17",
|
"pwacompat": "2.0.17",
|
||||||
"roughjs": "4.6.4",
|
"roughjs": "4.6.4",
|
||||||
"sass": "1.51.0",
|
"sass": "1.51.0",
|
||||||
"tinycolor2": "1.6.0",
|
|
||||||
"tunnel-rat": "0.1.2"
|
"tunnel-rat": "0.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { StaticCanvasAppState, AppState } from "../types";
|
|||||||
import type { StaticCanvasRenderConfig } from "../scene/types";
|
import type { StaticCanvasRenderConfig } from "../scene/types";
|
||||||
|
|
||||||
import { THEME, THEME_FILTER } from "../constants";
|
import { THEME, THEME_FILTER } from "../constants";
|
||||||
import { applyDarkModeFilter } from "../colors";
|
|
||||||
|
|
||||||
export const fillCircle = (
|
export const fillCircle = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@@ -51,7 +50,7 @@ export const bootstrapCanvas = ({
|
|||||||
context.scale(scale, scale);
|
context.scale(scale, scale);
|
||||||
|
|
||||||
if (isExporting && theme === THEME.DARK) {
|
if (isExporting && theme === THEME.DARK) {
|
||||||
// context.filter = THEME_FILTER;
|
context.filter = THEME_FILTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paint background
|
// Paint background
|
||||||
@@ -65,10 +64,7 @@ export const bootstrapCanvas = ({
|
|||||||
context.clearRect(0, 0, normalizedWidth, normalizedHeight);
|
context.clearRect(0, 0, normalizedWidth, normalizedHeight);
|
||||||
}
|
}
|
||||||
context.save();
|
context.save();
|
||||||
context.fillStyle =
|
context.fillStyle = viewBackgroundColor;
|
||||||
theme === THEME.DARK
|
|
||||||
? applyDarkModeFilter(viewBackgroundColor)
|
|
||||||
: viewBackgroundColor;
|
|
||||||
context.fillRect(0, 0, normalizedWidth, normalizedHeight);
|
context.fillRect(0, 0, normalizedWidth, normalizedHeight);
|
||||||
context.restore();
|
context.restore();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ import {
|
|||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
@@ -807,7 +806,6 @@ const _renderInteractiveScene = ({
|
|||||||
// Elbow arrow elements cannot be selected when bound on either end
|
// Elbow arrow elements cannot be selected when bound on either end
|
||||||
(
|
(
|
||||||
isSingleLinearElementSelected &&
|
isSingleLinearElementSelected &&
|
||||||
isArrowElement(element) &&
|
|
||||||
isElbowArrow(element) &&
|
isElbowArrow(element) &&
|
||||||
(element.startBinding || element.endBinding)
|
(element.startBinding || element.endBinding)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ import { ShapeCache } from "../scene/ShapeCache";
|
|||||||
import { getVerticalOffset } from "../fonts";
|
import { getVerticalOffset } from "../fonts";
|
||||||
import { isRightAngleRads } from "../../math";
|
import { isRightAngleRads } from "../../math";
|
||||||
import { getCornerRadius } from "../shapes";
|
import { getCornerRadius } from "../shapes";
|
||||||
import { applyDarkModeFilter } from "../colors";
|
|
||||||
|
|
||||||
// using a stronger invert (100% vs our regular 93%) and saturate
|
// using a stronger invert (100% vs our regular 93%) and saturate
|
||||||
// as a temp hack to make images in dark theme look closer to original
|
// as a temp hack to make images in dark theme look closer to original
|
||||||
@@ -248,9 +247,9 @@ const generateElementCanvas = (
|
|||||||
const rc = rough.canvas(canvas);
|
const rc = rough.canvas(canvas);
|
||||||
|
|
||||||
// in dark theme, revert the image color filter
|
// in dark theme, revert the image color filter
|
||||||
// if (shouldResetImageFilter(element, renderConfig, appState)) {
|
if (shouldResetImageFilter(element, renderConfig, appState)) {
|
||||||
// context.filter = IMAGE_INVERT_FILTER;
|
context.filter = IMAGE_INVERT_FILTER;
|
||||||
// }
|
}
|
||||||
|
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||||
|
|
||||||
@@ -404,10 +403,7 @@ const drawElementOnCanvas = (
|
|||||||
case "freedraw": {
|
case "freedraw": {
|
||||||
// Draw directly to canvas
|
// Draw directly to canvas
|
||||||
context.save();
|
context.save();
|
||||||
context.fillStyle =
|
context.fillStyle = element.strokeColor;
|
||||||
appState.theme === THEME.DARK
|
|
||||||
? applyDarkModeFilter(element.strokeColor)
|
|
||||||
: element.strokeColor;
|
|
||||||
|
|
||||||
const path = getFreeDrawPath2D(element) as Path2D;
|
const path = getFreeDrawPath2D(element) as Path2D;
|
||||||
const fillShape = ShapeCache.get(element);
|
const fillShape = ShapeCache.get(element);
|
||||||
@@ -416,10 +412,7 @@ const drawElementOnCanvas = (
|
|||||||
rc.draw(fillShape);
|
rc.draw(fillShape);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.fillStyle =
|
context.fillStyle = element.strokeColor;
|
||||||
appState.theme === THEME.DARK
|
|
||||||
? applyDarkModeFilter(element.strokeColor)
|
|
||||||
: element.strokeColor;
|
|
||||||
context.fill(path);
|
context.fill(path);
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
@@ -465,10 +458,7 @@ const drawElementOnCanvas = (
|
|||||||
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
|
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
|
||||||
context.save();
|
context.save();
|
||||||
context.font = getFontString(element);
|
context.font = getFontString(element);
|
||||||
context.fillStyle =
|
context.fillStyle = element.strokeColor;
|
||||||
appState.theme === THEME.DARK
|
|
||||||
? applyDarkModeFilter(element.strokeColor)
|
|
||||||
: element.strokeColor;
|
|
||||||
context.textAlign = element.textAlign as CanvasTextAlign;
|
context.textAlign = element.textAlign as CanvasTextAlign;
|
||||||
|
|
||||||
// Canvas does not support multiline text by default
|
// Canvas does not support multiline text by default
|
||||||
@@ -709,10 +699,7 @@ export const renderElement = (
|
|||||||
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
||||||
|
|
||||||
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
|
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
|
||||||
context.strokeStyle =
|
context.strokeStyle = FRAME_STYLE.strokeColor;
|
||||||
appState.theme === THEME.DARK
|
|
||||||
? applyDarkModeFilter(element.strokeColor)
|
|
||||||
: FRAME_STYLE.strokeColor;
|
|
||||||
|
|
||||||
// TODO change later to only affect AI frames
|
// TODO change later to only affect AI frames
|
||||||
if (isMagicFrameElement(element)) {
|
if (isMagicFrameElement(element)) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
SVG_NS,
|
SVG_NS,
|
||||||
THEME,
|
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { normalizeLink, toValidURL } from "../data/url";
|
import { normalizeLink, toValidURL } from "../data/url";
|
||||||
import { getElementAbsoluteCoords } from "../element";
|
import { getElementAbsoluteCoords } from "../element";
|
||||||
@@ -38,7 +37,6 @@ import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
|
|||||||
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
|
||||||
import { getVerticalOffset } from "../fonts";
|
import { getVerticalOffset } from "../fonts";
|
||||||
import { getCornerRadius, isPathALoop } from "../shapes";
|
import { getCornerRadius, isPathALoop } from "../shapes";
|
||||||
import { applyDarkModeFilter } from "../colors";
|
|
||||||
|
|
||||||
const roughSVGDrawWithPrecision = (
|
const roughSVGDrawWithPrecision = (
|
||||||
rsvg: RoughSVG,
|
rsvg: RoughSVG,
|
||||||
@@ -141,7 +139,7 @@ const renderElementToSvg = (
|
|||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
case "ellipse": {
|
case "ellipse": {
|
||||||
const shape = ShapeCache.generateElementShape(element, renderConfig);
|
const shape = ShapeCache.generateElementShape(element, null);
|
||||||
const node = roughSVGDrawWithPrecision(
|
const node = roughSVGDrawWithPrecision(
|
||||||
rsvg,
|
rsvg,
|
||||||
shape,
|
shape,
|
||||||
@@ -391,12 +389,7 @@ const renderElementToSvg = (
|
|||||||
);
|
);
|
||||||
node.setAttribute("stroke", "none");
|
node.setAttribute("stroke", "none");
|
||||||
const path = svgRoot.ownerDocument!.createElementNS(SVG_NS, "path");
|
const path = svgRoot.ownerDocument!.createElementNS(SVG_NS, "path");
|
||||||
path.setAttribute(
|
path.setAttribute("fill", element.strokeColor);
|
||||||
"fill",
|
|
||||||
renderConfig.theme === THEME.DARK
|
|
||||||
? applyDarkModeFilter(element.strokeColor)
|
|
||||||
: element.strokeColor,
|
|
||||||
);
|
|
||||||
path.setAttribute("d", getFreeDrawSvgPath(element));
|
path.setAttribute("d", getFreeDrawSvgPath(element));
|
||||||
node.appendChild(path);
|
node.appendChild(path);
|
||||||
|
|
||||||
@@ -533,12 +526,7 @@ const renderElementToSvg = (
|
|||||||
rect.setAttribute("ry", FRAME_STYLE.radius.toString());
|
rect.setAttribute("ry", FRAME_STYLE.radius.toString());
|
||||||
|
|
||||||
rect.setAttribute("fill", "none");
|
rect.setAttribute("fill", "none");
|
||||||
rect.setAttribute(
|
rect.setAttribute("stroke", FRAME_STYLE.strokeColor);
|
||||||
"stroke",
|
|
||||||
renderConfig.theme === THEME.DARK
|
|
||||||
? applyDarkModeFilter(FRAME_STYLE.strokeColor)
|
|
||||||
: FRAME_STYLE.strokeColor,
|
|
||||||
);
|
|
||||||
rect.setAttribute("stroke-width", FRAME_STYLE.strokeWidth.toString());
|
rect.setAttribute("stroke-width", FRAME_STYLE.strokeWidth.toString());
|
||||||
|
|
||||||
addToRoot(rect, element);
|
addToRoot(rect, element);
|
||||||
@@ -589,12 +577,7 @@ const renderElementToSvg = (
|
|||||||
text.setAttribute("y", `${i * lineHeightPx + verticalOffset}`);
|
text.setAttribute("y", `${i * lineHeightPx + verticalOffset}`);
|
||||||
text.setAttribute("font-family", getFontFamilyString(element));
|
text.setAttribute("font-family", getFontFamilyString(element));
|
||||||
text.setAttribute("font-size", `${element.fontSize}px`);
|
text.setAttribute("font-size", `${element.fontSize}px`);
|
||||||
text.setAttribute(
|
text.setAttribute("fill", element.strokeColor);
|
||||||
"fill",
|
|
||||||
renderConfig.theme === THEME.DARK
|
|
||||||
? applyDarkModeFilter(element.strokeColor)
|
|
||||||
: element.strokeColor,
|
|
||||||
);
|
|
||||||
text.setAttribute("text-anchor", textAnchor);
|
text.setAttribute("text-anchor", textAnchor);
|
||||||
text.setAttribute("style", "white-space: pre;");
|
text.setAttribute("style", "white-space: pre;");
|
||||||
text.setAttribute("direction", direction);
|
text.setAttribute("direction", direction);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type {
|
|||||||
import { generateFreeDrawShape } from "../renderer/renderElement";
|
import { generateFreeDrawShape } from "../renderer/renderElement";
|
||||||
import { isTransparent, assertNever } from "../utils";
|
import { isTransparent, assertNever } from "../utils";
|
||||||
import { simplify } from "points-on-curve";
|
import { simplify } from "points-on-curve";
|
||||||
import { ROUGHNESS, THEME } from "../constants";
|
import { ROUGHNESS } from "../constants";
|
||||||
import {
|
import {
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isEmbeddableElement,
|
isEmbeddableElement,
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
isLinearElement,
|
isLinearElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { canChangeRoundness } from "./comparisons";
|
import { canChangeRoundness } from "./comparisons";
|
||||||
import type { AppState, EmbedsValidationStatus } from "../types";
|
import type { EmbedsValidationStatus } from "../types";
|
||||||
import {
|
import {
|
||||||
point,
|
point,
|
||||||
pointDistance,
|
pointDistance,
|
||||||
@@ -30,7 +30,6 @@ import {
|
|||||||
type LocalPoint,
|
type LocalPoint,
|
||||||
} from "../../math";
|
} from "../../math";
|
||||||
import { getCornerRadius, isPathALoop } from "../shapes";
|
import { getCornerRadius, isPathALoop } from "../shapes";
|
||||||
import { applyDarkModeFilter } from "../colors";
|
|
||||||
|
|
||||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||||
|
|
||||||
@@ -62,7 +61,6 @@ function adjustRoughness(element: ExcalidrawElement): number {
|
|||||||
export const generateRoughOptions = (
|
export const generateRoughOptions = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
continuousPath = false,
|
continuousPath = false,
|
||||||
isDarkMode: boolean = false,
|
|
||||||
): Options => {
|
): Options => {
|
||||||
const options: Options = {
|
const options: Options = {
|
||||||
seed: element.seed,
|
seed: element.seed,
|
||||||
@@ -87,9 +85,7 @@ export const generateRoughOptions = (
|
|||||||
fillWeight: element.strokeWidth / 2,
|
fillWeight: element.strokeWidth / 2,
|
||||||
hachureGap: element.strokeWidth * 4,
|
hachureGap: element.strokeWidth * 4,
|
||||||
roughness: adjustRoughness(element),
|
roughness: adjustRoughness(element),
|
||||||
stroke: isDarkMode
|
stroke: element.strokeColor,
|
||||||
? applyDarkModeFilter(element.strokeColor)
|
|
||||||
: element.strokeColor,
|
|
||||||
preserveVertices:
|
preserveVertices:
|
||||||
continuousPath || element.roughness < ROUGHNESS.cartoonist,
|
continuousPath || element.roughness < ROUGHNESS.cartoonist,
|
||||||
};
|
};
|
||||||
@@ -103,8 +99,6 @@ export const generateRoughOptions = (
|
|||||||
options.fillStyle = element.fillStyle;
|
options.fillStyle = element.fillStyle;
|
||||||
options.fill = isTransparent(element.backgroundColor)
|
options.fill = isTransparent(element.backgroundColor)
|
||||||
? undefined
|
? undefined
|
||||||
: isDarkMode
|
|
||||||
? applyDarkModeFilter(element.backgroundColor)
|
|
||||||
: element.backgroundColor;
|
: element.backgroundColor;
|
||||||
if (element.type === "ellipse") {
|
if (element.type === "ellipse") {
|
||||||
options.curveFitting = 1;
|
options.curveFitting = 1;
|
||||||
@@ -118,8 +112,6 @@ export const generateRoughOptions = (
|
|||||||
options.fill =
|
options.fill =
|
||||||
element.backgroundColor === "transparent"
|
element.backgroundColor === "transparent"
|
||||||
? undefined
|
? undefined
|
||||||
: isDarkMode
|
|
||||||
? applyDarkModeFilter(element.backgroundColor)
|
|
||||||
: element.backgroundColor;
|
: element.backgroundColor;
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
@@ -173,7 +165,6 @@ const getArrowheadShapes = (
|
|||||||
generator: RoughGenerator,
|
generator: RoughGenerator,
|
||||||
options: Options,
|
options: Options,
|
||||||
canvasBackgroundColor: string,
|
canvasBackgroundColor: string,
|
||||||
isDarkMode: boolean,
|
|
||||||
) => {
|
) => {
|
||||||
const arrowheadPoints = getArrowheadPoints(
|
const arrowheadPoints = getArrowheadPoints(
|
||||||
element,
|
element,
|
||||||
@@ -201,14 +192,10 @@ const getArrowheadShapes = (
|
|||||||
fill:
|
fill:
|
||||||
arrowhead === "circle_outline"
|
arrowhead === "circle_outline"
|
||||||
? canvasBackgroundColor
|
? canvasBackgroundColor
|
||||||
: isDarkMode
|
|
||||||
? applyDarkModeFilter(element.strokeColor)
|
|
||||||
: element.strokeColor,
|
: element.strokeColor,
|
||||||
|
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
stroke: isDarkMode
|
stroke: element.strokeColor,
|
||||||
? applyDarkModeFilter(element.strokeColor)
|
|
||||||
: element.strokeColor,
|
|
||||||
roughness: Math.min(0.5, options.roughness || 0),
|
roughness: Math.min(0.5, options.roughness || 0),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@@ -233,8 +220,6 @@ const getArrowheadShapes = (
|
|||||||
fill:
|
fill:
|
||||||
arrowhead === "triangle_outline"
|
arrowhead === "triangle_outline"
|
||||||
? canvasBackgroundColor
|
? canvasBackgroundColor
|
||||||
: isDarkMode
|
|
||||||
? applyDarkModeFilter(element.strokeColor)
|
|
||||||
: element.strokeColor,
|
: element.strokeColor,
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
roughness: Math.min(1, options.roughness || 0),
|
roughness: Math.min(1, options.roughness || 0),
|
||||||
@@ -263,8 +248,6 @@ const getArrowheadShapes = (
|
|||||||
fill:
|
fill:
|
||||||
arrowhead === "diamond_outline"
|
arrowhead === "diamond_outline"
|
||||||
? canvasBackgroundColor
|
? canvasBackgroundColor
|
||||||
: isDarkMode
|
|
||||||
? applyDarkModeFilter(element.strokeColor)
|
|
||||||
: element.strokeColor,
|
: element.strokeColor,
|
||||||
fillStyle: "solid",
|
fillStyle: "solid",
|
||||||
roughness: Math.min(1, options.roughness || 0),
|
roughness: Math.min(1, options.roughness || 0),
|
||||||
@@ -308,15 +291,12 @@ export const _generateElementShape = (
|
|||||||
isExporting,
|
isExporting,
|
||||||
canvasBackgroundColor,
|
canvasBackgroundColor,
|
||||||
embedsValidationStatus,
|
embedsValidationStatus,
|
||||||
theme,
|
|
||||||
}: {
|
}: {
|
||||||
isExporting: boolean;
|
isExporting: boolean;
|
||||||
canvasBackgroundColor: string;
|
canvasBackgroundColor: string;
|
||||||
embedsValidationStatus: EmbedsValidationStatus | null;
|
embedsValidationStatus: EmbedsValidationStatus | null;
|
||||||
theme: AppState["theme"];
|
|
||||||
},
|
},
|
||||||
): Drawable | Drawable[] | null => {
|
): Drawable | Drawable[] | null => {
|
||||||
const isDarkMode = theme === THEME.DARK;
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "iframe":
|
case "iframe":
|
||||||
@@ -342,7 +322,6 @@ export const _generateElementShape = (
|
|||||||
embedsValidationStatus,
|
embedsValidationStatus,
|
||||||
),
|
),
|
||||||
true,
|
true,
|
||||||
isDarkMode,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -358,7 +337,6 @@ export const _generateElementShape = (
|
|||||||
embedsValidationStatus,
|
embedsValidationStatus,
|
||||||
),
|
),
|
||||||
false,
|
false,
|
||||||
isDarkMode,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -396,7 +374,7 @@ export const _generateElementShape = (
|
|||||||
C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
|
C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
|
||||||
topY + horizontalRadius
|
topY + horizontalRadius
|
||||||
}`,
|
}`,
|
||||||
generateRoughOptions(element, true, isDarkMode),
|
generateRoughOptions(element, true),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
shape = generator.polygon(
|
shape = generator.polygon(
|
||||||
@@ -406,7 +384,7 @@ export const _generateElementShape = (
|
|||||||
[bottomX, bottomY],
|
[bottomX, bottomY],
|
||||||
[leftX, leftY],
|
[leftX, leftY],
|
||||||
],
|
],
|
||||||
generateRoughOptions(element, undefined, isDarkMode),
|
generateRoughOptions(element),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return shape;
|
return shape;
|
||||||
@@ -417,14 +395,14 @@ export const _generateElementShape = (
|
|||||||
element.height / 2,
|
element.height / 2,
|
||||||
element.width,
|
element.width,
|
||||||
element.height,
|
element.height,
|
||||||
generateRoughOptions(element, undefined, isDarkMode),
|
generateRoughOptions(element),
|
||||||
);
|
);
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
case "line":
|
case "line":
|
||||||
case "arrow": {
|
case "arrow": {
|
||||||
let shape: ElementShapes[typeof element.type];
|
let shape: ElementShapes[typeof element.type];
|
||||||
const options = generateRoughOptions(element, undefined, isDarkMode);
|
const options = generateRoughOptions(element);
|
||||||
|
|
||||||
// points array can be empty in the beginning, so it is important to add
|
// points array can be empty in the beginning, so it is important to add
|
||||||
// initial position to it
|
// initial position to it
|
||||||
@@ -436,7 +414,7 @@ export const _generateElementShape = (
|
|||||||
shape = [
|
shape = [
|
||||||
generator.path(
|
generator.path(
|
||||||
generateElbowArrowShape(points, 16),
|
generateElbowArrowShape(points, 16),
|
||||||
generateRoughOptions(element, true, isDarkMode),
|
generateRoughOptions(element, true),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
} else if (!element.roundness) {
|
} else if (!element.roundness) {
|
||||||
@@ -468,7 +446,6 @@ export const _generateElementShape = (
|
|||||||
generator,
|
generator,
|
||||||
options,
|
options,
|
||||||
canvasBackgroundColor,
|
canvasBackgroundColor,
|
||||||
isDarkMode,
|
|
||||||
);
|
);
|
||||||
shape.push(...shapes);
|
shape.push(...shapes);
|
||||||
}
|
}
|
||||||
@@ -486,7 +463,6 @@ export const _generateElementShape = (
|
|||||||
generator,
|
generator,
|
||||||
options,
|
options,
|
||||||
canvasBackgroundColor,
|
canvasBackgroundColor,
|
||||||
isDarkMode,
|
|
||||||
);
|
);
|
||||||
shape.push(...shapes);
|
shape.push(...shapes);
|
||||||
}
|
}
|
||||||
@@ -501,7 +477,7 @@ export const _generateElementShape = (
|
|||||||
// generate rough polygon to fill freedraw shape
|
// generate rough polygon to fill freedraw shape
|
||||||
const simplifiedPoints = simplify(element.points, 0.75);
|
const simplifiedPoints = simplify(element.points, 0.75);
|
||||||
shape = generator.curve(simplifiedPoints as [number, number][], {
|
shape = generator.curve(simplifiedPoints as [number, number][], {
|
||||||
...generateRoughOptions(element, undefined, isDarkMode),
|
...generateRoughOptions(element),
|
||||||
stroke: "none",
|
stroke: "none",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { _generateElementShape } from "./Shape";
|
|||||||
import type { ElementShape, ElementShapes } from "./types";
|
import type { ElementShape, ElementShapes } from "./types";
|
||||||
import { COLOR_PALETTE } from "../colors";
|
import { COLOR_PALETTE } from "../colors";
|
||||||
import type { AppState, EmbedsValidationStatus } from "../types";
|
import type { AppState, EmbedsValidationStatus } from "../types";
|
||||||
import { THEME } from "..";
|
|
||||||
|
|
||||||
export class ShapeCache {
|
export class ShapeCache {
|
||||||
private static rg = new RoughGenerator();
|
private static rg = new RoughGenerator();
|
||||||
@@ -53,7 +52,6 @@ export class ShapeCache {
|
|||||||
isExporting: boolean;
|
isExporting: boolean;
|
||||||
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
||||||
embedsValidationStatus: EmbedsValidationStatus;
|
embedsValidationStatus: EmbedsValidationStatus;
|
||||||
theme: AppState["theme"];
|
|
||||||
} | null,
|
} | null,
|
||||||
) => {
|
) => {
|
||||||
// when exporting, always regenerated to guarantee the latest shape
|
// when exporting, always regenerated to guarantee the latest shape
|
||||||
@@ -76,7 +74,6 @@ export class ShapeCache {
|
|||||||
isExporting: false,
|
isExporting: false,
|
||||||
canvasBackgroundColor: COLOR_PALETTE.white,
|
canvasBackgroundColor: COLOR_PALETTE.white,
|
||||||
embedsValidationStatus: null,
|
embedsValidationStatus: null,
|
||||||
theme: THEME.LIGHT,
|
|
||||||
},
|
},
|
||||||
) as T["type"] extends keyof ElementShapes
|
) as T["type"] extends keyof ElementShapes
|
||||||
? ElementShapes[T["type"]]
|
? ElementShapes[T["type"]]
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import { syncInvalidIndices } from "../fractionalIndex";
|
|||||||
import { renderStaticScene } from "../renderer/staticScene";
|
import { renderStaticScene } from "../renderer/staticScene";
|
||||||
import { Fonts } from "../fonts";
|
import { Fonts } from "../fonts";
|
||||||
import type { Font } from "../fonts/ExcalidrawFont";
|
import type { Font } from "../fonts/ExcalidrawFont";
|
||||||
import { applyDarkModeFilter } from "../colors";
|
|
||||||
|
|
||||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||||
|
|
||||||
@@ -186,6 +185,11 @@ export const exportToCanvas = async (
|
|||||||
exportingFrame ?? null,
|
exportingFrame ?? null,
|
||||||
appState.frameRendering ?? null,
|
appState.frameRendering ?? null,
|
||||||
);
|
);
|
||||||
|
// for canvas export, don't clip if exporting a specific frame as it would
|
||||||
|
// clip the corners of the content
|
||||||
|
if (exportingFrame) {
|
||||||
|
frameRendering.clip = false;
|
||||||
|
}
|
||||||
|
|
||||||
const elementsForRender = prepareElementsForRender({
|
const elementsForRender = prepareElementsForRender({
|
||||||
elements,
|
elements,
|
||||||
@@ -215,8 +219,6 @@ export const exportToCanvas = async (
|
|||||||
files,
|
files,
|
||||||
});
|
});
|
||||||
|
|
||||||
const theme = appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT;
|
|
||||||
|
|
||||||
renderStaticScene({
|
renderStaticScene({
|
||||||
canvas,
|
canvas,
|
||||||
rc: rough.canvas(canvas),
|
rc: rough.canvas(canvas),
|
||||||
@@ -236,7 +238,7 @@ export const exportToCanvas = async (
|
|||||||
scrollY: -minY + exportPadding,
|
scrollY: -minY + exportPadding,
|
||||||
zoom: defaultAppState.zoom,
|
zoom: defaultAppState.zoom,
|
||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
theme,
|
theme: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT,
|
||||||
},
|
},
|
||||||
renderConfig: {
|
renderConfig: {
|
||||||
canvasBackgroundColor: viewBackgroundColor,
|
canvasBackgroundColor: viewBackgroundColor,
|
||||||
@@ -247,7 +249,6 @@ export const exportToCanvas = async (
|
|||||||
embedsValidationStatus: new Map(),
|
embedsValidationStatus: new Map(),
|
||||||
elementsPendingErasure: new Set(),
|
elementsPendingErasure: new Set(),
|
||||||
pendingFlowchartNodes: null,
|
pendingFlowchartNodes: null,
|
||||||
theme,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -334,7 +335,7 @@ export const exportToSvg = async (
|
|||||||
svgRoot.setAttribute("width", `${width * exportScale}`);
|
svgRoot.setAttribute("width", `${width * exportScale}`);
|
||||||
svgRoot.setAttribute("height", `${height * exportScale}`);
|
svgRoot.setAttribute("height", `${height * exportScale}`);
|
||||||
if (exportWithDarkMode) {
|
if (exportWithDarkMode) {
|
||||||
// svgRoot.setAttribute("filter", THEME_FILTER);
|
svgRoot.setAttribute("filter", THEME_FILTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
const offsetX = -minX + exportPadding;
|
const offsetX = -minX + exportPadding;
|
||||||
@@ -355,6 +356,11 @@ export const exportToSvg = async (
|
|||||||
}) rotate(${frame.angle} ${cx} ${cy})"
|
}) rotate(${frame.angle} ${cx} ${cy})"
|
||||||
width="${frame.width}"
|
width="${frame.width}"
|
||||||
height="${frame.height}"
|
height="${frame.height}"
|
||||||
|
${
|
||||||
|
exportingFrame
|
||||||
|
? ""
|
||||||
|
: `rx=${FRAME_STYLE.radius} ry=${FRAME_STYLE.radius}`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
</rect>
|
</rect>
|
||||||
</clipPath>`;
|
</clipPath>`;
|
||||||
@@ -380,12 +386,7 @@ export const exportToSvg = async (
|
|||||||
rect.setAttribute("y", "0");
|
rect.setAttribute("y", "0");
|
||||||
rect.setAttribute("width", `${width}`);
|
rect.setAttribute("width", `${width}`);
|
||||||
rect.setAttribute("height", `${height}`);
|
rect.setAttribute("height", `${height}`);
|
||||||
rect.setAttribute(
|
rect.setAttribute("fill", viewBackgroundColor);
|
||||||
"fill",
|
|
||||||
appState.exportWithDarkMode
|
|
||||||
? applyDarkModeFilter(viewBackgroundColor)
|
|
||||||
: viewBackgroundColor,
|
|
||||||
);
|
|
||||||
svgRoot.appendChild(rect);
|
svgRoot.appendChild(rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,8 +394,6 @@ export const exportToSvg = async (
|
|||||||
|
|
||||||
const renderEmbeddables = opts?.renderEmbeddables ?? false;
|
const renderEmbeddables = opts?.renderEmbeddables ?? false;
|
||||||
|
|
||||||
const theme = appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT;
|
|
||||||
|
|
||||||
renderSceneToSvg(
|
renderSceneToSvg(
|
||||||
elementsForRender,
|
elementsForRender,
|
||||||
toBrandedType<RenderableElementsMap>(arrayToMap(elementsForRender)),
|
toBrandedType<RenderableElementsMap>(arrayToMap(elementsForRender)),
|
||||||
@@ -416,7 +415,6 @@ export const exportToSvg = async (
|
|||||||
.map((element) => [element.id, true]),
|
.map((element) => [element.id, true]),
|
||||||
)
|
)
|
||||||
: new Map(),
|
: new Map(),
|
||||||
theme,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export type StaticCanvasRenderConfig = {
|
|||||||
embedsValidationStatus: EmbedsValidationStatus;
|
embedsValidationStatus: EmbedsValidationStatus;
|
||||||
elementsPendingErasure: ElementsPendingErasure;
|
elementsPendingErasure: ElementsPendingErasure;
|
||||||
pendingFlowchartNodes: PendingExcalidrawElements | null;
|
pendingFlowchartNodes: PendingExcalidrawElements | null;
|
||||||
theme: AppState["theme"];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SVGRenderConfig = {
|
export type SVGRenderConfig = {
|
||||||
@@ -47,7 +46,6 @@ export type SVGRenderConfig = {
|
|||||||
frameRendering: AppState["frameRendering"];
|
frameRendering: AppState["frameRendering"];
|
||||||
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
||||||
embedsValidationStatus: EmbedsValidationStatus;
|
embedsValidationStatus: EmbedsValidationStatus;
|
||||||
theme: AppState["theme"];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InteractiveCanvasRenderConfig = {
|
export type InteractiveCanvasRenderConfig = {
|
||||||
|
|||||||
@@ -8430,6 +8430,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
"selectedLinearElement": LinearElementEditor {
|
"selectedLinearElement": LinearElementEditor {
|
||||||
|
"elbowed": false,
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"endBindingElement": "keep",
|
"endBindingElement": "keep",
|
||||||
"hoverPointIndex": -1,
|
"hoverPointIndex": -1,
|
||||||
@@ -8649,6 +8650,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
|||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
"selectedLinearElement": LinearElementEditor {
|
"selectedLinearElement": LinearElementEditor {
|
||||||
|
"elbowed": false,
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"endBindingElement": "keep",
|
"endBindingElement": "keep",
|
||||||
"hoverPointIndex": -1,
|
"hoverPointIndex": -1,
|
||||||
@@ -9058,6 +9060,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
"selectedLinearElement": LinearElementEditor {
|
"selectedLinearElement": LinearElementEditor {
|
||||||
|
"elbowed": false,
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"endBindingElement": "keep",
|
"endBindingElement": "keep",
|
||||||
"hoverPointIndex": -1,
|
"hoverPointIndex": -1,
|
||||||
@@ -9454,6 +9457,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
|||||||
"selectedElementsAreBeingDragged": false,
|
"selectedElementsAreBeingDragged": false,
|
||||||
"selectedGroupIds": {},
|
"selectedGroupIds": {},
|
||||||
"selectedLinearElement": LinearElementEditor {
|
"selectedLinearElement": LinearElementEditor {
|
||||||
|
"elbowed": false,
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
"endBindingElement": "keep",
|
"endBindingElement": "keep",
|
||||||
"hoverPointIndex": -1,
|
"hoverPointIndex": -1,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import type {
|
|||||||
ExcalidrawFrameElement,
|
ExcalidrawFrameElement,
|
||||||
ExcalidrawElementType,
|
ExcalidrawElementType,
|
||||||
ExcalidrawMagicFrameElement,
|
ExcalidrawMagicFrameElement,
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
|
ExcalidrawArrowElement,
|
||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
import { newElement, newTextElement, newLinearElement } from "../../element";
|
import { newElement, newTextElement, newLinearElement } from "../../element";
|
||||||
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
|
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
|
||||||
@@ -127,6 +129,10 @@ export class API {
|
|||||||
expect(API.getSelectedElements().length).toBe(0);
|
expect(API.getSelectedElements().length).toBe(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static getElement = <T extends ExcalidrawElement>(element: T): T => {
|
||||||
|
return h.app.scene.getElementsMapIncludingDeleted().get(element.id) as T || element;
|
||||||
|
}
|
||||||
|
|
||||||
static createElement = <
|
static createElement = <
|
||||||
T extends Exclude<ExcalidrawElementType, "selection"> = "rectangle",
|
T extends Exclude<ExcalidrawElementType, "selection"> = "rectangle",
|
||||||
>({
|
>({
|
||||||
@@ -179,10 +185,16 @@ export class API {
|
|||||||
scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never;
|
scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never;
|
||||||
status?: T extends "image" ? ExcalidrawImageElement["status"] : never;
|
status?: T extends "image" ? ExcalidrawImageElement["status"] : never;
|
||||||
startBinding?: T extends "arrow"
|
startBinding?: T extends "arrow"
|
||||||
? ExcalidrawLinearElement["startBinding"]
|
? ExcalidrawArrowElement["startBinding"] | ExcalidrawElbowArrowElement["startBinding"]
|
||||||
: never;
|
: never;
|
||||||
endBinding?: T extends "arrow"
|
endBinding?: T extends "arrow"
|
||||||
? ExcalidrawLinearElement["endBinding"]
|
? ExcalidrawArrowElement["endBinding"] | ExcalidrawElbowArrowElement["endBinding"]
|
||||||
|
: never;
|
||||||
|
startArrowhead?: T extends "arrow"
|
||||||
|
? ExcalidrawArrowElement["startArrowhead"] | ExcalidrawElbowArrowElement["startArrowhead"]
|
||||||
|
: never;
|
||||||
|
endArrowhead?: T extends "arrow"
|
||||||
|
? ExcalidrawArrowElement["endArrowhead"] | ExcalidrawElbowArrowElement["endArrowhead"]
|
||||||
: never;
|
: never;
|
||||||
elbowed?: boolean;
|
elbowed?: boolean;
|
||||||
}): T extends "arrow" | "line"
|
}): T extends "arrow" | "line"
|
||||||
@@ -340,6 +352,8 @@ export class API {
|
|||||||
if (element.type === "arrow") {
|
if (element.type === "arrow") {
|
||||||
element.startBinding = rest.startBinding ?? null;
|
element.startBinding = rest.startBinding ?? null;
|
||||||
element.endBinding = rest.endBinding ?? null;
|
element.endBinding = rest.endBinding ?? null;
|
||||||
|
element.startArrowhead = rest.startArrowhead ?? null;
|
||||||
|
element.endArrowhead = rest.endArrowhead ?? null;
|
||||||
}
|
}
|
||||||
if (id) {
|
if (id) {
|
||||||
element.id = id;
|
element.id = id;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import type {
|
|||||||
ExcalidrawGenericElement,
|
ExcalidrawGenericElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
|
FixedPointBinding,
|
||||||
FractionalIndex,
|
FractionalIndex,
|
||||||
SceneElementsMap,
|
SceneElementsMap,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
@@ -2049,13 +2050,13 @@ describe("history", () => {
|
|||||||
focus: -0.001587301587301948,
|
focus: -0.001587301587301948,
|
||||||
gap: 5,
|
gap: 5,
|
||||||
fixedPoint: [1.0318471337579618, 0.49920634920634904],
|
fixedPoint: [1.0318471337579618, 0.49920634920634904],
|
||||||
},
|
} as FixedPointBinding,
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: "u2JGnnmoJ0VATV4vCNJE5",
|
elementId: "u2JGnnmoJ0VATV4vCNJE5",
|
||||||
focus: -0.0016129032258049847,
|
focus: -0.0016129032258049847,
|
||||||
gap: 3.537079145500037,
|
gap: 3.537079145500037,
|
||||||
fixedPoint: [0.4991935483870975, -0.03875193720914723],
|
fixedPoint: [0.4991935483870975, -0.03875193720914723],
|
||||||
},
|
} as FixedPointBinding,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
storeAction: StoreAction.CAPTURE,
|
storeAction: StoreAction.CAPTURE,
|
||||||
@@ -4455,7 +4456,7 @@ describe("history", () => {
|
|||||||
elements: [
|
elements: [
|
||||||
h.elements[0],
|
h.elements[0],
|
||||||
newElementWith(h.elements[1], { boundElements: [] }),
|
newElementWith(h.elements[1], { boundElements: [] }),
|
||||||
newElementWith(h.elements[2] as ExcalidrawLinearElement, {
|
newElementWith(h.elements[2] as ExcalidrawElbowArrowElement, {
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: remoteContainer.id,
|
elementId: remoteContainer.id,
|
||||||
gap: 1,
|
gap: 1,
|
||||||
@@ -4655,7 +4656,7 @@ describe("history", () => {
|
|||||||
// Simulate remote update
|
// Simulate remote update
|
||||||
API.updateScene({
|
API.updateScene({
|
||||||
elements: [
|
elements: [
|
||||||
newElementWith(h.elements[0] as ExcalidrawLinearElement, {
|
newElementWith(h.elements[0] as ExcalidrawElbowArrowElement, {
|
||||||
startBinding: {
|
startBinding: {
|
||||||
elementId: rect1.id,
|
elementId: rect1.id,
|
||||||
gap: 1,
|
gap: 1,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { render } from "./test-utils";
|
|||||||
import { reseed } from "../random";
|
import { reseed } from "../random";
|
||||||
import { UI, Keyboard, Pointer } from "./helpers/ui";
|
import { UI, Keyboard, Pointer } from "./helpers/ui";
|
||||||
import type {
|
import type {
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
@@ -333,6 +334,62 @@ describe("arrow element", () => {
|
|||||||
expect(label.angle).toBeCloseTo(0);
|
expect(label.angle).toBeCloseTo(0);
|
||||||
expect(label.fontSize).toEqual(20);
|
expect(label.fontSize).toEqual(20);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("flips the fixed point binding on negative resize for single bindable", () => {
|
||||||
|
const rectangle = UI.createElement("rectangle", {
|
||||||
|
x: -100,
|
||||||
|
y: -75,
|
||||||
|
width: 95,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
UI.clickOnTestId("elbow-arrow");
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(-5, 0);
|
||||||
|
mouse.click();
|
||||||
|
mouse.moveTo(120, 200);
|
||||||
|
mouse.click();
|
||||||
|
|
||||||
|
const arrow = h.scene.getSelectedElements(
|
||||||
|
h.state,
|
||||||
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
|
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
|
||||||
|
UI.resize(rectangle, "se", [-200, -150]);
|
||||||
|
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flips the fixed point binding on negative resize for group selection", () => {
|
||||||
|
const rectangle = UI.createElement("rectangle", {
|
||||||
|
x: -100,
|
||||||
|
y: -75,
|
||||||
|
width: 95,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
UI.clickOnTestId("elbow-arrow");
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(-5, 0);
|
||||||
|
mouse.click();
|
||||||
|
mouse.moveTo(120, 200);
|
||||||
|
mouse.click();
|
||||||
|
|
||||||
|
const arrow = h.scene.getSelectedElements(
|
||||||
|
h.state,
|
||||||
|
)[0] as ExcalidrawElbowArrowElement;
|
||||||
|
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05);
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75);
|
||||||
|
|
||||||
|
UI.resize([rectangle, arrow], "nw", [300, 350]);
|
||||||
|
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.144, 2);
|
||||||
|
expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("text element", () => {
|
describe("text element", () => {
|
||||||
@@ -828,7 +885,6 @@ describe("multiple selection", () => {
|
|||||||
expect(leftBoundArrow.endBinding?.elementId).toBe(
|
expect(leftBoundArrow.endBinding?.elementId).toBe(
|
||||||
leftArrowBinding.elementId,
|
leftArrowBinding.elementId,
|
||||||
);
|
);
|
||||||
expect(leftBoundArrow.endBinding?.fixedPoint).toBeNull();
|
|
||||||
expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus);
|
expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus);
|
||||||
|
|
||||||
expect(rightBoundArrow.x).toBeCloseTo(210);
|
expect(rightBoundArrow.x).toBeCloseTo(210);
|
||||||
@@ -843,7 +899,6 @@ describe("multiple selection", () => {
|
|||||||
expect(rightBoundArrow.endBinding?.elementId).toBe(
|
expect(rightBoundArrow.endBinding?.elementId).toBe(
|
||||||
rightArrowBinding.elementId,
|
rightArrowBinding.elementId,
|
||||||
);
|
);
|
||||||
expect(rightBoundArrow.endBinding?.fixedPoint).toBeNull();
|
|
||||||
expect(rightBoundArrow.endBinding?.focus).toBe(rightArrowBinding.focus);
|
expect(rightBoundArrow.endBinding?.focus).toBe(rightArrowBinding.focus);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ export const debugDrawBoundingBox = (
|
|||||||
export const debugDrawBounds = (
|
export const debugDrawBounds = (
|
||||||
box: Bounds | Bounds[],
|
box: Bounds | Bounds[],
|
||||||
opts?: {
|
opts?: {
|
||||||
color: string;
|
color?: string;
|
||||||
permanent: boolean;
|
permanent?: boolean;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
(isBounds(box) ? [box] : box).forEach((bbox) =>
|
(isBounds(box) ? [box] : box).forEach((bbox) =>
|
||||||
@@ -136,7 +136,7 @@ export const debugDrawBounds = (
|
|||||||
],
|
],
|
||||||
{
|
{
|
||||||
color: opts?.color ?? "green",
|
color: opts?.color ?? "green",
|
||||||
permanent: opts?.permanent,
|
permanent: !!opts?.permanent,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3299,11 +3299,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/jest" "*"
|
"@types/jest" "*"
|
||||||
|
|
||||||
"@types/tinycolor2@1.4.6":
|
|
||||||
version "1.4.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.6.tgz#670cbc0caf4e58dd61d1e3a6f26386e473087f06"
|
|
||||||
integrity sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==
|
|
||||||
|
|
||||||
"@types/trusted-types@^2.0.2":
|
"@types/trusted-types@^2.0.2":
|
||||||
version "2.0.7"
|
version "2.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
||||||
@@ -9981,11 +9976,6 @@ tinybench@^2.8.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
|
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
|
||||||
integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
|
integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
|
||||||
|
|
||||||
tinycolor2@1.6.0:
|
|
||||||
version "1.6.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e"
|
|
||||||
integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==
|
|
||||||
|
|
||||||
tinypool@^1.0.0:
|
tinypool@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.1.tgz#c64233c4fac4304e109a64340178760116dbe1fe"
|
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.1.tgz#c64233c4fac4304e109a64340178760116dbe1fe"
|
||||||
|
|||||||
Reference in New Issue
Block a user