Compare commits

..

10 Commits

Author SHA1 Message Date
zsviczian 5c8adfa1a9 Update App.tsx 2024-09-28 13:05:40 +02:00
Subhadeep Sengupta a977dd1bf5 feat: Added reddit links as embeddable (#8099)
feat: #8063 Added reddit links as embeddable

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2024-09-28 11:49:18 +05:30
Aakansha Doshi 3fe1883f3f feat: prefer user defined coords and dimensions over calculated for for frame (#8517)
* feat: prefer user defined coords and dimensions over calculated for frame

* update changelog

* lint

* show the info only in dev mode and when children present
2024-09-24 21:09:15 +05:30
Marcel Mraz a80cb5896a feat: self-hosting existing google fonts (#8540) 2024-09-24 17:30:21 +02:00
David Luzar 6dfa18414a test: decrease min coverage thresholds (#8541) 2024-09-24 12:01:28 +00:00
David Luzar 8ca4cf3260 feat: flip arrowheads if only arrow(s) selected (#8525)
Co-authored-by: Mark Tolmacs <mark@lazycat.hu>
2024-09-19 15:46:36 +02:00
Márk Tolmács f3f0ab7c83 fix: Elbow arrow fixedpoint flipping now properly flips on inverted resize and flip action (#8324)
* Flipping action now properly mirrors selections with elbow arrows
* Flipping action now re-centers the selection to the original center to avoid "walking" selections on repeated flipping
2024-09-19 08:47:23 +02:00
David Luzar 44a1c8d857 fix: svg and png frame clipping cases (#8515) 2024-09-18 00:20:22 +02:00
Márk Tolmács e0a22edfbd fix: Re-route elbow arrows when pasted (#8448)
Re-route elbow arrows when pasted
2024-09-17 12:20:40 +02:00
Márk Tolmács c07f5a0c80 feat: Common elbow mid segments (#8440)
Common start or end segment length for elbow arrows regardless of arrowhead is present
2024-09-17 10:11:07 +02:00
57 changed files with 776 additions and 627 deletions
+6 -1
View File
@@ -649,7 +649,12 @@ const ExcalidrawWrapper = () => {
// Render the debug scene if the debug canvas is available
if (debugCanvasRef.current && excalidrawAPI) {
debugRenderer(debugCanvasRef.current, appState, window.devicePixelRatio);
debugRenderer(
debugCanvasRef.current,
appState,
window.devicePixelRatio,
() => forceRefresh((prev) => !prev),
);
}
};
+12 -2
View File
@@ -68,12 +68,17 @@ const _debugRenderer = (
canvas: HTMLCanvasElement,
appState: AppState,
scale: number,
refresh: () => void,
) => {
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
canvas,
scale,
);
if (appState.height !== canvas.height || appState.width !== canvas.width) {
refresh();
}
const context = bootstrapCanvas({
canvas,
scale,
@@ -138,8 +143,13 @@ export const saveDebugState = (debug: { enabled: boolean }) => {
};
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 },
);
-9
View File
@@ -130,15 +130,6 @@
</script>
<% } %>
<!-- For Nunito only preload the latin range, which should be good enough for now -->
<link
rel="preload"
href="https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<!-- Register Assistant as the UI font, before the scene inits -->
<link
rel="stylesheet"
+2
View File
@@ -48,6 +48,8 @@ export default defineConfig({
},
},
sourcemap: true,
// don't auto-inline small assets (i.e. fonts hosted on CDN)
assetsInlineLimit: 0,
},
plugins: [
woff2BrowserPlugin(),
+2
View File
@@ -15,6 +15,8 @@ Please add the latest change on the top under the correct section.
### Features
- Prefer user defined coordinates and dimensions when creating a frame using [`convertToExcalidrawElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/excalidraw-element-skeleton#converttoexcalidrawelements) [#8517](https://github.com/excalidraw/excalidraw/pull/8517)
- `props.initialData` can now be a function that returns `ExcalidrawInitialDataState` or `Promise<ExcalidrawInitialDataState>`. [#8107](https://github.com/excalidraw/excalidraw/pull/8135)
- Added support for multiplayer undo/redo, by calculating invertible increments and storing them inside the local-only undo/redo stacks. [#7348](https://github.com/excalidraw/excalidraw/pull/7348)
@@ -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);
});
});
+69 -2
View File
@@ -2,6 +2,8 @@ import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element";
import type {
ExcalidrawArrowElement,
ExcalidrawElbowArrowElement,
ExcalidrawElement,
NonDeleted,
NonDeletedSceneElementsMap,
@@ -18,7 +20,13 @@ import {
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { flipHorizontal, flipVertical } from "../components/icons";
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({
name: "flipHorizontal",
@@ -109,7 +117,23 @@ const flipElements = (
flipDirection: "horizontal" | "vertical",
app: AppClassProperties,
): 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(
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;
};
@@ -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;
-85
View File
@@ -1,90 +1,5 @@
import oc from "open-color";
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
const pick = <R extends Record<string, any>, K extends readonly (keyof R)[]>(
+62 -16
View File
@@ -185,6 +185,7 @@ import type {
MagicGenerationData,
ExcalidrawNonSelectionElement,
ExcalidrawArrowElement,
NonDeletedSceneElementsMap,
} from "../element/types";
import { getCenter, getDistance } from "../gesture";
import {
@@ -287,6 +288,7 @@ import {
getDateTime,
isShallowEqual,
arrayToMap,
toBrandedType,
} from "../utils";
import {
createSrcDoc,
@@ -435,7 +437,7 @@ import { actionTextAutoResize } from "../actions/actionTextAutoResize";
import { getVisibleSceneBounds } from "../element/bounds";
import { isMaybeMermaidDefinition } from "../mermaid";
import NewElementCanvas from "./canvases/NewElementCanvas";
import { mutateElbowArrow } from "../element/routing";
import { mutateElbowArrow, updateElbowArrow } from "../element/routing";
import {
FlowChartCreator,
FlowChartNavigator,
@@ -1700,7 +1702,6 @@ class App extends React.Component<AppProps, AppState> {
elementsPendingErasure: this.elementsPendingErasure,
pendingFlowchartNodes:
this.flowChartCreator.pendingNodes,
theme: this.state.theme,
}}
/>
{this.state.newElement && (
@@ -1721,7 +1722,6 @@ class App extends React.Component<AppProps, AppState> {
elementsPendingErasure:
this.elementsPendingErasure,
pendingFlowchartNodes: null,
theme: this.state.theme,
}}
/>
)}
@@ -2697,11 +2697,6 @@ class App extends React.Component<AppProps, AppState> {
activeTool: updateActiveTool(this.state, { type: "selection" }),
});
}
if (prevState.theme !== this.state.theme) {
this.scene
.getElementsIncludingDeleted()
.forEach((element) => ShapeCache.delete(element));
}
if (
this.state.activeTool.type === "eraser" &&
prevState.theme !== this.state.theme
@@ -3116,7 +3111,45 @@ class App extends React.Component<AppProps, AppState> {
retainSeed?: 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 elementsCenterX = distance(minX, maxX) / 2;
@@ -3824,40 +3857,53 @@ class App extends React.Component<AppProps, AppState> {
public getEditorUIOffsets = (): Offsets => {
const toolbarBottom =
this.excalidrawContainerRef?.current
(this.excalidrawContainerRef?.current
?.querySelector(".App-toolbar")
?.getBoundingClientRect()?.bottom ?? 0;
?.getBoundingClientRect()?.bottom ?? 0) - this.state.offsetTop;
const sidebarRect = this.excalidrawContainerRef?.current
?.querySelector(".sidebar")
?.getBoundingClientRect();
const propertiesPanelRect = this.excalidrawContainerRef?.current
?.querySelector(".App-menu__left")
?.getBoundingClientRect();
const PADDING = 16;
const adjustRectValueForOffset = (
value: number | undefined,
fallback: number,
) => (value ?? fallback + this.state.offsetLeft) - this.state.offsetLeft;
return getLanguage().rtl
? {
top: toolbarBottom + PADDING,
right:
Math.max(
this.state.width -
(propertiesPanelRect?.left ?? this.state.width),
adjustRectValueForOffset(
propertiesPanelRect?.left,
this.state.width,
),
0,
) + PADDING,
bottom: PADDING,
left: Math.max(sidebarRect?.right ?? 0, 0) + PADDING,
left:
Math.max(adjustRectValueForOffset(sidebarRect?.right, 0), 0) +
PADDING,
}
: {
top: toolbarBottom + PADDING,
right: Math.max(
this.state.width -
(sidebarRect?.left ?? this.state.width) +
adjustRectValueForOffset(sidebarRect?.left, this.state.width) +
PADDING,
0,
),
bottom: PADDING,
left: Math.max(propertiesPanelRect?.right ?? 0, 0) + PADDING,
left:
Math.max(
adjustRectValueForOffset(propertiesPanelRect?.right, 0),
0,
) + PADDING,
};
};
+1 -1
View File
@@ -124,7 +124,7 @@ body.excalidraw-cursor-resize * {
// recommends surface color of #121212, 93% yields #111111 for #FFF
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%)"; // prod
// $theme-filter: "hue-rotate(180deg)"; // prod
// $theme-filter: "hue-rotate(180deg) invert(93%)";
$theme-filter: "invert(93%) hue-rotate(180deg)";
$right-sidebar-width: "302px";
:export {
@@ -6,11 +6,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"backgroundColor": "#d8f5a2",
"boundElements": [
{
"id": "id45",
"id": "id47",
"type": "arrow",
},
{
"id": "id46",
"id": "id48",
"type": "arrow",
},
],
@@ -47,7 +47,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id46",
"id": "id48",
"type": "arrow",
},
],
@@ -118,7 +118,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": {
"elementId": "id47",
"elementId": "id49",
"fixedPoint": null,
"focus": -0.08139534883720931,
"gap": 1,
@@ -200,7 +200,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id45",
"id": "id47",
"type": "arrow",
},
],
@@ -238,7 +238,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id48",
"id": "id50",
"type": "arrow",
},
],
@@ -284,7 +284,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id48",
"id": "id50",
"type": "arrow",
},
],
@@ -329,7 +329,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id49",
"id": "id51",
"type": "text",
},
],
@@ -392,7 +392,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id48",
"containerId": "id50",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 5,
@@ -433,7 +433,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id38",
"id": "id40",
"type": "text",
},
],
@@ -441,7 +441,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id40",
"elementId": "id42",
"fixedPoint": null,
"focus": 0,
"gap": 1,
@@ -472,7 +472,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": {
"elementId": "id39",
"elementId": "id41",
"fixedPoint": null,
"focus": 0,
"gap": 1,
@@ -496,7 +496,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id37",
"containerId": "id39",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 5,
@@ -537,7 +537,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id37",
"id": "id39",
"type": "arrow",
},
],
@@ -574,7 +574,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id37",
"id": "id39",
"type": "arrow",
},
],
@@ -611,7 +611,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id42",
"id": "id44",
"type": "text",
},
],
@@ -619,7 +619,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"elbowed": false,
"endArrowhead": "arrow",
"endBinding": {
"elementId": "id44",
"elementId": "id46",
"fixedPoint": null,
"focus": 0,
"gap": 1,
@@ -650,7 +650,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"seed": Any<Number>,
"startArrowhead": null,
"startBinding": {
"elementId": "id43",
"elementId": "id45",
"fixedPoint": null,
"focus": 0,
"gap": 1,
@@ -674,7 +674,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id41",
"containerId": "id43",
"customData": undefined,
"fillStyle": "solid",
"fontFamily": 5,
@@ -716,7 +716,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id41",
"id": "id43",
"type": "arrow",
},
],
@@ -762,7 +762,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id41",
"id": "id43",
"type": "arrow",
},
],
@@ -1303,7 +1303,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id54",
"id": "id56",
"type": "text",
},
{
@@ -1346,7 +1346,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id55",
"id": "id57",
"type": "text",
},
],
@@ -1385,7 +1385,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id56",
"id": "id58",
"type": "text",
},
{
@@ -1428,7 +1428,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id57",
"id": "id59",
"type": "text",
},
{
@@ -1475,7 +1475,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id58",
"id": "id60",
"type": "text",
},
],
@@ -1540,7 +1540,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
"backgroundColor": "transparent",
"boundElements": [
{
"id": "id59",
"id": "id61",
"type": "text",
},
],
+9 -5
View File
@@ -5,6 +5,7 @@ import type {
ExcalidrawLinearElement,
ExcalidrawSelectionElement,
ExcalidrawTextElement,
FixedPointBinding,
FontFamilyValues,
OrderedExcalidrawElement,
PointBinding,
@@ -21,6 +22,7 @@ import {
import {
isArrowElement,
isElbowArrow,
isFixedPointBinding,
isLinearElement,
isTextElement,
isUsingAdaptiveRadius,
@@ -101,8 +103,8 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
const repairBinding = (
element: ExcalidrawLinearElement,
binding: PointBinding | null,
): PointBinding | null => {
binding: PointBinding | FixedPointBinding | null,
): PointBinding | FixedPointBinding | null => {
if (!binding) {
return null;
}
@@ -110,9 +112,11 @@ const repairBinding = (
return {
...binding,
focus: binding.focus || 0,
fixedPoint: isElbowArrow(element)
? normalizeFixedPoint(binding.fixedPoint ?? [0, 0])
: null,
...(isElbowArrow(element) && isFixedPointBinding(binding)
? {
fixedPoint: normalizeFixedPoint(binding.fixedPoint ?? [0, 0]),
}
: {}),
};
};
+47 -42
View File
@@ -309,28 +309,32 @@ describe("Test Transform", () => {
});
describe("Test Frames", () => {
const elements: ExcalidrawElementSkeleton[] = [
{
type: "rectangle",
x: 10,
y: 10,
strokeWidth: 2,
id: "1",
},
{
type: "diamond",
x: 120,
y: 20,
backgroundColor: "#fff3bf",
strokeWidth: 2,
label: {
text: "HELLO EXCALIDRAW",
strokeColor: "#099268",
fontSize: 30,
},
id: "2",
},
];
it("should transform frames and update frame ids when regenerated", () => {
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
{
type: "rectangle",
x: 10,
y: 10,
strokeWidth: 2,
id: "1",
},
{
type: "diamond",
x: 120,
y: 20,
backgroundColor: "#fff3bf",
strokeWidth: 2,
label: {
text: "HELLO EXCALIDRAW",
strokeColor: "#099268",
fontSize: 30,
},
id: "2",
},
...elements,
{
type: "frame",
children: ["1", "2"],
@@ -352,28 +356,9 @@ describe("Test Transform", () => {
});
});
it("should consider max of calculated and frame dimensions when provided", () => {
it("should consider user defined frame dimensions over calculated when provided", () => {
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
{
type: "rectangle",
x: 10,
y: 10,
strokeWidth: 2,
id: "1",
},
{
type: "diamond",
x: 120,
y: 20,
backgroundColor: "#fff3bf",
strokeWidth: 2,
label: {
text: "HELLO EXCALIDRAW",
strokeColor: "#099268",
fontSize: 30,
},
id: "2",
},
...elements,
{
type: "frame",
children: ["1", "2"],
@@ -388,7 +373,27 @@ describe("Test Transform", () => {
);
const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
expect(frame.width).toBe(800);
expect(frame.height).toBe(126);
expect(frame.height).toBe(100);
});
it("should consider user defined frame coordinates calculated when provided", () => {
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
...elements,
{
type: "frame",
children: ["1", "2"],
name: "My frame",
x: 100,
y: 300,
},
];
const excalidrawElements = convertToExcalidrawElements(
elementsSkeleton,
opts,
);
const frame = excalidrawElements.find((ele) => ele.type === "frame")!;
expect(frame.x).toBe(100);
expect(frame.y).toBe(300);
});
});
+22 -5
View File
@@ -46,6 +46,7 @@ import {
assertNever,
cloneJSON,
getFontString,
isDevEnv,
toBrandedType,
} from "../utils";
import { getSizeFromPoints } from "../points";
@@ -717,7 +718,7 @@ export const convertToExcalidrawElements = (
}
// Once all the excalidraw elements are created, we can add frames since we
// need to calculate coordinates and dimensions of frame which is possibe after all
// need to calculate coordinates and dimensions of frame which is possible after all
// frame children are processed.
for (const [id, element] of elementsWithIds) {
if (element.type !== "frame" && element.type !== "magicframe") {
@@ -764,10 +765,26 @@ export const convertToExcalidrawElements = (
maxX = maxX + PADDING;
maxY = maxY + PADDING;
// Take the max of calculated and provided frame dimensions, whichever is higher
const width = Math.max(frame?.width, maxX - minX);
const height = Math.max(frame?.height, maxY - minY);
Object.assign(frame, { x: minX, y: minY, width, height });
const frameX = frame?.x || minX;
const frameY = frame?.y || minY;
const frameWidth = frame?.width || maxX - minX;
const frameHeight = frame?.height || maxY - minY;
Object.assign(frame, {
x: frameX,
y: frameY,
width: frameWidth,
height: frameHeight,
});
if (
isDevEnv() &&
element.children.length &&
(frame?.x || frame?.y || frame?.width || frame?.height)
) {
console.info(
"User provided frame attributes are being considered, if you find this inaccurate, please remove any of the attributes - x, y, width and height so frame coordinates and dimensions are calculated automatically",
);
}
}
return elementStore.getElements();
+3 -2
View File
@@ -39,6 +39,7 @@ import {
isBindingElement,
isBoundToContainer,
isElbowArrow,
isFixedPointBinding,
isFrameLikeElement,
isLinearElement,
isRectangularElement,
@@ -797,7 +798,7 @@ export const bindPointToSnapToElementOutline = (
isVertical
? Math.abs(p[1] - i[1]) < 0.1
: Math.abs(p[0] - i[0]) < 0.1,
)[0] ?? point;
)[0] ?? p;
}
return p;
@@ -1013,7 +1014,7 @@ const updateBoundPoint = (
const direction = startOrEnd === "startBinding" ? -1 : 1;
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
if (isElbowArrow(linearElement)) {
if (isElbowArrow(linearElement) && isFixedPointBinding(binding)) {
const fixedPoint =
normalizeFixedPoint(binding.fixedPoint) ??
calculateFixedPointForElbowArrowBinding(
+1 -8
View File
@@ -35,7 +35,6 @@ export const dragSelectedElements = (
) => {
if (
_selectedElements.length === 1 &&
isArrowElement(_selectedElements[0]) &&
isElbowArrow(_selectedElements[0]) &&
(_selectedElements[0].startBinding || _selectedElements[0].endBinding)
) {
@@ -43,13 +42,7 @@ export const dragSelectedElements = (
}
const selectedElements = _selectedElements.filter(
(el) =>
!(
isArrowElement(el) &&
isElbowArrow(el) &&
el.startBinding &&
el.endBinding
),
(el) => !(isElbowArrow(el) && el.startBinding && el.endBinding),
);
// we do not want a frame and its elements to be selected at the same time
+31
View File
@@ -45,6 +45,12 @@ const RE_GENERIC_EMBED =
const RE_GIPHY =
/giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/;
const RE_REDDIT =
/^(?:http(?:s)?:\/\/)?(?:www\.)?reddit\.com\/r\/([a-zA-Z0-9_]+)\/comments\/([a-zA-Z0-9_]+)\/([a-zA-Z0-9_]+)\/?(?:\?[^#\s]*)?(?:#[^\s]*)?$/;
const RE_REDDIT_EMBED =
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:www\.)?reddit\.com\/[^"']*)/i;
const ALLOWED_DOMAINS = new Set([
"youtube.com",
"youtu.be",
@@ -59,6 +65,7 @@ const ALLOWED_DOMAINS = new Set([
"stackblitz.com",
"val.town",
"giphy.com",
"reddit.com",
]);
const ALLOW_SAME_ORIGIN = new Set([
@@ -71,6 +78,7 @@ const ALLOW_SAME_ORIGIN = new Set([
"x.com",
"*.simplepdf.eu",
"stackblitz.com",
"reddit.com",
]);
export const createSrcDoc = (body: string) => {
@@ -218,6 +226,24 @@ export const getEmbedLink = (
return ret;
}
if (RE_REDDIT.test(link)) {
const [, page, postId, title] = link.match(RE_REDDIT)!;
const safeURL = sanitizeHTMLAttribute(
`https://reddit.com/r/${page}/comments/${postId}/${title}`,
);
const ret: IframeDataWithSandbox = {
type: "document",
srcdoc: (theme: string) =>
createSrcDoc(
`<blockquote class="reddit-embed-bq" data-embed-theme="${theme}"><a href="${safeURL}"></a><br></blockquote><script async="" src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>`,
),
intrinsicSize: { w: 480, h: 480 },
sandbox: { allowSameOrigin },
};
embeddedLinkCache.set(originalLink, ret);
return ret;
}
if (RE_GH_GIST.test(link)) {
const [, user, gistId] = link.match(RE_GH_GIST)!;
const safeURL = sanitizeHTMLAttribute(
@@ -361,6 +387,11 @@ export const maybeParseEmbedSrc = (str: string): string => {
return twitterMatch[1];
}
const redditMatch = str.match(RE_REDDIT_EMBED);
if (redditMatch && redditMatch.length === 2) {
return redditMatch[1];
}
const gistMatch = str.match(RE_GH_GIST_EMBED);
if (gistMatch && gistMatch.length === 2) {
return gistMatch[1];
@@ -102,6 +102,7 @@ export class LinearElementEditor {
public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
public readonly hoverPointIndex: number;
public readonly segmentMidPointHoveredCoords: GlobalPoint | null;
public readonly elbowed: boolean;
constructor(element: NonDeleted<ExcalidrawLinearElement>) {
this.elementId = element.id as string & {
@@ -131,6 +132,7 @@ export class LinearElementEditor {
};
this.hoverPointIndex = -1;
this.segmentMidPointHoveredCoords = null;
this.elbowed = isElbowArrow(element) && element.elbowed;
}
// ---------------------------------------------------------------------------
@@ -1477,7 +1479,9 @@ export class LinearElementEditor {
nextPoints,
vector(offsetX, offsetY),
bindings,
options,
{
isDragging: options?.isDragging,
},
);
} else {
const nextCoords = getElementPointsCoords(element, nextPoints);
+4 -14
View File
@@ -9,6 +9,7 @@ import type {
ExcalidrawTextElementWithContainer,
ExcalidrawImageElement,
ElementsMap,
ExcalidrawArrowElement,
NonDeletedSceneElementsMap,
SceneElementsMap,
} from "./types";
@@ -909,6 +910,8 @@ export const resizeMultipleElements = (
fontSize?: ExcalidrawTextElement["fontSize"];
scale?: ExcalidrawImageElement["scale"];
boundTextFontSize?: ExcalidrawTextElement["fontSize"];
startBinding?: ExcalidrawArrowElement["startBinding"];
endBinding?: ExcalidrawArrowElement["endBinding"];
};
}[] = [];
@@ -993,19 +996,6 @@ export const resizeMultipleElements = (
mutateElement(element, update, false);
if (isArrowElement(element) && isElbowArrow(element)) {
mutateElbowArrow(
element,
elementsMap,
element.points,
undefined,
undefined,
{
informMutation: false,
},
);
}
updateBoundElements(element, elementsMap, {
simultaneouslyUpdated: elementsToUpdate,
oldSize: { width: oldWidth, height: oldHeight },
@@ -1059,7 +1049,7 @@ const rotateMultipleElements = (
(centerAngle + origAngle - element.angle) as Radians,
);
if (isArrowElement(element) && isElbowArrow(element)) {
if (isElbowArrow(element)) {
const points = getArrowLocalFixedPoints(element, elementsMap);
mutateElbowArrow(element, elementsMap, points);
} else {
+13 -12
View File
@@ -94,7 +94,16 @@ describe("elbow arrow routing", () => {
describe("elbow arrow ui", () => {
beforeEach(async () => {
localStorage.clear();
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 () => {
@@ -130,8 +139,8 @@ describe("elbow arrow ui", () => {
expect(arrow.elbowed).toBe(true);
expect(arrow.points).toEqual([
[0, 0],
[35, 0],
[35, 200],
[45, 0],
[45, 200],
[90, 200],
]);
});
@@ -163,14 +172,6 @@ describe("elbow arrow ui", () => {
h.state,
)[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);
const inputAngle = UI.queryStatsProperty("A")?.querySelector(
@@ -182,8 +183,8 @@ describe("elbow arrow ui", () => {
[0, 0],
[35, 0],
[35, 90],
[25, 90],
[25, 165],
[35, 90], // Note that coordinates are rounded above!
[35, 165],
[103, 165],
]);
});
+62 -32
View File
@@ -36,11 +36,11 @@ import {
HEADING_UP,
vectorToHeading,
} from "./heading";
import type { ElementUpdate } from "./mutateElement";
import { mutateElement } from "./mutateElement";
import { isBindableElement, isRectanguloidElement } from "./typeChecks";
import type {
ExcalidrawElbowArrowElement,
FixedPointBinding,
NonDeletedSceneElementsMap,
SceneElementsMap,
} from "./types";
@@ -72,16 +72,48 @@ export const mutateElbowArrow = (
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
nextPoints: readonly LocalPoint[],
offset?: Vector,
otherUpdates?: {
startBinding?: FixedPointBinding | null;
endBinding?: FixedPointBinding | null;
otherUpdates?: Omit<
ElementUpdate<ExcalidrawElbowArrowElement>,
"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?: {
isDragging?: boolean;
disableBinding?: boolean;
informMutation?: boolean;
},
) => {
): ElementUpdate<ExcalidrawElbowArrowElement> | null => {
const origStartGlobalPoint: GlobalPoint = pointTranslate(
pointTranslate<LocalPoint, GlobalPoint>(
nextPoints[0],
@@ -235,6 +267,8 @@ export const mutateElbowArrow = (
BASE_PADDING,
),
boundsOverlap,
hoveredStartElement && aabbForElement(hoveredStartElement),
hoveredEndElement && aabbForElement(hoveredEndElement),
);
const startDonglePosition = getDonglePosition(
dynamicAABBs[0],
@@ -295,18 +329,10 @@ export const mutateElbowArrow = (
startDongle && points.unshift(startGlobalPoint);
endDongle && points.push(endGlobalPoint);
mutateElement(
arrow,
{
...otherUpdates,
...normalizedArrowElementUpdate(simplifyElbowArrowPoints(points), 0, 0),
angle: 0 as Radians,
},
options?.informMutation,
);
} else {
console.error("Elbow arrow cannot find a route");
return normalizedArrowElementUpdate(simplifyElbowArrowPoints(points), 0, 0);
}
return null;
};
const offsetFromHeading = (
@@ -475,7 +501,11 @@ const generateDynamicAABBs = (
startDifference?: [number, number, number, number],
endDifference?: [number, number, number, number],
disableSideHack?: boolean,
startElementBounds?: Bounds | null,
endElementBounds?: Bounds | null,
): Bounds[] => {
const startEl = startElementBounds ?? a;
const endEl = endElementBounds ?? b;
const [startUp, startRight, startDown, startLeft] = startDifference ?? [
0, 0, 0, 0,
];
@@ -484,29 +514,29 @@ const generateDynamicAABBs = (
const first = [
a[0] > b[2]
? a[1] > b[3] || a[3] < b[1]
? Math.min((a[0] + b[2]) / 2, a[0] - startLeft)
: (a[0] + b[2]) / 2
? Math.min((startEl[0] + endEl[2]) / 2, a[0] - startLeft)
: (startEl[0] + endEl[2]) / 2
: a[0] > b[0]
? a[0] - startLeft
: common[0] - startLeft,
a[1] > b[3]
? a[0] > b[2] || a[2] < b[0]
? Math.min((a[1] + b[3]) / 2, a[1] - startUp)
: (a[1] + b[3]) / 2
? Math.min((startEl[1] + endEl[3]) / 2, a[1] - startUp)
: (startEl[1] + endEl[3]) / 2
: a[1] > b[1]
? a[1] - startUp
: common[1] - startUp,
a[2] < b[0]
? a[1] > b[3] || a[3] < b[1]
? Math.max((a[2] + b[0]) / 2, a[2] + startRight)
: (a[2] + b[0]) / 2
? Math.max((startEl[2] + endEl[0]) / 2, a[2] + startRight)
: (startEl[2] + endEl[0]) / 2
: a[2] < b[2]
? a[2] + startRight
: common[2] + startRight,
a[3] < b[1]
? a[0] > b[2] || a[2] < b[0]
? Math.max((a[3] + b[1]) / 2, a[3] + startDown)
: (a[3] + b[1]) / 2
? Math.max((startEl[3] + endEl[1]) / 2, a[3] + startDown)
: (startEl[3] + endEl[1]) / 2
: a[3] < b[3]
? a[3] + startDown
: common[3] + startDown,
@@ -514,29 +544,29 @@ const generateDynamicAABBs = (
const second = [
b[0] > a[2]
? b[1] > a[3] || b[3] < a[1]
? Math.min((b[0] + a[2]) / 2, b[0] - endLeft)
: (b[0] + a[2]) / 2
? Math.min((endEl[0] + startEl[2]) / 2, b[0] - endLeft)
: (endEl[0] + startEl[2]) / 2
: b[0] > a[0]
? b[0] - endLeft
: common[0] - endLeft,
b[1] > a[3]
? b[0] > a[2] || b[2] < a[0]
? Math.min((b[1] + a[3]) / 2, b[1] - endUp)
: (b[1] + a[3]) / 2
? Math.min((endEl[1] + startEl[3]) / 2, b[1] - endUp)
: (endEl[1] + startEl[3]) / 2
: b[1] > a[1]
? b[1] - endUp
: common[1] - endUp,
b[2] < a[0]
? b[1] > a[3] || b[3] < a[1]
? Math.max((b[2] + a[0]) / 2, b[2] + endRight)
: (b[2] + a[0]) / 2
? Math.max((endEl[2] + startEl[0]) / 2, b[2] + endRight)
: (endEl[2] + startEl[0]) / 2
: b[2] < a[2]
? b[2] + endRight
: common[2] + endRight,
b[3] < a[1]
? b[0] > a[2] || b[2] < a[0]
? Math.max((b[3] + a[1]) / 2, b[3] + endDown)
: (b[3] + a[1]) / 2
? Math.max((endEl[3] + startEl[1]) / 2, b[3] + endDown)
: (endEl[3] + startEl[1]) / 2
: b[3] < a[3]
? b[3] + endDown
: common[3] + endDown,
+1 -7
View File
@@ -11,7 +11,7 @@ import {
isBoundToContainer,
isTextElement,
} from "./typeChecks";
import { CLASSES, isSafari, POINTER_BUTTON, THEME } from "../constants";
import { CLASSES, isSafari, POINTER_BUTTON } from "../constants";
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
@@ -50,7 +50,6 @@ import {
originalContainerCache,
updateOriginalContainerCache,
} from "./containerCache";
import { applyDarkModeFilter } from "../colors";
const getTransform = (
width: number,
@@ -274,15 +273,10 @@ export const textWysiwyg = ({
textAlign,
verticalAlign,
color: updatedTextElement.strokeColor,
// color:
// appState.theme === THEME.DARK
// ? applyDarkModeFilter(updatedTextElement.strokeColor)
// : updatedTextElement.strokeColor,
opacity: updatedTextElement.opacity / 100,
filter: "var(--theme-filter)",
maxHeight: `${editorMaxHeight}px`,
});
// console.log("...", updatedTextElement.strokeColor);
editable.scrollTop = 0;
// For some reason updating font attribute doesn't set font family
// hence updating font family explicitly for test environment
+5 -2
View File
@@ -320,9 +320,12 @@ export const getDefaultRoundnessTypeForElement = (
};
export const isFixedPointBinding = (
binding: PointBinding,
binding: PointBinding | FixedPointBinding,
): binding is FixedPointBinding => {
return binding.fixedPoint != null;
return (
Object.hasOwn(binding, "fixedPoint") &&
(binding as FixedPointBinding).fixedPoint != null
);
};
// TODO: Move this to @excalidraw/math
+12 -7
View File
@@ -193,6 +193,7 @@ export type ExcalidrawElement =
| ExcalidrawGenericElement
| ExcalidrawTextElement
| ExcalidrawLinearElement
| ExcalidrawArrowElement
| ExcalidrawFreeDrawElement
| ExcalidrawImageElement
| ExcalidrawFrameElement
@@ -268,15 +269,19 @@ export type PointBinding = {
elementId: ExcalidrawBindableElement["id"];
focus: 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 =
| "arrow"
+7 -7
View File
@@ -24,14 +24,14 @@ import Cascadia from "./assets/CascadiaCode-Regular.woff2";
import ComicShanns from "./assets/ComicShanns-Regular.woff2";
import LiberationSans from "./assets/LiberationSans-Regular.woff2";
import LilitaLatin from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2";
import LilitaLatinExt from "https://fonts.gstatic.com/s/lilitaone/v15/i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
import LilitaLatin from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYEF8RXi4EwQ.woff2";
import LilitaLatinExt from "./assets/Lilita-Regular-i7dPIFZ9Zz-WBtRtedDbYE98RXi4EwSsbg.woff2";
import NunitoLatin from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2";
import NunitoLatinExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2";
import NunitoCyrilic from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2";
import NunitoCyrilicExt from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2";
import NunitoVietnamese from "https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
import NunitoLatin from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2";
import NunitoLatinExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTo3j6zbXWjgevT5.woff2";
import NunitoCyrilic from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTA3j6zbXWjgevT5.woff2";
import NunitoCyrilicExt from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTk3j6zbXWjgevT5.woff2";
import NunitoVietnamese from "./assets/Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTs3j6zbXWjgevT5.woff2";
export class Fonts {
// it's ok to track fonts across multiple instances only once, so let's use
-10
View File
@@ -104,13 +104,3 @@ declare namespace jest {
toBeNonNaNNumber(): void;
}
}
declare namespace tinycolor {
interface Instance {
_r: number;
_g: number;
_b: number;
_a: number;
_ok: boolean;
}
}
-2
View File
@@ -63,7 +63,6 @@
"@radix-ui/react-popover": "1.0.3",
"@radix-ui/react-tabs": "1.0.2",
"@tldraw/vec": "1.7.1",
"@types/tinycolor2": "1.4.6",
"browser-fs-access": "0.29.1",
"canvas-roundrect-polyfill": "0.0.1",
"clsx": "1.1.1",
@@ -85,7 +84,6 @@
"pwacompat": "2.0.17",
"roughjs": "4.6.4",
"sass": "1.51.0",
"tinycolor2": "1.6.0",
"tunnel-rat": "0.1.2"
},
"devDependencies": {
+2 -6
View File
@@ -3,7 +3,6 @@ import type { StaticCanvasAppState, AppState } from "../types";
import type { StaticCanvasRenderConfig } from "../scene/types";
import { THEME, THEME_FILTER } from "../constants";
import { applyDarkModeFilter } from "../colors";
export const fillCircle = (
context: CanvasRenderingContext2D,
@@ -51,7 +50,7 @@ export const bootstrapCanvas = ({
context.scale(scale, scale);
if (isExporting && theme === THEME.DARK) {
// context.filter = THEME_FILTER;
context.filter = THEME_FILTER;
}
// Paint background
@@ -65,10 +64,7 @@ export const bootstrapCanvas = ({
context.clearRect(0, 0, normalizedWidth, normalizedHeight);
}
context.save();
context.fillStyle =
theme === THEME.DARK
? applyDarkModeFilter(viewBackgroundColor)
: viewBackgroundColor;
context.fillStyle = viewBackgroundColor;
context.fillRect(0, 0, normalizedWidth, normalizedHeight);
context.restore();
} else {
@@ -52,7 +52,6 @@ import {
} from "./helpers";
import oc from "open-color";
import {
isArrowElement,
isElbowArrow,
isFrameLikeElement,
isLinearElement,
@@ -807,7 +806,6 @@ const _renderInteractiveScene = ({
// Elbow arrow elements cannot be selected when bound on either end
(
isSingleLinearElementSelected &&
isArrowElement(element) &&
isElbowArrow(element) &&
(element.startBinding || element.endBinding)
)
+7 -20
View File
@@ -61,7 +61,6 @@ import { ShapeCache } from "../scene/ShapeCache";
import { getVerticalOffset } from "../fonts";
import { isRightAngleRads } from "../../math";
import { getCornerRadius } from "../shapes";
import { applyDarkModeFilter } from "../colors";
// 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
@@ -248,9 +247,9 @@ const generateElementCanvas = (
const rc = rough.canvas(canvas);
// in dark theme, revert the image color filter
// if (shouldResetImageFilter(element, renderConfig, appState)) {
// context.filter = IMAGE_INVERT_FILTER;
// }
if (shouldResetImageFilter(element, renderConfig, appState)) {
context.filter = IMAGE_INVERT_FILTER;
}
drawElementOnCanvas(element, rc, context, renderConfig, appState);
@@ -404,10 +403,7 @@ const drawElementOnCanvas = (
case "freedraw": {
// Draw directly to canvas
context.save();
context.fillStyle =
appState.theme === THEME.DARK
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor;
context.fillStyle = element.strokeColor;
const path = getFreeDrawPath2D(element) as Path2D;
const fillShape = ShapeCache.get(element);
@@ -416,10 +412,7 @@ const drawElementOnCanvas = (
rc.draw(fillShape);
}
context.fillStyle =
appState.theme === THEME.DARK
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor;
context.fillStyle = element.strokeColor;
context.fill(path);
context.restore();
@@ -465,10 +458,7 @@ const drawElementOnCanvas = (
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
context.save();
context.font = getFontString(element);
context.fillStyle =
appState.theme === THEME.DARK
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor;
context.fillStyle = element.strokeColor;
context.textAlign = element.textAlign as CanvasTextAlign;
// Canvas does not support multiline text by default
@@ -709,10 +699,7 @@ export const renderElement = (
context.fillStyle = "rgba(0, 0, 200, 0.04)";
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
context.strokeStyle =
appState.theme === THEME.DARK
? applyDarkModeFilter(element.strokeColor)
: FRAME_STYLE.strokeColor;
context.strokeStyle = FRAME_STYLE.strokeColor;
// TODO change later to only affect AI frames
if (isMagicFrameElement(element)) {
+4 -21
View File
@@ -5,7 +5,6 @@ import {
MAX_DECIMALS_FOR_SVG_EXPORT,
MIME_TYPES,
SVG_NS,
THEME,
} from "../constants";
import { normalizeLink, toValidURL } from "../data/url";
import { getElementAbsoluteCoords } from "../element";
@@ -38,7 +37,6 @@ import { getFontFamilyString, isRTL, isTestEnv } from "../utils";
import { getFreeDrawSvgPath, IMAGE_INVERT_FILTER } from "./renderElement";
import { getVerticalOffset } from "../fonts";
import { getCornerRadius, isPathALoop } from "../shapes";
import { applyDarkModeFilter } from "../colors";
const roughSVGDrawWithPrecision = (
rsvg: RoughSVG,
@@ -141,7 +139,7 @@ const renderElementToSvg = (
case "rectangle":
case "diamond":
case "ellipse": {
const shape = ShapeCache.generateElementShape(element, renderConfig);
const shape = ShapeCache.generateElementShape(element, null);
const node = roughSVGDrawWithPrecision(
rsvg,
shape,
@@ -391,12 +389,7 @@ const renderElementToSvg = (
);
node.setAttribute("stroke", "none");
const path = svgRoot.ownerDocument!.createElementNS(SVG_NS, "path");
path.setAttribute(
"fill",
renderConfig.theme === THEME.DARK
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor,
);
path.setAttribute("fill", element.strokeColor);
path.setAttribute("d", getFreeDrawSvgPath(element));
node.appendChild(path);
@@ -533,12 +526,7 @@ const renderElementToSvg = (
rect.setAttribute("ry", FRAME_STYLE.radius.toString());
rect.setAttribute("fill", "none");
rect.setAttribute(
"stroke",
renderConfig.theme === THEME.DARK
? applyDarkModeFilter(FRAME_STYLE.strokeColor)
: FRAME_STYLE.strokeColor,
);
rect.setAttribute("stroke", FRAME_STYLE.strokeColor);
rect.setAttribute("stroke-width", FRAME_STYLE.strokeWidth.toString());
addToRoot(rect, element);
@@ -589,12 +577,7 @@ const renderElementToSvg = (
text.setAttribute("y", `${i * lineHeightPx + verticalOffset}`);
text.setAttribute("font-family", getFontFamilyString(element));
text.setAttribute("font-size", `${element.fontSize}px`);
text.setAttribute(
"fill",
renderConfig.theme === THEME.DARK
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor,
);
text.setAttribute("fill", element.strokeColor);
text.setAttribute("text-anchor", textAnchor);
text.setAttribute("style", "white-space: pre;");
text.setAttribute("direction", direction);
+10 -34
View File
@@ -13,7 +13,7 @@ import type {
import { generateFreeDrawShape } from "../renderer/renderElement";
import { isTransparent, assertNever } from "../utils";
import { simplify } from "points-on-curve";
import { ROUGHNESS, THEME } from "../constants";
import { ROUGHNESS } from "../constants";
import {
isElbowArrow,
isEmbeddableElement,
@@ -22,7 +22,7 @@ import {
isLinearElement,
} from "../element/typeChecks";
import { canChangeRoundness } from "./comparisons";
import type { AppState, EmbedsValidationStatus } from "../types";
import type { EmbedsValidationStatus } from "../types";
import {
point,
pointDistance,
@@ -30,7 +30,6 @@ import {
type LocalPoint,
} from "../../math";
import { getCornerRadius, isPathALoop } from "../shapes";
import { applyDarkModeFilter } from "../colors";
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
@@ -62,7 +61,6 @@ function adjustRoughness(element: ExcalidrawElement): number {
export const generateRoughOptions = (
element: ExcalidrawElement,
continuousPath = false,
isDarkMode: boolean = false,
): Options => {
const options: Options = {
seed: element.seed,
@@ -87,9 +85,7 @@ export const generateRoughOptions = (
fillWeight: element.strokeWidth / 2,
hachureGap: element.strokeWidth * 4,
roughness: adjustRoughness(element),
stroke: isDarkMode
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor,
stroke: element.strokeColor,
preserveVertices:
continuousPath || element.roughness < ROUGHNESS.cartoonist,
};
@@ -103,8 +99,6 @@ export const generateRoughOptions = (
options.fillStyle = element.fillStyle;
options.fill = isTransparent(element.backgroundColor)
? undefined
: isDarkMode
? applyDarkModeFilter(element.backgroundColor)
: element.backgroundColor;
if (element.type === "ellipse") {
options.curveFitting = 1;
@@ -118,8 +112,6 @@ export const generateRoughOptions = (
options.fill =
element.backgroundColor === "transparent"
? undefined
: isDarkMode
? applyDarkModeFilter(element.backgroundColor)
: element.backgroundColor;
}
return options;
@@ -173,7 +165,6 @@ const getArrowheadShapes = (
generator: RoughGenerator,
options: Options,
canvasBackgroundColor: string,
isDarkMode: boolean,
) => {
const arrowheadPoints = getArrowheadPoints(
element,
@@ -201,14 +192,10 @@ const getArrowheadShapes = (
fill:
arrowhead === "circle_outline"
? canvasBackgroundColor
: isDarkMode
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor,
fillStyle: "solid",
stroke: isDarkMode
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor,
stroke: element.strokeColor,
roughness: Math.min(0.5, options.roughness || 0),
}),
];
@@ -233,8 +220,6 @@ const getArrowheadShapes = (
fill:
arrowhead === "triangle_outline"
? canvasBackgroundColor
: isDarkMode
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor,
fillStyle: "solid",
roughness: Math.min(1, options.roughness || 0),
@@ -263,8 +248,6 @@ const getArrowheadShapes = (
fill:
arrowhead === "diamond_outline"
? canvasBackgroundColor
: isDarkMode
? applyDarkModeFilter(element.strokeColor)
: element.strokeColor,
fillStyle: "solid",
roughness: Math.min(1, options.roughness || 0),
@@ -308,15 +291,12 @@ export const _generateElementShape = (
isExporting,
canvasBackgroundColor,
embedsValidationStatus,
theme,
}: {
isExporting: boolean;
canvasBackgroundColor: string;
embedsValidationStatus: EmbedsValidationStatus | null;
theme: AppState["theme"];
},
): Drawable | Drawable[] | null => {
const isDarkMode = theme === THEME.DARK;
switch (element.type) {
case "rectangle":
case "iframe":
@@ -342,7 +322,6 @@ export const _generateElementShape = (
embedsValidationStatus,
),
true,
isDarkMode,
),
);
} else {
@@ -358,7 +337,6 @@ export const _generateElementShape = (
embedsValidationStatus,
),
false,
isDarkMode,
),
);
}
@@ -396,7 +374,7 @@ export const _generateElementShape = (
C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${
topY + horizontalRadius
}`,
generateRoughOptions(element, true, isDarkMode),
generateRoughOptions(element, true),
);
} else {
shape = generator.polygon(
@@ -406,7 +384,7 @@ export const _generateElementShape = (
[bottomX, bottomY],
[leftX, leftY],
],
generateRoughOptions(element, undefined, isDarkMode),
generateRoughOptions(element),
);
}
return shape;
@@ -417,14 +395,14 @@ export const _generateElementShape = (
element.height / 2,
element.width,
element.height,
generateRoughOptions(element, undefined, isDarkMode),
generateRoughOptions(element),
);
return shape;
}
case "line":
case "arrow": {
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
// initial position to it
@@ -436,7 +414,7 @@ export const _generateElementShape = (
shape = [
generator.path(
generateElbowArrowShape(points, 16),
generateRoughOptions(element, true, isDarkMode),
generateRoughOptions(element, true),
),
];
} else if (!element.roundness) {
@@ -468,7 +446,6 @@ export const _generateElementShape = (
generator,
options,
canvasBackgroundColor,
isDarkMode,
);
shape.push(...shapes);
}
@@ -486,7 +463,6 @@ export const _generateElementShape = (
generator,
options,
canvasBackgroundColor,
isDarkMode,
);
shape.push(...shapes);
}
@@ -501,7 +477,7 @@ export const _generateElementShape = (
// generate rough polygon to fill freedraw shape
const simplifiedPoints = simplify(element.points, 0.75);
shape = generator.curve(simplifiedPoints as [number, number][], {
...generateRoughOptions(element, undefined, isDarkMode),
...generateRoughOptions(element),
stroke: "none",
});
} else {
-3
View File
@@ -9,7 +9,6 @@ import { _generateElementShape } from "./Shape";
import type { ElementShape, ElementShapes } from "./types";
import { COLOR_PALETTE } from "../colors";
import type { AppState, EmbedsValidationStatus } from "../types";
import { THEME } from "..";
export class ShapeCache {
private static rg = new RoughGenerator();
@@ -53,7 +52,6 @@ export class ShapeCache {
isExporting: boolean;
canvasBackgroundColor: AppState["viewBackgroundColor"];
embedsValidationStatus: EmbedsValidationStatus;
theme: AppState["theme"];
} | null,
) => {
// when exporting, always regenerated to guarantee the latest shape
@@ -76,7 +74,6 @@ export class ShapeCache {
isExporting: false,
canvasBackgroundColor: COLOR_PALETTE.white,
embedsValidationStatus: null,
theme: THEME.LIGHT,
},
) as T["type"] extends keyof ElementShapes
? ElementShapes[T["type"]]
+13 -15
View File
@@ -40,7 +40,6 @@ import { syncInvalidIndices } from "../fractionalIndex";
import { renderStaticScene } from "../renderer/staticScene";
import { Fonts } from "../fonts";
import type { Font } from "../fonts/ExcalidrawFont";
import { applyDarkModeFilter } from "../colors";
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
@@ -186,6 +185,11 @@ export const exportToCanvas = async (
exportingFrame ?? 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({
elements,
@@ -215,8 +219,6 @@ export const exportToCanvas = async (
files,
});
const theme = appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT;
renderStaticScene({
canvas,
rc: rough.canvas(canvas),
@@ -236,7 +238,7 @@ export const exportToCanvas = async (
scrollY: -minY + exportPadding,
zoom: defaultAppState.zoom,
shouldCacheIgnoreZoom: false,
theme,
theme: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT,
},
renderConfig: {
canvasBackgroundColor: viewBackgroundColor,
@@ -247,7 +249,6 @@ export const exportToCanvas = async (
embedsValidationStatus: new Map(),
elementsPendingErasure: new Set(),
pendingFlowchartNodes: null,
theme,
},
});
@@ -334,7 +335,7 @@ export const exportToSvg = async (
svgRoot.setAttribute("width", `${width * exportScale}`);
svgRoot.setAttribute("height", `${height * exportScale}`);
if (exportWithDarkMode) {
// svgRoot.setAttribute("filter", THEME_FILTER);
svgRoot.setAttribute("filter", THEME_FILTER);
}
const offsetX = -minX + exportPadding;
@@ -355,6 +356,11 @@ export const exportToSvg = async (
}) rotate(${frame.angle} ${cx} ${cy})"
width="${frame.width}"
height="${frame.height}"
${
exportingFrame
? ""
: `rx=${FRAME_STYLE.radius} ry=${FRAME_STYLE.radius}`
}
>
</rect>
</clipPath>`;
@@ -380,12 +386,7 @@ export const exportToSvg = async (
rect.setAttribute("y", "0");
rect.setAttribute("width", `${width}`);
rect.setAttribute("height", `${height}`);
rect.setAttribute(
"fill",
appState.exportWithDarkMode
? applyDarkModeFilter(viewBackgroundColor)
: viewBackgroundColor,
);
rect.setAttribute("fill", viewBackgroundColor);
svgRoot.appendChild(rect);
}
@@ -393,8 +394,6 @@ export const exportToSvg = async (
const renderEmbeddables = opts?.renderEmbeddables ?? false;
const theme = appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT;
renderSceneToSvg(
elementsForRender,
toBrandedType<RenderableElementsMap>(arrayToMap(elementsForRender)),
@@ -416,7 +415,6 @@ export const exportToSvg = async (
.map((element) => [element.id, true]),
)
: new Map(),
theme,
},
);
-2
View File
@@ -35,7 +35,6 @@ export type StaticCanvasRenderConfig = {
embedsValidationStatus: EmbedsValidationStatus;
elementsPendingErasure: ElementsPendingErasure;
pendingFlowchartNodes: PendingExcalidrawElements | null;
theme: AppState["theme"];
};
export type SVGRenderConfig = {
@@ -47,7 +46,6 @@ export type SVGRenderConfig = {
frameRendering: AppState["frameRendering"];
canvasBackgroundColor: AppState["viewBackgroundColor"];
embedsValidationStatus: EmbedsValidationStatus;
theme: AppState["theme"];
};
export type InteractiveCanvasRenderConfig = {
@@ -8430,6 +8430,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
"selectedLinearElement": LinearElementEditor {
"elbowed": false,
"elementId": "id0",
"endBindingElement": "keep",
"hoverPointIndex": -1,
@@ -8649,6 +8650,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
"selectedLinearElement": LinearElementEditor {
"elbowed": false,
"elementId": "id0",
"endBindingElement": "keep",
"hoverPointIndex": -1,
@@ -9058,6 +9060,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
"selectedLinearElement": LinearElementEditor {
"elbowed": false,
"elementId": "id0",
"endBindingElement": "keep",
"hoverPointIndex": -1,
@@ -9454,6 +9457,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": {},
"selectedLinearElement": LinearElementEditor {
"elbowed": false,
"elementId": "id0",
"endBindingElement": "keep",
"hoverPointIndex": -1,
+16 -2
View File
@@ -9,6 +9,8 @@ import type {
ExcalidrawFrameElement,
ExcalidrawElementType,
ExcalidrawMagicFrameElement,
ExcalidrawElbowArrowElement,
ExcalidrawArrowElement,
} from "../../element/types";
import { newElement, newTextElement, newLinearElement } from "../../element";
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
@@ -127,6 +129,10 @@ export class API {
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 = <
T extends Exclude<ExcalidrawElementType, "selection"> = "rectangle",
>({
@@ -179,10 +185,16 @@ export class API {
scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never;
status?: T extends "image" ? ExcalidrawImageElement["status"] : never;
startBinding?: T extends "arrow"
? ExcalidrawLinearElement["startBinding"]
? ExcalidrawArrowElement["startBinding"] | ExcalidrawElbowArrowElement["startBinding"]
: never;
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;
elbowed?: boolean;
}): T extends "arrow" | "line"
@@ -340,6 +352,8 @@ export class API {
if (element.type === "arrow") {
element.startBinding = rest.startBinding ?? null;
element.endBinding = rest.endBinding ?? null;
element.startArrowhead = rest.startArrowhead ?? null;
element.endArrowhead = rest.endArrowhead ?? null;
}
if (id) {
element.id = id;
+5 -4
View File
@@ -31,6 +31,7 @@ import type {
ExcalidrawGenericElement,
ExcalidrawLinearElement,
ExcalidrawTextElement,
FixedPointBinding,
FractionalIndex,
SceneElementsMap,
} from "../element/types";
@@ -2049,13 +2050,13 @@ describe("history", () => {
focus: -0.001587301587301948,
gap: 5,
fixedPoint: [1.0318471337579618, 0.49920634920634904],
},
} as FixedPointBinding,
endBinding: {
elementId: "u2JGnnmoJ0VATV4vCNJE5",
focus: -0.0016129032258049847,
gap: 3.537079145500037,
fixedPoint: [0.4991935483870975, -0.03875193720914723],
},
} as FixedPointBinding,
},
],
storeAction: StoreAction.CAPTURE,
@@ -4455,7 +4456,7 @@ describe("history", () => {
elements: [
h.elements[0],
newElementWith(h.elements[1], { boundElements: [] }),
newElementWith(h.elements[2] as ExcalidrawLinearElement, {
newElementWith(h.elements[2] as ExcalidrawElbowArrowElement, {
endBinding: {
elementId: remoteContainer.id,
gap: 1,
@@ -4655,7 +4656,7 @@ describe("history", () => {
// Simulate remote update
API.updateScene({
elements: [
newElementWith(h.elements[0] as ExcalidrawLinearElement, {
newElementWith(h.elements[0] as ExcalidrawElbowArrowElement, {
startBinding: {
elementId: rect1.id,
gap: 1,
+57 -2
View File
@@ -4,6 +4,7 @@ import { render } from "./test-utils";
import { reseed } from "../random";
import { UI, Keyboard, Pointer } from "./helpers/ui";
import type {
ExcalidrawElbowArrowElement,
ExcalidrawFreeDrawElement,
ExcalidrawLinearElement,
} from "../element/types";
@@ -333,6 +334,62 @@ describe("arrow element", () => {
expect(label.angle).toBeCloseTo(0);
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", () => {
@@ -828,7 +885,6 @@ describe("multiple selection", () => {
expect(leftBoundArrow.endBinding?.elementId).toBe(
leftArrowBinding.elementId,
);
expect(leftBoundArrow.endBinding?.fixedPoint).toBeNull();
expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus);
expect(rightBoundArrow.x).toBeCloseTo(210);
@@ -843,7 +899,6 @@ describe("multiple selection", () => {
expect(rightBoundArrow.endBinding?.elementId).toBe(
rightArrowBinding.elementId,
);
expect(rightBoundArrow.endBinding?.fixedPoint).toBeNull();
expect(rightBoundArrow.endBinding?.focus).toBe(rightArrowBinding.focus);
});
File diff suppressed because one or more lines are too long
+3 -3
View File
@@ -110,8 +110,8 @@ export const debugDrawBoundingBox = (
export const debugDrawBounds = (
box: Bounds | Bounds[],
opts?: {
color: string;
permanent: boolean;
color?: string;
permanent?: boolean;
},
) => {
(isBounds(box) ? [box] : box).forEach((bbox) =>
@@ -136,7 +136,7 @@ export const debugDrawBounds = (
],
{
color: opts?.color ?? "green",
permanent: opts?.permanent,
permanent: !!opts?.permanent,
},
),
);
-1
View File
@@ -68,7 +68,6 @@
"css-loader": "6.7.1",
"file-loader": "6.2.0",
"fonteditor-core": "2.4.0",
"node-fetch": "3.3.2",
"sass-loader": "13.0.2",
"ts-loader": "9.3.1",
"typescript": "4.9.4",
+5 -3
View File
@@ -1,7 +1,6 @@
const { build } = require("esbuild");
const { sassPlugin } = require("esbuild-sass-plugin");
const { externalGlobalPlugin } = require("esbuild-plugin-external-global");
const { woff2BrowserPlugin } = require("./woff2/woff2-esbuild-plugins");
// Will be used later for treeshaking
//const fs = require("fs");
@@ -45,13 +44,15 @@ const browserConfig = {
format: "esm",
plugins: [
sassPlugin(),
woff2BrowserPlugin(),
externalGlobalPlugin({
react: "React",
"react-dom": "ReactDOM",
}),
],
splitting: true,
loader: {
".woff2": "file",
},
};
const createESMBrowserBuild = async () => {
// Development unminified build with source maps
@@ -100,9 +101,10 @@ const rawConfig = {
entryPoints: ["index.tsx"],
bundle: true,
format: "esm",
plugins: [sassPlugin(), woff2BrowserPlugin()],
plugins: [sassPlugin()],
loader: {
".json": "copy",
".woff2": "file",
},
packages: "external",
};
+5 -5
View File
@@ -1,17 +1,17 @@
const fs = require("fs");
const { build } = require("esbuild");
const { sassPlugin } = require("esbuild-sass-plugin");
const {
woff2BrowserPlugin,
woff2ServerPlugin,
} = require("./woff2/woff2-esbuild-plugins");
const { woff2ServerPlugin } = require("./woff2/woff2-esbuild-plugins");
const browserConfig = {
entryPoints: ["index.ts"],
bundle: true,
format: "esm",
plugins: [sassPlugin(), woff2BrowserPlugin()],
plugins: [sassPlugin()],
assetNames: "assets/[name]",
loader: {
".woff2": "file",
},
};
// Will be used later for treeshaking
+2 -63
View File
@@ -2,45 +2,9 @@ const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const which = require("which");
const fetch = require("node-fetch");
const wawoff = require("wawoff2");
const { Font } = require("fonteditor-core");
/**
* Custom esbuild plugin to convert url woff2 imports into a text.
* Other woff2 imports are handled by a "file" loader.
*
* @returns {import("esbuild").Plugin}
*/
module.exports.woff2BrowserPlugin = () => {
return {
name: "woff2BrowserPlugin",
setup(build) {
build.initialOptions.loader = {
".woff2": "file",
...build.initialOptions.loader,
};
build.onResolve({ filter: /^https:\/\/.+?\.woff2$/ }, (args) => {
return {
path: args.path,
namespace: "woff2BrowserPlugin",
};
});
build.onLoad(
{ filter: /.*/, namespace: "woff2BrowserPlugin" },
async (args) => {
return {
contents: args.path,
loader: "text",
};
},
);
},
};
};
/**
* Custom esbuild plugin to:
* 1. inline all woff2 (url and relative imports) as base64 for server-side use cases (no need for additional font fetch; works in both esm and commonjs)
@@ -53,27 +17,6 @@ module.exports.woff2BrowserPlugin = () => {
* @returns {import("esbuild").Plugin}
*/
module.exports.woff2ServerPlugin = (options = {}) => {
// google CDN fails time to time, so let's retry
async function fetchRetry(url, options = {}, retries = 0, delay = 1000) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`Status: ${response.status}, ${await response.json()}`);
}
return response;
} catch (e) {
if (retries > 0) {
await new Promise((resolve) => setTimeout(resolve, delay));
return fetchRetry(url, options, retries - 1, delay * 2);
}
console.error(`Couldn't fetch: ${url}, error: ${e.message}`);
throw e;
}
}
return {
name: "woff2ServerPlugin",
setup(build) {
@@ -82,9 +25,7 @@ module.exports.woff2ServerPlugin = (options = {}) => {
const fonts = new Map();
build.onResolve({ filter: /\.woff2$/ }, (args) => {
const resolvedPath = args.path.startsWith("http")
? args.path // url
: path.resolve(args.resolveDir, args.path); // absolute path
const resolvedPath = path.resolve(args.resolveDir, args.path);
return {
path: resolvedPath,
@@ -101,9 +42,7 @@ module.exports.woff2ServerPlugin = (options = {}) => {
// read local woff2 as a buffer (WARN: `readFileSync` does not work!)
woff2Buffer = await fs.promises.readFile(args.path);
} else {
// fetch remote woff2 as a buffer (i.e. from a cdn)
const response = await fetchRetry(args.path, {}, 3);
woff2Buffer = await response.buffer();
throw new Error(`Font path has to be absolute! "${args.path}"`);
}
// google's brotli decompression into snft
+13 -33
View File
@@ -1,15 +1,13 @@
// `EXCALIDRAW_ASSET_PATH` as a SSOT
const OSS_FONTS_CDN =
"https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/";
/**
* Custom vite plugin to convert url woff2 imports into a text.
* Other woff2 imports are automatically served and resolved as a file uri.
* Custom vite plugin for auto-prefixing `EXCALIDRAW_ASSET_PATH` woff2 fonts in `excalidraw-app`.
*
* @returns {import("vite").PluginOption}
*/
module.exports.woff2BrowserPlugin = () => {
// for now limited to woff2 only, might be extended to any assets in the future
const regex = /^https:\/\/.+?\.woff2$/;
let isDev;
return {
@@ -18,34 +16,9 @@ module.exports.woff2BrowserPlugin = () => {
config(_, { command }) {
isDev = command === "serve";
},
resolveId(source) {
if (!regex.test(source)) {
return null;
}
// getting the url to the dependency tree
return source;
},
load(id) {
if (!regex.test(id)) {
return null;
}
// loading the url as string
return `export default "${id}"`;
},
// necessary for dev as vite / rollup does skips https imports in serve (~dev) mode
// aka dev mode equivalent of "export default x" above (resolveId + load)
transform(code, id) {
// treat https woff2 imports as a text
if (isDev && id.endsWith("/excalidraw/fonts/index.ts")) {
return code.replaceAll(
/import\s+(\w+)\s+from\s+(["']https:\/\/.+?\.woff2["'])/g,
`const $1 = $2`,
);
}
// use CDN for Assistant
// using copy / replace as fonts defined in the `.css` don't have to be manually copied over (vite/rollup does this automatically),
// but at the same time can't be easily prefixed with the `EXCALIDRAW_ASSET_PATH` only for the `excalidraw-app`
if (!isDev && id.endsWith("/excalidraw/fonts/assets/fonts.css")) {
return `/* WARN: The following content is generated during excalidraw-app build */
@@ -90,7 +63,6 @@ module.exports.woff2BrowserPlugin = () => {
}`;
}
// using EXCALIDRAW_ASSET_PATH as a SSOT
if (!isDev && id.endsWith("excalidraw-app/index.html")) {
return code.replace(
"<!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->",
@@ -110,9 +82,10 @@ module.exports.woff2BrowserPlugin = () => {
type="font/woff2"
crossorigin="anonymous"
/>
<!-- For Nunito only preload the latin range, which should be good enough for now -->
<link
rel="preload"
href="${OSS_FONTS_CDN}Virgil-Regular-hO16qHwV.woff2"
href="${OSS_FONTS_CDN}Nunito-Regular-XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg-DqUjjPte.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
@@ -124,6 +97,13 @@ module.exports.woff2BrowserPlugin = () => {
type="font/woff2"
crossorigin="anonymous"
/>
<link
rel="preload"
href="${OSS_FONTS_CDN}Virgil-Regular-hO16qHwV.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
`,
);
}
+2 -4
View File
@@ -1,9 +1,7 @@
import { defineConfig } from "vitest/config";
import { woff2BrowserPlugin } from "./scripts/woff2/woff2-vite-plugins";
export default defineConfig({
//@ts-ignore
plugins: [woff2BrowserPlugin()],
test: {
// Since hooks are running in stack in v2, which means all hooks run serially whereas
// we need to run them in parallel
@@ -19,10 +17,10 @@ export default defineConfig({
// Additionally the thresholds also needs to be updated slightly as a result of this change
ignoreEmptyLines: false,
thresholds: {
lines: 66,
lines: 60,
branches: 70,
functions: 63,
statements: 66,
statements: 60,
},
},
},
+3 -77
View File
@@ -3299,11 +3299,6 @@
dependencies:
"@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":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
@@ -5199,11 +5194,6 @@ damerau-levenshtein@^1.0.8:
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
data-uri-to-buffer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
data-urls@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-4.0.0.tgz#333a454eca6f9a5b7b0f1013ff89074c3f522dd4"
@@ -6267,14 +6257,6 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
version "3.2.0"
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
dependencies:
node-domexception "^1.0.0"
web-streams-polyfill "^3.0.3"
fflate@^0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea"
@@ -6413,13 +6395,6 @@ form-data@^4.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
formdata-polyfill@^4.0.10:
version "4.0.10"
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
dependencies:
fetch-blob "^3.1.2"
fraction.js@^4.2.0:
version "4.3.7"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
@@ -8261,11 +8236,6 @@ no-case@^3.0.4:
lower-case "^2.0.2"
tslib "^2.0.3"
node-domexception@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
node-fetch@2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
@@ -8278,15 +8248,6 @@ node-fetch@2.6.7:
dependencies:
whatwg-url "^5.0.0"
node-fetch@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b"
integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==
dependencies:
data-uri-to-buffer "^4.0.0"
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"
node-html-parser@^5.3.3:
version "5.4.2"
resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-5.4.2.tgz#93e004038c17af80226c942336990a0eaed8136a"
@@ -9672,7 +9633,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0":
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -9690,15 +9651,6 @@ string-width@^4.1.0, string-width@^4.2.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
@@ -9770,14 +9722,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@6.0.1, strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^3.0.0, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -9981,11 +9926,6 @@ tinybench@^2.8.0:
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b"
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:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.1.tgz#c64233c4fac4304e109a64340178760116dbe1fe"
@@ -10572,11 +10512,6 @@ wawoff2@2.0.1:
dependencies:
argparse "^2.0.1"
web-streams-polyfill@^3.0.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b"
integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==
web-worker@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.3.0.tgz#e5f2df5c7fe356755a5fb8f8410d4312627e6776"
@@ -11019,7 +10954,7 @@ workbox-window@7.1.0, workbox-window@^7.0.0:
"@types/trusted-types" "^2.0.2"
workbox-core "7.1.0"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -11037,15 +10972,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"