Compare commits

..

3 Commits

Author SHA1 Message Date
Arnošt Pleskot 671ed94d74 chore: add timers 2023-06-16 23:42:45 +02:00
Arnošt Pleskot d5ac76d4ea feat: working export with pngjs 2023-06-16 22:36:24 +02:00
Arnošt Pleskot 2b19d53549 feat: init of generating blobs in chunk 2023-06-14 11:52:16 +02:00
114 changed files with 908 additions and 5496 deletions
+3 -3
View File
@@ -19,7 +19,6 @@
]
},
"dependencies": {
"@excalidraw/random-username": "1.0.0",
"@radix-ui/react-popover": "1.0.3",
"@radix-ui/react-tabs": "1.0.2",
"@sentry/browser": "6.2.5",
@@ -28,7 +27,6 @@
"@testing-library/react": "12.1.5",
"@tldraw/vec": "1.7.1",
"browser-fs-access": "0.29.1",
"canvas-roundrect-polyfill": "0.0.1",
"clsx": "1.1.1",
"cross-env": "7.0.3",
"fake-indexeddb": "3.1.7",
@@ -46,6 +44,7 @@
"png-chunk-text": "1.0.0",
"png-chunks-encode": "1.0.0",
"png-chunks-extract": "1.0.0",
"pngjs": "7.0.0",
"points-on-curve": "0.2.0",
"pwacompat": "2.0.17",
"react": "18.2.0",
@@ -76,6 +75,7 @@
"@types/lodash.throttle": "4.1.7",
"@types/pako": "1.0.3",
"@types/pica": "5.1.3",
"@types/pngjs": "6.0.1",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"@types/resize-observer-browser": "0.1.7",
@@ -108,7 +108,7 @@
"<rootDir>/src/packages/excalidraw/example"
],
"transformIgnorePatterns": [
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access|canvas-roundrect-polyfill)/)"
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access)/)"
],
"resetMocks": false
},
+1 -4
View File
@@ -12,10 +12,7 @@ export const actionAddToLibrary = register({
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
includeElementsInFrames: true,
},
true,
);
if (selectedElements.some((element) => element.type === "image")) {
return {
+12 -28
View File
@@ -10,7 +10,6 @@ import {
import { ToolButton } from "../components/ToolButton";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n";
import { KEYS } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene";
@@ -18,20 +17,10 @@ import { AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
const alignActionsPredicate = (
const enableActionGroup = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return (
selectedElements.length > 1 &&
// TODO enable aligning frames when implemented properly
!selectedElements.some((el) => el.type === "frame")
);
};
) => getSelectedElements(getNonDeletedElements(elements), appState).length > 1;
const alignSelectedElements = (
elements: readonly ExcalidrawElement[],
@@ -47,16 +36,14 @@ const alignSelectedElements = (
const updatedElementsMap = arrayToMap(updatedElements);
return updateFrameMembershipOfSelectedElements(
elements.map((element) => updatedElementsMap.get(element.id) || element),
appState,
return elements.map(
(element) => updatedElementsMap.get(element.id) || element,
);
};
export const actionAlignTop = register({
name: "alignTop",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState) => {
return {
appState,
@@ -71,7 +58,7 @@ export const actionAlignTop = register({
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState)}
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={AlignTopIcon}
onClick={() => updateData(null)}
@@ -87,7 +74,6 @@ export const actionAlignTop = register({
export const actionAlignBottom = register({
name: "alignBottom",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState) => {
return {
appState,
@@ -102,7 +88,7 @@ export const actionAlignBottom = register({
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState)}
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={AlignBottomIcon}
onClick={() => updateData(null)}
@@ -118,7 +104,6 @@ export const actionAlignBottom = register({
export const actionAlignLeft = register({
name: "alignLeft",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState) => {
return {
appState,
@@ -133,7 +118,7 @@ export const actionAlignLeft = register({
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState)}
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={AlignLeftIcon}
onClick={() => updateData(null)}
@@ -149,7 +134,7 @@ export const actionAlignLeft = register({
export const actionAlignRight = register({
name: "alignRight",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState) => {
return {
appState,
@@ -164,7 +149,7 @@ export const actionAlignRight = register({
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState)}
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={AlignRightIcon}
onClick={() => updateData(null)}
@@ -180,7 +165,7 @@ export const actionAlignRight = register({
export const actionAlignVerticallyCentered = register({
name: "alignVerticallyCentered",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState) => {
return {
appState,
@@ -193,7 +178,7 @@ export const actionAlignVerticallyCentered = register({
},
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState)}
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={CenterVerticallyIcon}
onClick={() => updateData(null)}
@@ -207,7 +192,6 @@ export const actionAlignVerticallyCentered = register({
export const actionAlignHorizontallyCentered = register({
name: "alignHorizontallyCentered",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState) => {
return {
appState,
@@ -220,7 +204,7 @@ export const actionAlignHorizontallyCentered = register({
},
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState)}
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={CenterHorizontallyIcon}
onClick={() => updateData(null)}
-1
View File
@@ -249,7 +249,6 @@ export const actionWrapTextInContainer = register({
"rectangle",
),
groupIds: textElement.groupIds,
frameId: textElement.frameId,
});
// update bindings
+3 -7
View File
@@ -20,8 +20,6 @@ import {
isHandToolActive,
} from "../appState";
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
import { excludeElementsInFramesFromSelection } from "../scene/selection";
import { Bounds } from "../element/bounds";
export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor",
@@ -208,7 +206,7 @@ export const actionResetZoom = register({
});
const zoomValueToFitBoundsOnViewport = (
bounds: Bounds,
bounds: [number, number, number, number],
viewportDimensions: { width: number; height: number },
) => {
const [x1, y1, x2, y2] = bounds;
@@ -236,10 +234,8 @@ export const zoomToFitElements = (
const commonBounds =
zoomToSelection && selectedElements.length > 0
? getCommonBounds(excludeElementsInFramesFromSelection(selectedElements))
: getCommonBounds(
excludeElementsInFramesFromSelection(nonDeletedElements),
);
? getCommonBounds(selectedElements)
: getCommonBounds(nonDeletedElements);
const newZoom = {
value: zoomValueToFitBoundsOnViewport(commonBounds, {
+6 -19
View File
@@ -16,12 +16,9 @@ export const actionCopy = register({
name: "copy",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
const elementsToCopy = getSelectedElements(elements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
});
const selectedElements = getSelectedElements(elements, appState, true);
copyToClipboard(elementsToCopy, app.files);
copyToClipboard(selectedElements, app.files);
return {
commitToHistory: false,
@@ -78,10 +75,7 @@ export const actionCopyAsSvg = register({
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
includeElementsInFrames: true,
},
true,
);
try {
await exportCanvas(
@@ -125,10 +119,7 @@ export const actionCopyAsPng = register({
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
includeElementsInFrames: true,
},
true,
);
try {
await exportCanvas(
@@ -181,9 +172,7 @@ export const copyText = register({
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
true,
);
const text = selectedElements
@@ -202,9 +191,7 @@ export const copyText = register({
predicate: (elements, appState) => {
return (
probablySupportsClipboardWriteText &&
getSelectedElements(elements, appState, {
includeBoundTextElement: true,
}).some(isTextElement)
getSelectedElements(elements, appState, true).some(isTextElement)
);
},
contextItemLabel: "labels.copyText",
+1 -13
View File
@@ -1,4 +1,4 @@
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { isSomeElementSelected } from "../scene";
import { KEYS } from "../keys";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
@@ -18,23 +18,11 @@ const deleteSelectedElements = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const framesToBeDeleted = new Set(
getSelectedElements(
elements.filter((el) => el.type === "frame"),
appState,
).map((el) => el.id),
);
return {
elements: elements.map((el) => {
if (appState.selectedElementIds[el.id]) {
return newElementWith(el, { isDeleted: true });
}
if (el.frameId && framesToBeDeleted.has(el.frameId)) {
return newElementWith(el, { isDeleted: true });
}
if (
isBoundToContainer(el) &&
appState.selectedElementIds[el.containerId]
+3 -15
View File
@@ -6,7 +6,6 @@ import { ToolButton } from "../components/ToolButton";
import { distributeElements, Distribution } from "../distribute";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n";
import { CODES, KEYS } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene";
@@ -17,17 +16,7 @@ import { register } from "./register";
const enableActionGroup = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return (
selectedElements.length > 1 &&
// TODO enable distributing frames when implemented properly
!selectedElements.some((el) => el.type === "frame")
);
};
) => getSelectedElements(getNonDeletedElements(elements), appState).length > 1;
const distributeSelectedElements = (
elements: readonly ExcalidrawElement[],
@@ -43,9 +32,8 @@ const distributeSelectedElements = (
const updatedElementsMap = arrayToMap(updatedElements);
return updateFrameMembershipOfSelectedElements(
elements.map((element) => updatedElementsMap.get(element.id) || element),
appState,
return elements.map(
(element) => updatedElementsMap.get(element.id) || element,
);
};
+12 -68
View File
@@ -2,7 +2,7 @@ import { KEYS } from "../keys";
import { register } from "./register";
import { ExcalidrawElement } from "../element/types";
import { duplicateElement, getNonDeletedElements } from "../element";
import { isSomeElementSelected } from "../scene";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { arrayToMap, getShortcutKey } from "../utils";
@@ -20,17 +20,9 @@ import {
bindTextToShapeAfterDuplication,
getBoundTextElement,
} from "../element/textElement";
import { isBoundToContainer, isFrameElement } from "../element/typeChecks";
import { isBoundToContainer } from "../element/typeChecks";
import { normalizeElementOrder } from "../element/sortElements";
import { DuplicateIcon } from "../components/icons";
import {
bindElementsToFramesAfterDuplication,
getFrameElements,
} from "../frame";
import {
excludeElementsInFramesFromSelection,
getSelectedElements,
} from "../scene/selection";
export const actionDuplicateSelection = register({
name: "duplicateSelection",
@@ -102,11 +94,8 @@ const duplicateElements = (
return newElement;
};
const idsOfElementsToDuplicate = arrayToMap(
getSelectedElements(sortedElements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
}),
const selectedElementIds = arrayToMap(
getSelectedElements(sortedElements, appState, true),
);
// Ids of elements that have already been processed so we don't push them
@@ -140,25 +129,12 @@ const duplicateElements = (
}
const boundTextElement = getBoundTextElement(element);
const isElementAFrame = isFrameElement(element);
if (idsOfElementsToDuplicate.get(element.id)) {
// if a group or a container/bound-text or frame, duplicate atomically
if (element.groupIds.length || boundTextElement || isElementAFrame) {
if (selectedElementIds.get(element.id)) {
// if a group or a container/bound-text, duplicate atomically
if (element.groupIds.length || boundTextElement) {
const groupId = getSelectedGroupForElement(appState, element);
if (groupId) {
// TODO:
// remove `.flatMap...`
// if the elements in a frame are grouped when the frame is grouped
const groupElements = getElementsInGroup(
sortedElements,
groupId,
).flatMap((element) =>
isFrameElement(element)
? [...getFrameElements(elements, element.id), element]
: [element],
);
const groupElements = getElementsInGroup(sortedElements, groupId);
elementsWithClones.push(
...markAsProcessed([
...groupElements,
@@ -180,34 +156,10 @@ const duplicateElements = (
);
continue;
}
if (isElementAFrame) {
const elementsInFrame = getFrameElements(sortedElements, element.id);
elementsWithClones.push(
...markAsProcessed([
...elementsInFrame,
element,
...elementsInFrame.map((e) => duplicateAndOffsetElement(e)),
duplicateAndOffsetElement(element),
]),
);
continue;
}
}
// since elements in frames have a lower z-index than the frame itself,
// they will be looped first and if their frames are selected as well,
// they will have been copied along with the frame atomically in the
// above branch, so we must skip those elements here
//
// now, for elements do not belong any frames or elements whose frames
// are selected (or elements that are left out from the above
// steps for whatever reason) we (should at least) duplicate them here
if (!element.frameId || !idsOfElementsToDuplicate.has(element.frameId)) {
elementsWithClones.push(
...markAsProcessed([element, duplicateAndOffsetElement(element)]),
);
}
elementsWithClones.push(
...markAsProcessed([element, duplicateAndOffsetElement(element)]),
);
} else {
elementsWithClones.push(...markAsProcessed([element]));
}
@@ -248,14 +200,6 @@ const duplicateElements = (
oldElements,
oldIdToDuplicatedId,
);
bindElementsToFramesAfterDuplication(
finalElements,
oldElements,
oldIdToDuplicatedId,
);
const nextElementsToSelect =
excludeElementsInFramesFromSelection(newElements);
return {
elements: finalElements,
@@ -263,7 +207,7 @@ const duplicateElements = (
{
...appState,
selectedGroupIds: {},
selectedElementIds: nextElementsToSelect.reduce(
selectedElementIds: newElements.reduce(
(acc: Record<ExcalidrawElement["id"], true>, element) => {
if (!isBoundToContainer(element)) {
acc[element.id] = true;
+4 -17
View File
@@ -11,17 +11,8 @@ const shouldLock = (elements: readonly ExcalidrawElement[]) =>
export const actionToggleElementLock = register({
name: "toggleElementLock",
trackEvent: { category: "element" },
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return !selectedElements.some(
(element) => element.locked && element.frameId,
);
},
perform: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
});
const selectedElements = getSelectedElements(elements, appState, true);
if (!selectedElements.length) {
return false;
@@ -47,10 +38,8 @@ export const actionToggleElementLock = register({
};
},
contextItemLabel: (elements, appState) => {
const selected = getSelectedElements(elements, appState, {
includeBoundTextElement: false,
});
if (selected.length === 1 && selected[0].type !== "frame") {
const selected = getSelectedElements(elements, appState, false);
if (selected.length === 1) {
return selected[0].locked
? "labels.elementLock.unlock"
: "labels.elementLock.lock";
@@ -65,9 +54,7 @@ export const actionToggleElementLock = register({
event.key.toLocaleLowerCase() === KEYS.L &&
event[KEYS.CTRL_OR_CMD] &&
event.shiftKey &&
getSelectedElements(elements, appState, {
includeBoundTextElement: false,
}).length > 0
getSelectedElements(elements, appState, false).length > 0
);
},
});
+2 -12
View File
@@ -12,17 +12,13 @@ import {
isBindingEnabled,
unbindLinearElements,
} from "../element/binding";
import { updateFrameMembershipOfSelectedElements } from "../frame";
export const actionFlipHorizontal = register({
name: "flipHorizontal",
trackEvent: { category: "element" },
perform: (elements, appState) => {
return {
elements: updateFrameMembershipOfSelectedElements(
flipSelectedElements(elements, appState, "horizontal"),
appState,
),
elements: flipSelectedElements(elements, appState, "horizontal"),
appState,
commitToHistory: true,
};
@@ -36,10 +32,7 @@ export const actionFlipVertical = register({
trackEvent: { category: "element" },
perform: (elements, appState) => {
return {
elements: updateFrameMembershipOfSelectedElements(
flipSelectedElements(elements, appState, "vertical"),
appState,
),
elements: flipSelectedElements(elements, appState, "vertical"),
appState,
commitToHistory: true,
};
@@ -57,9 +50,6 @@ const flipSelectedElements = (
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeElementsInFrames: true,
},
);
const updatedElements = flipElements(
-140
View File
@@ -1,140 +0,0 @@
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { removeAllElementsFromFrame } from "../frame";
import { getFrameElements } from "../frame";
import { KEYS } from "../keys";
import { getSelectedElements } from "../scene";
import { AppState } from "../types";
import { setCursorForShape, updateActiveTool } from "../utils";
import { register } from "./register";
const isSingleFrameSelected = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return selectedElements.length === 1 && selectedElements[0].type === "frame";
};
export const actionSelectAllElementsInFrame = register({
name: "selectAllElementsInFrame",
trackEvent: { category: "canvas" },
perform: (elements, appState) => {
const selectedFrame = getSelectedElements(
getNonDeletedElements(elements),
appState,
)[0];
if (selectedFrame && selectedFrame.type === "frame") {
const elementsInFrame = getFrameElements(
getNonDeletedElements(elements),
selectedFrame.id,
).filter((element) => !(element.type === "text" && element.containerId));
return {
elements,
appState: {
...appState,
selectedElementIds: elementsInFrame.reduce((acc, element) => {
acc[element.id] = true;
return acc;
}, {} as Record<ExcalidrawElement["id"], true>),
},
commitToHistory: false,
};
}
return {
elements,
appState,
commitToHistory: false,
};
},
contextItemLabel: "labels.selectAllElementsInFrame",
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
});
export const actionRemoveAllElementsFromFrame = register({
name: "removeAllElementsFromFrame",
trackEvent: { category: "history" },
perform: (elements, appState) => {
const selectedFrame = getSelectedElements(
getNonDeletedElements(elements),
appState,
)[0];
if (selectedFrame && selectedFrame.type === "frame") {
return {
elements: removeAllElementsFromFrame(elements, selectedFrame, appState),
appState: {
...appState,
selectedElementIds: {
[selectedFrame.id]: true,
},
},
commitToHistory: true,
};
}
return {
elements,
appState,
commitToHistory: false,
};
},
contextItemLabel: "labels.removeAllElementsFromFrame",
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
});
export const actionToggleFrameRendering = register({
name: "toggleFrameRendering",
viewMode: true,
trackEvent: { category: "canvas" },
perform: (elements, appState) => {
return {
elements,
appState: {
...appState,
shouldRenderFrames: !appState.shouldRenderFrames,
},
commitToHistory: false,
};
},
contextItemLabel: "labels.toggleFrameRendering",
checked: (appState: AppState) => appState.shouldRenderFrames,
});
export const actionSetFrameAsActiveTool = register({
name: "setFrameAsActiveTool",
trackEvent: { category: "toolbar" },
perform: (elements, appState, _, app) => {
const nextActiveTool = updateActiveTool(appState, {
type: "frame",
});
setCursorForShape(app.canvas, {
...appState,
activeTool: nextActiveTool,
});
return {
elements,
appState: {
...appState,
activeTool: updateActiveTool(appState, {
type: "frame",
}),
},
commitToHistory: false,
};
},
keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] &&
!event.shiftKey &&
!event.altKey &&
event.key.toLocaleLowerCase() === KEYS.F,
});
+16 -70
View File
@@ -17,19 +17,9 @@ import {
import { getNonDeletedElements } from "../element";
import { randomId } from "../random";
import { ToolButton } from "../components/ToolButton";
import {
ExcalidrawElement,
ExcalidrawFrameElement,
ExcalidrawTextElement,
} from "../element/types";
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
import { AppState } from "../types";
import { isBoundToContainer } from "../element/typeChecks";
import {
getElementsInResizingFrame,
groupByFrames,
removeElementsFromFrame,
replaceAllElementsInFrame,
} from "../frame";
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
if (elements.length >= 2) {
@@ -55,9 +45,7 @@ const enableActionGroup = (
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
true,
);
return (
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
@@ -67,13 +55,11 @@ const enableActionGroup = (
export const actionGroup = register({
name: "group",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
true,
);
if (selectedElements.length < 2) {
// nothing to group
@@ -100,31 +86,9 @@ export const actionGroup = register({
return { appState, elements, commitToHistory: false };
}
}
let nextElements = [...elements];
// this includes the case where we are grouping elements inside a frame
// and elements outside that frame
const groupingElementsFromDifferentFrames =
new Set(selectedElements.map((element) => element.frameId)).size > 1;
// when it happens, we want to remove elements that are in the frame
// and are going to be grouped from the frame (mouthful, I know)
if (groupingElementsFromDifferentFrames) {
const frameElementsMap = groupByFrames(selectedElements);
frameElementsMap.forEach((elementsInFrame, frameId) => {
nextElements = removeElementsFromFrame(
nextElements,
elementsInFrame,
appState,
);
});
}
const newGroupId = randomId();
const selectElementIds = arrayToMap(selectedElements);
nextElements = nextElements.map((element) => {
const updatedElements = elements.map((element) => {
if (!selectElementIds.get(element.id)) {
return element;
}
@@ -138,16 +102,17 @@ export const actionGroup = register({
});
// keep the z order within the group the same, but move them
// to the z order of the highest element in the layer stack
const elementsInGroup = getElementsInGroup(nextElements, newGroupId);
const elementsInGroup = getElementsInGroup(updatedElements, newGroupId);
const lastElementInGroup = elementsInGroup[elementsInGroup.length - 1];
const lastGroupElementIndex = nextElements.lastIndexOf(lastElementInGroup);
const elementsAfterGroup = nextElements.slice(lastGroupElementIndex + 1);
const elementsBeforeGroup = nextElements
const lastGroupElementIndex =
updatedElements.lastIndexOf(lastElementInGroup);
const elementsAfterGroup = updatedElements.slice(lastGroupElementIndex + 1);
const elementsBeforeGroup = updatedElements
.slice(0, lastGroupElementIndex)
.filter(
(updatedElement) => !isElementInGroup(updatedElement, newGroupId),
);
nextElements = [
const updatedElementsInOrder = [
...elementsBeforeGroup,
...elementsInGroup,
...elementsAfterGroup,
@@ -157,9 +122,9 @@ export const actionGroup = register({
appState: selectGroup(
newGroupId,
{ ...appState, selectedGroupIds: {} },
getNonDeletedElements(nextElements),
getNonDeletedElements(updatedElementsInOrder),
),
elements: nextElements,
elements: updatedElementsInOrder,
commitToHistory: true,
};
},
@@ -183,23 +148,14 @@ export const actionGroup = register({
export const actionUngroup = register({
name: "ungroup",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
const groupIds = getSelectedGroupIds(appState);
if (groupIds.length === 0) {
return { appState, elements, commitToHistory: false };
}
let nextElements = [...elements];
const selectedElements = getSelectedElements(nextElements, appState);
const frames = selectedElements
.filter((element) => element.frameId)
.map((element) =>
app.scene.getElement(element.frameId!),
) as ExcalidrawFrameElement[];
const boundTextElementIds: ExcalidrawTextElement["id"][] = [];
nextElements = nextElements.map((element) => {
const nextElements = elements.map((element) => {
if (isBoundToContainer(element)) {
boundTextElementIds.push(element.id);
}
@@ -220,23 +176,13 @@ export const actionUngroup = register({
getNonDeletedElements(nextElements),
);
frames.forEach((frame) => {
if (frame) {
nextElements = replaceAllElementsInFrame(
nextElements,
getElementsInResizingFrame(nextElements, frame, appState),
frame,
appState,
);
}
});
// remove binded text elements from selection
boundTextElementIds.forEach(
(id) => (updateAppState.selectedElementIds[id] = false),
);
return {
appState: updateAppState,
elements: nextElements,
commitToHistory: true,
};
+2 -6
View File
@@ -21,9 +21,7 @@ export const actionToggleLinearEditor = register({
const selectedElement = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
true,
)[0] as ExcalidrawLinearElement;
const editingLinearElement =
@@ -42,9 +40,7 @@ export const actionToggleLinearEditor = register({
const selectedElement = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
true,
)[0] as ExcalidrawLinearElement;
return appState.editingLinearElement?.elementId === selectedElement.id
? "labels.lineEditor.exit"
+1
View File
@@ -67,6 +67,7 @@ export const actionFullScreen = register({
commitToHistory: false,
};
},
keyTest: (event) => event.key === KEYS.F && !event[KEYS.CTRL_OR_CMD],
});
export const actionShortcuts = register({
+4 -3
View File
@@ -1,4 +1,4 @@
import { getClientColor } from "../clients";
import { getClientColors } from "../clients";
import { Avatar } from "../components/Avatar";
import { centerScrollOn } from "../scene/scroll";
import { Collaborator } from "../types";
@@ -31,14 +31,15 @@ export const actionGoToCollaborator = register({
commitToHistory: false,
};
},
PanelComponent: ({ updateData, data }) => {
PanelComponent: ({ appState, updateData, data }) => {
const [clientId, collaborator] = data as [string, Collaborator];
const background = getClientColor(clientId);
const { background, stroke } = getClientColors(clientId, appState);
return (
<Avatar
color={background}
border={stroke}
onClick={() => updateData(collaborator.pointer)}
name={collaborator.username || ""}
src={collaborator.avatarUrl}
+1 -4
View File
@@ -102,11 +102,8 @@ const changeProperty = (
includeBoundText = false,
) => {
const selectedElementIds = arrayToMap(
getSelectedElements(elements, appState, {
includeBoundTextElement: includeBoundText,
}),
getSelectedElements(elements, appState, includeBoundText),
);
return elements.map((element) => {
if (
selectedElementIds.get(element.id) ||
+11 -11
View File
@@ -5,7 +5,6 @@ import { getNonDeletedElements, isTextElement } from "../element";
import { ExcalidrawElement } from "../element/types";
import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor";
import { excludeElementsInFramesFromSelection } from "../scene/selection";
export const actionSelectAll = register({
name: "selectAll",
@@ -14,18 +13,19 @@ export const actionSelectAll = register({
if (appState.editingLinearElement) {
return false;
}
const selectedElementIds = excludeElementsInFramesFromSelection(
elements.filter(
(element) =>
const selectedElementIds = elements.reduce(
(map: Record<ExcalidrawElement["id"], true>, element) => {
if (
!element.isDeleted &&
!(isTextElement(element) && element.containerId) &&
!element.locked,
),
).reduce((map: Record<ExcalidrawElement["id"], true>, element) => {
map[element.id] = true;
return map;
}, {});
!element.locked
) {
map[element.id] = true;
}
return map;
},
{},
);
return {
appState: selectGroupsForSelectedElements(
+1 -11
View File
@@ -20,7 +20,6 @@ import {
hasBoundTextElement,
canApplyRoundnessTypeToElement,
getDefaultRoundnessTypeForElement,
isFrameElement,
} from "../element/typeChecks";
import { getSelectedElements } from "../scene";
@@ -65,9 +64,7 @@ export const actionPasteStyles = register({
return { elements, commitToHistory: false };
}
const selectedElements = getSelectedElements(elements, appState, {
includeBoundTextElement: true,
});
const selectedElements = getSelectedElements(elements, appState, true);
const selectedElementIds = selectedElements.map((element) => element.id);
return {
elements: elements.map((element) => {
@@ -130,13 +127,6 @@ export const actionPasteStyles = register({
});
}
if (isFrameElement(element)) {
newElement = newElementWith(newElement, {
roundness: null,
backgroundColor: "transparent",
});
}
return newElement;
}
return element;
+11 -36
View File
@@ -2,10 +2,10 @@ import React from "react";
import {
Action,
UpdaterFn,
ActionName,
ActionResult,
PanelComponentProps,
ActionSource,
ActionPredicateFn,
} from "./types";
import { ExcalidrawElement } from "../element/types";
import { AppClassProperties, AppState } from "../types";
@@ -40,8 +40,7 @@ const trackAction = (
};
export class ActionManager {
actions = {} as Record<Action["name"], Action>;
actionPredicates = [] as ActionPredicateFn[];
actions = {} as Record<ActionName, Action>;
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
@@ -69,12 +68,6 @@ export class ActionManager {
this.app = app;
}
registerActionPredicate(predicate: ActionPredicateFn) {
if (!this.actionPredicates.includes(predicate)) {
this.actionPredicates.push(predicate);
}
}
registerAction(action: Action) {
this.actions[action.name] = action;
}
@@ -91,7 +84,7 @@ export class ActionManager {
(action) =>
(action.name in canvasActions
? canvasActions[action.name as keyof typeof canvasActions]
: this.isActionEnabled(action, { noPredicates: true })) &&
: true) &&
action.keyTest &&
action.keyTest(
event,
@@ -141,7 +134,7 @@ export class ActionManager {
/**
* @param data additional data sent to the PanelComponent
*/
renderAction = (name: Action["name"], data?: PanelComponentProps["data"]) => {
renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
const canvasActions = this.app.props.UIOptions.canvasActions;
if (
@@ -149,7 +142,7 @@ export class ActionManager {
"PanelComponent" in this.actions[name] &&
(name in canvasActions
? canvasActions[name as keyof typeof canvasActions]
: this.isActionEnabled(this.actions[name], { noPredicates: true }))
: true)
) {
const action = this.actions[name];
const PanelComponent = action.PanelComponent!;
@@ -183,31 +176,13 @@ export class ActionManager {
return null;
};
isActionEnabled = (
action: Action,
opts?: {
elements?: readonly ExcalidrawElement[];
data?: Record<string, any>;
noPredicates?: boolean;
},
): boolean => {
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
isActionEnabled = (action: Action) => {
const elements = this.getElementsIncludingDeleted();
const appState = this.getAppState();
const data = opts?.data;
if (
!opts?.noPredicates &&
action.predicate &&
!action.predicate(elements, appState, this.app.props, this.app, data)
) {
return false;
}
let enabled = true;
this.actionPredicates.forEach((fn) => {
if (!fn(action, elements, appState, data)) {
enabled = false;
}
});
return enabled;
return (
!action.predicate ||
action.predicate(elements, appState, this.app.props, this.app)
);
};
}
+1 -16
View File
@@ -32,15 +32,6 @@ type ActionFn = (
app: AppClassProperties,
) => ActionResult | Promise<ActionResult>;
// Return `true` *unless* `Action` should be disabled
// given `elements`, `appState`, and optionally `data`.
export type ActionPredicateFn = (
action: Action,
elements: readonly ExcalidrawElement[],
appState: AppState,
data?: Record<string, any>,
) => boolean;
export type UpdaterFn = (res: ActionResult) => void;
export type ActionFilterFn = (action: Action) => void;
@@ -125,11 +116,6 @@ export type ActionName =
| "toggleLinearEditor"
| "toggleEraserTool"
| "toggleHandTool"
| "selectAllElementsInFrame"
| "removeAllElementsFromFrame"
| "toggleFrameRendering"
| "setFrameAsActiveTool"
| "createContainerFromText"
| "wrapTextInContainer";
export type PanelComponentProps = {
@@ -141,7 +127,7 @@ export type PanelComponentProps = {
};
export interface Action {
name: string;
name: ActionName;
PanelComponent?: React.FC<PanelComponentProps>;
perform: ActionFn;
keyPriority?: number;
@@ -161,7 +147,6 @@ export interface Action {
appState: AppState,
appProps: ExcalidrawProps,
app: AppClassProperties,
data?: Record<string, any>,
) => boolean;
checked?: (appState: Readonly<AppState>) => boolean;
trackEvent:
+2 -2
View File
@@ -1,6 +1,6 @@
import { ExcalidrawElement } from "./element/types";
import { newElementWith } from "./element/mutateElement";
import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
import { Box, getCommonBoundingBox } from "./element/bounds";
import { getMaximumGroups } from "./groups";
export interface Alignment {
@@ -33,7 +33,7 @@ export const alignElements = (
const calculateTranslation = (
group: ExcalidrawElement[],
selectionBoundingBox: BoundingBox,
selectionBoundingBox: Box,
{ axis, position }: Alignment,
): { x: number; y: number } => {
const groupBoundingBox = getCommonBoundingBox(group);
-14
View File
@@ -78,16 +78,11 @@ export const getDefaultAppState = (): Omit<
scrollY: 0,
selectedElementIds: {},
selectedGroupIds: {},
selectedElementsAreBeingDragged: false,
selectionElement: null,
shouldCacheIgnoreZoom: false,
showStats: false,
startBoundElement: null,
suggestedBindings: [],
shouldRenderFrames: true,
frameToHighlight: null,
editingFrame: null,
elementsToHighlight: null,
toast: null,
viewBackgroundColor: COLOR_PALETTE.white,
zenModeEnabled: false,
@@ -181,20 +176,11 @@ const APP_STATE_STORAGE_CONF = (<
scrollY: { browser: true, export: false, server: false },
selectedElementIds: { browser: true, export: false, server: false },
selectedGroupIds: { browser: true, export: false, server: false },
selectedElementsAreBeingDragged: {
browser: false,
export: false,
server: false,
},
selectionElement: { browser: false, export: false, server: false },
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
showStats: { browser: true, export: false, server: false },
startBoundElement: { browser: false, export: false, server: false },
suggestedBindings: { browser: false, export: false, server: false },
shouldRenderFrames: { browser: false, export: false, server: false },
frameToHighlight: { browser: false, export: false, server: false },
editingFrame: { browser: false, export: false, server: false },
elementsToHighlight: { browser: false, export: false, server: false },
toast: { browser: false, export: false, server: false },
viewBackgroundColor: { browser: true, export: true, server: true },
width: { browser: false, export: false, server: false },
+26 -26
View File
@@ -1,31 +1,31 @@
function hashToInteger(id: string) {
let hash = 0;
if (id.length === 0) {
return hash;
}
for (let i = 0; i < id.length; i++) {
const char = id.charCodeAt(i);
hash = (hash << 5) - hash + char;
}
return hash;
}
import {
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
getAllColorsSpecificShade,
} from "./colors";
import { AppState } from "./types";
export const getClientColor = (
/**
* any uniquely identifying key, such as user id or socket id
*/
id: string,
) => {
// to get more even distribution in case `id` is not uniformly distributed to
// begin with, we hash it
const hash = Math.abs(hashToInteger(id));
// we want to get a multiple of 10 number in the range of 0-360 (in other
// words a hue value of step size 10). There are 37 such values including 0.
const hue = (hash % 37) * 10;
const saturation = 100;
const lightness = 83;
const BG_COLORS = getAllColorsSpecificShade(
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
);
const STROKE_COLORS = getAllColorsSpecificShade(
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
);
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
export const getClientColors = (clientId: string, appState: AppState) => {
if (appState?.collaborators) {
const currentUser = appState.collaborators.get(clientId);
if (currentUser?.color) {
return currentUser.color;
}
}
// Naive way of getting an integer out of the clientId
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
return {
background: BG_COLORS[sum % BG_COLORS.length],
stroke: STROKE_COLORS[sum % STROKE_COLORS.length],
};
};
/**
+1 -20
View File
@@ -7,9 +7,6 @@ import { SVG_EXPORT_TAG } from "./scene/export";
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
import { isInitializedImageElement } from "./element/typeChecks";
import { deepCopyElement } from "./element/newElement";
import { mutateElement } from "./element/mutateElement";
import { getContainingFrame } from "./frame";
import { isPromiseLike, isTestEnv } from "./utils";
type ElementsClipboard = {
@@ -60,9 +57,6 @@ export const copyToClipboard = async (
elements: readonly NonDeletedExcalidrawElement[],
files: BinaryFiles | null,
) => {
const framesToCopy = new Set(
elements.filter((element) => element.type === "frame"),
);
let foundFile = false;
const _files = elements.reduce((acc, element) => {
@@ -84,20 +78,7 @@ export const copyToClipboard = async (
// select binded text elements when copying
const contents: ElementsClipboard = {
type: EXPORT_DATA_TYPES.excalidrawClipboard,
elements: elements.map((element) => {
if (
getContainingFrame(element) &&
!framesToCopy.has(getContainingFrame(element)!)
) {
const copiedElement = deepCopyElement(element);
mutateElement(copiedElement, {
frameId: null,
});
return copiedElement;
}
return element;
}),
elements,
files: files ? _files : undefined,
};
const json = JSON.stringify(contents);
+36 -114
View File
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
import { ActionManager } from "../actions/manager";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement, PointerType } from "../element/types";
@@ -35,9 +35,6 @@ import {
} from "../element/textElement";
import "./Actions.scss";
import DropdownMenu from "./dropdownMenu/DropdownMenu";
import { extraToolsIcon, frameToolIcon } from "./icons";
import { KEYS } from "../keys";
export const SelectedShapeActions = ({
appState,
@@ -92,8 +89,7 @@ export const SelectedShapeActions = ({
<div>
{((hasStrokeColor(appState.activeTool.type) &&
appState.activeTool.type !== "image" &&
commonSelectedType !== "image" &&
commonSelectedType !== "frame") ||
commonSelectedType !== "image") ||
targetElements.some((element) => hasStrokeColor(element.type))) &&
renderAction("changeStrokeColor")}
</div>
@@ -224,78 +220,28 @@ export const ShapesSwitcher = ({
setAppState: React.Component<any, UIAppState>["setState"];
onImageAction: (data: { pointerType: PointerType | null }) => void;
appState: UIAppState;
}) => {
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
const device = useDevice();
return (
<>
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
const label = t(`toolBar.${value}`);
const letter =
key && capitalizeString(typeof key === "string" ? key : key[0]);
const shortcut = letter
? `${letter} ${t("helpDialog.or")} ${numericKey}`
: `${numericKey}`;
return (
<ToolButton
className={clsx("Shape", { fillable })}
key={value}
type="radio"
icon={icon}
checked={activeTool.type === value}
name="editor-current-shape"
title={`${capitalizeString(label)}${shortcut}`}
keyBindingLabel={numericKey || letter}
aria-label={capitalizeString(label)}
aria-keyshortcuts={shortcut}
data-testid={`toolbar-${value}`}
onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") {
setAppState({
penDetected: true,
penMode: true,
});
}
}}
onChange={({ pointerType }) => {
if (appState.activeTool.type !== value) {
trackEvent("toolbar", value, "ui");
}
const nextActiveTool = updateActiveTool(appState, {
type: value,
});
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
});
setCursorForShape(canvas, {
...appState,
activeTool: nextActiveTool,
});
if (value === "image") {
onImageAction({ pointerType });
}
}}
/>
);
})}
<div className="App-toolbar__divider" />
{/* TEMP HACK because dropdown doesn't work well inside mobile toolbar */}
{device.isMobile ? (
}) => (
<>
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
const label = t(`toolBar.${value}`);
const letter =
key && capitalizeString(typeof key === "string" ? key : key[0]);
const shortcut = letter
? `${letter} ${t("helpDialog.or")} ${numericKey}`
: `${numericKey}`;
return (
<ToolButton
className={clsx("Shape", { fillable: false })}
className={clsx("Shape", { fillable })}
key={value}
type="radio"
icon={frameToolIcon}
checked={activeTool.type === "frame"}
icon={icon}
checked={activeTool.type === value}
name="editor-current-shape"
title={`${capitalizeString(
t("toolBar.frame"),
)}${KEYS.F.toLocaleUpperCase()}`}
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
aria-label={capitalizeString(t("toolBar.frame"))}
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
data-testid={`toolbar-frame`}
title={`${capitalizeString(label)}${shortcut}`}
keyBindingLabel={numericKey || letter}
aria-label={capitalizeString(label)}
aria-keyshortcuts={shortcut}
data-testid={`toolbar-${value}`}
onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") {
setAppState({
@@ -305,54 +251,30 @@ export const ShapesSwitcher = ({
}
}}
onChange={({ pointerType }) => {
trackEvent("toolbar", "frame", "ui");
if (appState.activeTool.type !== value) {
trackEvent("toolbar", value, "ui");
}
const nextActiveTool = updateActiveTool(appState, {
type: "frame",
type: value,
});
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
});
setCursorForShape(canvas, {
...appState,
activeTool: nextActiveTool,
});
if (value === "image") {
onImageAction({ pointerType });
}
}}
/>
) : (
<DropdownMenu open={isExtraToolsMenuOpen}>
<DropdownMenu.Trigger
className="App-toolbar__extra-tools-trigger"
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
title={t("toolBar.extraTools")}
>
{extraToolsIcon}
</DropdownMenu.Trigger>
<DropdownMenu.Content
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
onSelect={() => setIsExtraToolsMenuOpen(false)}
className="App-toolbar__extra-tools-dropdown"
>
<DropdownMenu.Item
onSelect={() => {
const nextActiveTool = updateActiveTool(appState, {
type: "frame",
});
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
});
}}
icon={frameToolIcon}
shortcut={KEYS.F.toLocaleUpperCase()}
data-testid="toolbar-frame"
>
{t("toolBar.frame")}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
)}
</>
);
};
);
})}
</>
);
export const ZoomActions = ({
renderAction,
+134 -904
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -10,9 +10,10 @@
display: flex;
justify-content: center;
align-items: center;
color: $oc-white;
cursor: pointer;
font-size: 0.75rem;
font-weight: 800;
font-size: 0.625rem;
font-weight: 500;
line-height: 1;
&-img {
+1
View File
@@ -6,6 +6,7 @@ import { getNameInitial } from "../clients";
type AvatarProps = {
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
color: string;
border: string;
name: string;
src?: string;
};
-1
View File
@@ -164,7 +164,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("toolBar.eraser")}
shortcuts={[KEYS.E, KEYS["0"]]}
/>
<Shortcut label={t("toolBar.frame")} shortcuts={[KEYS.F]} />
<Shortcut
label={t("labels.eyeDropper")}
shortcuts={[KEYS.I, "Shift+S", "Shift+G"]}
+1 -4
View File
@@ -84,10 +84,7 @@ const ImageExportModal = ({
const [renderError, setRenderError] = useState<Error | null>(null);
const exportedElements = exportSelected
? getSelectedElements(elements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
})
? getSelectedElements(elements, appState, true)
: elements;
useEffect(() => {
+7 -2
View File
@@ -204,7 +204,12 @@ const LayerUI = ({
return (
<FixedSideContainer side="top">
<div className="App-menu App-menu_top">
<Stack.Col gap={6} className={clsx("App-menu_top__left")}>
<Stack.Col
gap={6}
className={clsx("App-menu_top__left", {
"disable-pointerEvents": appState.zenModeEnabled,
})}
>
{renderCanvasActions()}
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
</Stack.Col>
@@ -249,7 +254,7 @@ const LayerUI = ({
title={t("toolBar.lock")}
/>
<div className="App-toolbar__divider" />
<div className="App-toolbar__divider"></div>
<HandButton
checked={isHandToolActive(appState)}
+1 -5
View File
@@ -148,11 +148,7 @@ const usePendingElementsMemo = (
appState: UIAppState,
elements: readonly NonDeletedExcalidrawElement[],
) => {
const create = () =>
getSelectedElements(elements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
});
const create = () => getSelectedElements(elements, appState, true);
const val = useRef(create());
const prevAppState = useRef<UIAppState>(appState);
const prevElements = useRef(elements);
+1 -3
View File
@@ -1,6 +1,6 @@
import "./ToolIcon.scss";
import React, { CSSProperties, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import clsx from "clsx";
import { useExcalidrawContainer } from "./App";
import { AbortError } from "../errors";
@@ -25,7 +25,6 @@ type ToolButtonBaseProps = {
visible?: boolean;
selected?: boolean;
className?: string;
style?: CSSProperties;
isLoading?: boolean;
};
@@ -115,7 +114,6 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
"ToolIcon--plain": props.type === "icon",
},
)}
style={props.style}
data-testid={props["data-testid"]}
hidden={props.hidden}
title={props.title}
+1 -18
View File
@@ -15,24 +15,7 @@
height: 1.5rem;
align-self: center;
background-color: var(--default-border-color);
margin: 0 0.25rem;
margin: 0 0.5rem;
}
}
.App-toolbar__extra-tools-trigger {
box-shadow: none;
border: 0;
&:active {
background-color: var(--button-hover-bg);
box-shadow: 0 0 0 1px
var(--button-active-border, var(--color-primary-darkest)) inset;
}
}
.App-toolbar__extra-tools-dropdown {
margin-top: 0.375rem;
right: 0;
min-width: 11.875rem;
}
}
@@ -1,23 +1,23 @@
import clsx from "clsx";
import { useUIAppState } from "../../context/ui-appState";
import { useDevice } from "../App";
const MenuTrigger = ({
className = "",
children,
onToggle,
title,
...rest
}: {
className?: string;
children: React.ReactNode;
onToggle: () => void;
title?: string;
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
}) => {
const appState = useUIAppState();
const device = useDevice();
const classNames = clsx(
`dropdown-menu-button ${className}`,
"zen-mode-transition",
{
"transition-left": appState.zenModeEnabled,
"dropdown-menu-button--mobile": device.isMobile,
},
).trim();
@@ -28,8 +28,6 @@ const MenuTrigger = ({
onClick={onToggle}
type="button"
data-testid="dropdown-menu-button"
title={title}
{...rest}
>
{children}
</button>
@@ -12,17 +12,17 @@ describe("Test internal component fallback rendering", () => {
</div>,
);
expect(queryAllByTestId(container, "main-menu-trigger")?.length).toBe(2);
expect(queryAllByTestId(container, "dropdown-menu-button")?.length).toBe(2);
const excalContainers = container.querySelectorAll<HTMLDivElement>(
".excalidraw-container",
);
expect(
queryAllByTestId(excalContainers[0], "main-menu-trigger")?.length,
queryAllByTestId(excalContainers[0], "dropdown-menu-button")?.length,
).toBe(1);
expect(
queryAllByTestId(excalContainers[1], "main-menu-trigger")?.length,
queryAllByTestId(excalContainers[1], "dropdown-menu-button")?.length,
).toBe(1);
});
@@ -36,17 +36,17 @@ describe("Test internal component fallback rendering", () => {
</div>,
);
expect(queryAllByTestId(container, "main-menu-trigger")?.length).toBe(2);
expect(queryAllByTestId(container, "dropdown-menu-button")?.length).toBe(2);
const excalContainers = container.querySelectorAll<HTMLDivElement>(
".excalidraw-container",
);
expect(
queryAllByTestId(excalContainers[0], "main-menu-trigger")?.length,
queryAllByTestId(excalContainers[0], "dropdown-menu-button")?.length,
).toBe(1);
expect(
queryAllByTestId(excalContainers[1], "main-menu-trigger")?.length,
queryAllByTestId(excalContainers[1], "dropdown-menu-button")?.length,
).toBe(1);
});
@@ -62,17 +62,17 @@ describe("Test internal component fallback rendering", () => {
</div>,
);
expect(queryAllByTestId(container, "main-menu-trigger")?.length).toBe(2);
expect(queryAllByTestId(container, "dropdown-menu-button")?.length).toBe(2);
const excalContainers = container.querySelectorAll<HTMLDivElement>(
".excalidraw-container",
);
expect(
queryAllByTestId(excalContainers[0], "main-menu-trigger")?.length,
queryAllByTestId(excalContainers[0], "dropdown-menu-button")?.length,
).toBe(1);
expect(
queryAllByTestId(excalContainers[1], "main-menu-trigger")?.length,
queryAllByTestId(excalContainers[1], "dropdown-menu-button")?.length,
).toBe(1);
});
@@ -84,17 +84,17 @@ describe("Test internal component fallback rendering", () => {
</div>,
);
expect(queryAllByTestId(container, "main-menu-trigger")?.length).toBe(2);
expect(queryAllByTestId(container, "dropdown-menu-button")?.length).toBe(2);
const excalContainers = container.querySelectorAll<HTMLDivElement>(
".excalidraw-container",
);
expect(
queryAllByTestId(excalContainers[0], "main-menu-trigger")?.length,
queryAllByTestId(excalContainers[0], "dropdown-menu-button")?.length,
).toBe(1);
expect(
queryAllByTestId(excalContainers[1], "main-menu-trigger")?.length,
queryAllByTestId(excalContainers[1], "dropdown-menu-button")?.length,
).toBe(1);
});
});
-21
View File
@@ -1616,24 +1616,3 @@ export const eyeDropperIcon = createIcon(
</g>,
tablerIconProps,
);
export const extraToolsIcon = createIcon(
<g strokeWidth={1.5}>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 3l-4 7h8z"></path>
<path d="M17 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
<path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path>
</g>,
tablerIconProps,
);
export const frameToolIcon = createIcon(
<g strokeWidth={1.5}>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 7l16 0"></path>
<path d="M4 17l16 0"></path>
<path d="M7 4l0 16"></path>
<path d="M17 4l0 16"></path>
</g>,
tablerIconProps,
);
-1
View File
@@ -42,7 +42,6 @@ const MainMenu = Object.assign(
openMenu: appState.openMenu === "canvas" ? null : "canvas",
});
}}
data-testid="main-menu-trigger"
>
{HamburgerMenuIcon}
</DropdownMenu.Trigger>
-11
View File
@@ -94,17 +94,6 @@ export const THEME = {
DARK: "dark",
};
export const FRAME_STYLE = {
strokeColor: "#bbb" as ExcalidrawElement["strokeColor"],
strokeWidth: 1 as ExcalidrawElement["strokeWidth"],
strokeStyle: "solid" as ExcalidrawElement["strokeStyle"],
fillStyle: "solid" as ExcalidrawElement["fillStyle"],
roughness: 0 as ExcalidrawElement["roughness"],
roundness: null as ExcalidrawElement["roundness"],
backgroundColor: "transparent" as ExcalidrawElement["backgroundColor"],
radius: 8,
};
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
export const DEFAULT_FONT_SIZE = 20;
+82 -3
View File
@@ -13,6 +13,7 @@ import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
import { isValidExcalidrawData, isValidLibrary } from "./json";
import { restore, restoreLibraryItems } from "./restore";
import { ImportedLibraryData } from "./types";
import { PNG } from "pngjs/browser";
const parseFileContents = async (blob: Blob | File) => {
let contents: string;
@@ -210,9 +211,7 @@ export const loadLibraryFromBlob = async (
return parseLibraryJSON(await parseFileContents(blob), defaultStatus);
};
export const canvasToBlob = async (
canvas: HTMLCanvasElement,
): Promise<Blob> => {
const _canvasToBlob = async (canvas: HTMLCanvasElement): Promise<Blob> => {
return new Promise((resolve, reject) => {
try {
canvas.toBlob((blob) => {
@@ -232,6 +231,86 @@ export const canvasToBlob = async (
});
};
export const canvasToBlob = async (
canvas: HTMLCanvasElement,
): Promise<Blob> => {
const tileWidth = 1000;
const tileHeight = 1000;
const tileDataArray: Uint8ClampedArray[][] = []; // Two-dimensional array to store tile data
const { width: canvasWidth, height: canvasHeight } = canvas;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("No canvas context");
}
// Function to process each tile
function processTile(tileX: number, tileY: number) {
// Calculate the starting and ending coordinates for the tile
const startX = tileX * tileWidth;
const startY = tileY * tileHeight;
const endX = Math.min(startX + tileWidth, canvasWidth);
const endY = Math.min(startY + tileHeight, canvasHeight);
// Get the image data for the tile directly from the main canvas
const imageData = ctx!.getImageData(
startX,
startY,
endX - startX,
endY - startY,
).data;
// Store the tile data in the two-dimensional array
tileDataArray[tileY] = tileDataArray[tileY] || [];
tileDataArray[tileY][tileX] = imageData;
}
console.time("tiling");
// Iterate over the tiles and process each one
for (let tileY = 0; tileY < canvasHeight / tileHeight; tileY++) {
for (let tileX = 0; tileX < canvasWidth / tileWidth; tileX++) {
processTile(tileX, tileY);
}
}
console.timeEnd("tiling");
console.time("create png");
// Create a new PNG image with the final dimensions
const finalImage = new PNG({ width: canvasWidth, height: canvasHeight });
console.timeEnd("create png");
console.time("concat tiles");
// Merge the tiles into the final image
for (let tileY = 0; tileY < canvasHeight / tileHeight; tileY++) {
for (let tileX = 0; tileX < canvasWidth / tileWidth; tileX++) {
const imageData = tileDataArray[tileY][tileX];
const destX = tileX * tileWidth;
const destY = tileY * tileHeight;
// Copy the pixels from the tile to the final image
for (let y = 0; y < tileHeight; y++) {
for (let x = 0; x < tileWidth; x++) {
const index = (y * tileWidth + x) * 4;
const destIndex = ((destY + y) * canvasWidth + destX + x) * 4;
finalImage.data[destIndex] = imageData[index];
finalImage.data[destIndex + 1] = imageData[index + 1];
finalImage.data[destIndex + 2] = imageData[index + 2];
finalImage.data[destIndex + 3] = imageData[index + 3];
}
}
}
}
console.timeEnd("concat tiles");
console.time("create buffer");
const buffer = PNG.sync.write(finalImage);
console.timeEnd("create buffer");
return new Blob([buffer], { type: "image/png" });
};
/** generates SHA-1 digest from supplied file (if not supported, falls back
to a 40-char base64 random id) */
export const generateIdFromFile = async (file: File): Promise<FileId> => {
+2
View File
@@ -75,7 +75,9 @@ export const exportCanvas = async (
document.body.appendChild(tempCanvas);
if (type === "png") {
console.time("export png");
let blob = await canvasToBlob(tempCanvas);
console.timeEnd("export png");
tempCanvas.remove();
if (appState.exportEmbedScene) {
blob = await (
-6
View File
@@ -62,7 +62,6 @@ export const AllowedExcalidrawActiveTools: Record<
freedraw: true,
eraser: false,
custom: true,
frame: true,
hand: true,
};
@@ -126,7 +125,6 @@ const restoreElementWithProperties = <
height: element.height || 0,
seed: element.seed ?? 1,
groupIds: element.groupIds ?? [],
frameId: element.frameId ?? null,
roundness: element.roundness
? element.roundness
: element.strokeSharpness === "round"
@@ -274,10 +272,6 @@ const restoreElement = (
return restoreElementWithProperties(element, {});
case "diamond":
return restoreElementWithProperties(element, {});
case "frame":
return restoreElementWithProperties(element, {
name: element.name ?? null,
});
// Don't use default case so as to catch a missing an element type case.
// We also don't want to throw, but instead return void so we filter
+2 -4
View File
@@ -344,7 +344,7 @@ export const isPointHittingLinkIcon = (
if (
!isMobile &&
appState.viewModeEnabled &&
isPointHittingElementBoundingBox(element, [x, y], threshold, null)
isPointHittingElementBoundingBox(element, [x, y], threshold)
) {
return true;
}
@@ -440,9 +440,7 @@ export const shouldHideLinkPopup = (
const threshold = 15 / appState.zoom.value;
// hitbox to prevent hiding when hovered in element bounding box
if (
isPointHittingElementBoundingBox(element, [sceneX, sceneY], threshold, null)
) {
if (isPointHittingElementBoundingBox(element, [sceneX, sceneY], threshold)) {
return false;
}
const [x1, y1, x2] = getElementAbsoluteCoords(element);
+1 -1
View File
@@ -39,7 +39,7 @@ export type SuggestedPointBinding = [
];
export const shouldEnableBindingForPointerEvent = (
event: React.PointerEvent<HTMLElement>,
event: React.PointerEvent<HTMLCanvasElement>,
) => {
return !event[KEYS.CTRL_OR_CMD];
};
+61 -205
View File
@@ -6,7 +6,7 @@ import {
NonDeleted,
ExcalidrawTextElementWithContainer,
} from "./types";
import { distance2d, rotate, rotatePoint } from "../math";
import { distance2d, rotate } from "../math";
import rough from "roughjs/bin/rough";
import { Drawable, Op } from "roughjs/bin/core";
import { Point } from "../types";
@@ -25,101 +25,10 @@ import { getBoundTextElement, getContainerElement } from "./textElement";
import { LinearElementEditor } from "./linearElementEditor";
import { Mutable } from "../utility-types";
export type RectangleBox = {
x: number;
y: number;
width: number;
height: number;
angle: number;
};
// x and y position of top left corner, x and y position of bottom right corner
export type Bounds = readonly [number, number, number, number];
type MaybeQuadraticSolution = [number | null, number | null] | false;
// x and y position of top left corner, x and y position of bottom right corner
export type Bounds = readonly [x1: number, y1: number, x2: number, y2: number];
export class ElementBounds {
private static boundsCache = new WeakMap<
ExcalidrawElement,
{
bounds: Bounds;
version: ExcalidrawElement["version"];
}
>();
static getBounds(element: ExcalidrawElement) {
const cachedBounds = ElementBounds.boundsCache.get(element);
if (cachedBounds?.version && cachedBounds.version === element.version) {
return cachedBounds.bounds;
}
const bounds = ElementBounds.calculateBounds(element);
ElementBounds.boundsCache.set(element, {
version: element.version,
bounds,
});
return bounds;
}
private static calculateBounds(element: ExcalidrawElement): Bounds {
let bounds: [number, number, number, number];
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
if (isFreeDrawElement(element)) {
const [minX, minY, maxX, maxY] = getBoundsFromPoints(
element.points.map(([x, y]) =>
rotate(x, y, cx - element.x, cy - element.y, element.angle),
),
);
return [
minX + element.x,
minY + element.y,
maxX + element.x,
maxY + element.y,
];
} else if (isLinearElement(element)) {
bounds = getLinearElementRotatedBounds(element, cx, cy);
} else if (element.type === "diamond") {
const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
const [x22, y22] = rotate(x1, cy, cx, cy, element.angle);
const [x21, y21] = rotate(x2, cy, cx, cy, element.angle);
const minX = Math.min(x11, x12, x22, x21);
const minY = Math.min(y11, y12, y22, y21);
const maxX = Math.max(x11, x12, x22, x21);
const maxY = Math.max(y11, y12, y22, y21);
bounds = [minX, minY, maxX, maxY];
} else if (element.type === "ellipse") {
const w = (x2 - x1) / 2;
const h = (y2 - y1) / 2;
const cos = Math.cos(element.angle);
const sin = Math.sin(element.angle);
const ww = Math.hypot(w * cos, h * sin);
const hh = Math.hypot(h * cos, w * sin);
bounds = [cx - ww, cy - hh, cx + ww, cy + hh];
} else {
const [x11, y11] = rotate(x1, y1, cx, cy, element.angle);
const [x12, y12] = rotate(x1, y2, cx, cy, element.angle);
const [x22, y22] = rotate(x2, y2, cx, cy, element.angle);
const [x21, y21] = rotate(x2, y1, cx, cy, element.angle);
const minX = Math.min(x11, x12, x22, x21);
const minY = Math.min(y11, y12, y22, y21);
const maxX = Math.max(x11, x12, x22, x21);
const maxY = Math.max(y11, y12, y22, y21);
bounds = [minX, minY, maxX, maxY];
}
return bounds;
}
}
// Scene -> Scene coords, but in x1,x2,y1,y2 format.
//
// If the element is created from right to left, the width is going to be negative
// This set of functions retrieves the absolute position of the 4 points.
export const getElementAbsoluteCoords = (
@@ -160,111 +69,6 @@ export const getElementAbsoluteCoords = (
];
};
/**
* for a given element, `getElementLineSegments` returns line segments
* that can be used for visual collision detection (useful for frames)
* as opposed to bounding box collision detection
*/
export const getElementLineSegments = (
element: ExcalidrawElement,
): [Point, Point][] => {
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
const center: Point = [cx, cy];
if (isLinearElement(element) || isFreeDrawElement(element)) {
const segments: [Point, Point][] = [];
let i = 0;
while (i < element.points.length - 1) {
segments.push([
rotatePoint(
[
element.points[i][0] + element.x,
element.points[i][1] + element.y,
] as Point,
center,
element.angle,
),
rotatePoint(
[
element.points[i + 1][0] + element.x,
element.points[i + 1][1] + element.y,
] as Point,
center,
element.angle,
),
]);
i++;
}
return segments;
}
const [nw, ne, sw, se, n, s, w, e] = (
[
[x1, y1],
[x2, y1],
[x1, y2],
[x2, y2],
[cx, y1],
[cx, y2],
[x1, cy],
[x2, cy],
] as Point[]
).map((point) => rotatePoint(point, center, element.angle));
if (element.type === "diamond") {
return [
[n, w],
[n, e],
[s, w],
[s, e],
];
}
if (element.type === "ellipse") {
return [
[n, w],
[n, e],
[s, w],
[s, e],
[n, w],
[n, e],
[s, w],
[s, e],
];
}
return [
[nw, ne],
[sw, se],
[nw, sw],
[ne, se],
[nw, e],
[sw, e],
[ne, w],
[se, w],
];
};
/**
* Scene -> Scene coords, but in x1,x2,y1,y2 format.
*
* Rectangle here means any rectangular frame, not an excalidraw element.
*/
export const getRectangleBoxAbsoluteCoords = (boxSceneCoords: RectangleBox) => {
return [
boxSceneCoords.x,
boxSceneCoords.y,
boxSceneCoords.x + boxSceneCoords.width,
boxSceneCoords.y + boxSceneCoords.height,
boxSceneCoords.x + boxSceneCoords.width / 2,
boxSceneCoords.y + boxSceneCoords.height / 2,
];
};
export const pointRelativeTo = (
element: ExcalidrawElement,
absoluteCoords: Point,
@@ -650,12 +454,64 @@ const getLinearElementRotatedBounds = (
return coords;
};
export const getElementBounds = (element: ExcalidrawElement): Bounds => {
return ElementBounds.getBounds(element);
// We could cache this stuff
export const getElementBounds = (
element: ExcalidrawElement,
): [number, number, number, number] => {
let bounds: [number, number, number, number];
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
if (isFreeDrawElement(element)) {
const [minX, minY, maxX, maxY] = getBoundsFromPoints(
element.points.map(([x, y]) =>
rotate(x, y, cx - element.x, cy - element.y, element.angle),
),
);
return [
minX + element.x,
minY + element.y,
maxX + element.x,
maxY + element.y,
];
} else if (isLinearElement(element)) {
bounds = getLinearElementRotatedBounds(element, cx, cy);
} else if (element.type === "diamond") {
const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
const [x22, y22] = rotate(x1, cy, cx, cy, element.angle);
const [x21, y21] = rotate(x2, cy, cx, cy, element.angle);
const minX = Math.min(x11, x12, x22, x21);
const minY = Math.min(y11, y12, y22, y21);
const maxX = Math.max(x11, x12, x22, x21);
const maxY = Math.max(y11, y12, y22, y21);
bounds = [minX, minY, maxX, maxY];
} else if (element.type === "ellipse") {
const w = (x2 - x1) / 2;
const h = (y2 - y1) / 2;
const cos = Math.cos(element.angle);
const sin = Math.sin(element.angle);
const ww = Math.hypot(w * cos, h * sin);
const hh = Math.hypot(h * cos, w * sin);
bounds = [cx - ww, cy - hh, cx + ww, cy + hh];
} else {
const [x11, y11] = rotate(x1, y1, cx, cy, element.angle);
const [x12, y12] = rotate(x1, y2, cx, cy, element.angle);
const [x22, y22] = rotate(x2, y2, cx, cy, element.angle);
const [x21, y21] = rotate(x2, y1, cx, cy, element.angle);
const minX = Math.min(x11, x12, x22, x21);
const minY = Math.min(y11, y12, y22, y21);
const maxX = Math.max(x11, x12, x22, x21);
const maxY = Math.max(y11, y12, y22, y21);
bounds = [minX, minY, maxX, maxY];
}
return bounds;
};
export const getCommonBounds = (
elements: readonly ExcalidrawElement[],
): Bounds => {
): [number, number, number, number] => {
if (!elements.length) {
return [0, 0, 0, 0];
}
@@ -752,7 +608,7 @@ export const getElementPointsCoords = (
export const getClosestElementBounds = (
elements: readonly ExcalidrawElement[],
from: { x: number; y: number },
): Bounds => {
): [number, number, number, number] => {
if (!elements.length) {
return [0, 0, 0, 0];
}
@@ -773,7 +629,7 @@ export const getClosestElementBounds = (
return getElementBounds(closestElement);
};
export interface BoundingBox {
export interface Box {
minX: number;
minY: number;
maxX: number;
@@ -786,7 +642,7 @@ export interface BoundingBox {
export const getCommonBoundingBox = (
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
): BoundingBox => {
): Box => {
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
return {
minX,
+22 -146
View File
@@ -26,16 +26,10 @@ import {
ExcalidrawImageElement,
ExcalidrawLinearElement,
StrokeRoundness,
ExcalidrawFrameElement,
} from "./types";
import {
getElementAbsoluteCoords,
getCurvePathOps,
getRectangleBoxAbsoluteCoords,
RectangleBox,
} from "./bounds";
import { FrameNameBoundsCache, Point } from "../types";
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
import { Point } from "../types";
import { Drawable } from "roughjs/bin/core";
import { AppState } from "../types";
import { getShapeForElement } from "../renderer/renderElement";
@@ -67,7 +61,6 @@ const isElementDraggableFromInside = (
export const hitTest = (
element: NonDeletedExcalidrawElement,
appState: AppState,
frameNameBoundsCache: FrameNameBoundsCache,
x: number,
y: number,
): boolean => {
@@ -79,39 +72,22 @@ export const hitTest = (
isElementSelected(appState, element) &&
shouldShowBoundingBox([element], appState)
) {
return isPointHittingElementBoundingBox(
element,
point,
threshold,
frameNameBoundsCache,
);
return isPointHittingElementBoundingBox(element, point, threshold);
}
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
const isHittingBoundTextElement = hitTest(
boundTextElement,
appState,
frameNameBoundsCache,
x,
y,
);
const isHittingBoundTextElement = hitTest(boundTextElement, appState, x, y);
if (isHittingBoundTextElement) {
return true;
}
}
return isHittingElementNotConsideringBoundingBox(
element,
appState,
frameNameBoundsCache,
point,
);
return isHittingElementNotConsideringBoundingBox(element, appState, point);
};
export const isHittingElementBoundingBoxWithoutHittingElement = (
element: NonDeletedExcalidrawElement,
appState: AppState,
frameNameBoundsCache: FrameNameBoundsCache,
x: number,
y: number,
): boolean => {
@@ -120,33 +96,19 @@ export const isHittingElementBoundingBoxWithoutHittingElement = (
// So that bound text element hit is considered within bounding box of container even if its outside actual bounding box of element
// eg for linear elements text can be outside the element bounding box
const boundTextElement = getBoundTextElement(element);
if (
boundTextElement &&
hitTest(boundTextElement, appState, frameNameBoundsCache, x, y)
) {
if (boundTextElement && hitTest(boundTextElement, appState, x, y)) {
return false;
}
return (
!isHittingElementNotConsideringBoundingBox(
element,
appState,
frameNameBoundsCache,
[x, y],
) &&
isPointHittingElementBoundingBox(
element,
[x, y],
threshold,
frameNameBoundsCache,
)
!isHittingElementNotConsideringBoundingBox(element, appState, [x, y]) &&
isPointHittingElementBoundingBox(element, [x, y], threshold)
);
};
export const isHittingElementNotConsideringBoundingBox = (
element: NonDeletedExcalidrawElement,
appState: AppState,
frameNameBoundsCache: FrameNameBoundsCache | null,
point: Point,
): boolean => {
const threshold = 10 / appState.zoom.value;
@@ -155,13 +117,7 @@ export const isHittingElementNotConsideringBoundingBox = (
: isElementDraggableFromInside(element)
? isInsideCheck
: isNearCheck;
return hitTestPointAgainstElement({
element,
point,
threshold,
check,
frameNameBoundsCache,
});
return hitTestPointAgainstElement({ element, point, threshold, check });
};
const isElementSelected = (
@@ -173,22 +129,7 @@ export const isPointHittingElementBoundingBox = (
element: NonDeleted<ExcalidrawElement>,
[x, y]: Point,
threshold: number,
frameNameBoundsCache: FrameNameBoundsCache | null,
) => {
// frames needs be checked differently so as to be able to drag it
// by its frame, whether it has been selected or not
// this logic here is not ideal
// TODO: refactor it later...
if (element.type === "frame") {
return hitTestPointAgainstElement({
element,
point: [x, y],
threshold,
check: isInsideCheck,
frameNameBoundsCache,
});
}
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const elementCenterX = (x1 + x2) / 2;
const elementCenterY = (y1 + y2) / 2;
@@ -216,13 +157,7 @@ export const bindingBorderTest = (
const threshold = maxBindingGap(element, element.width, element.height);
const check = isOutsideCheck;
const point: Point = [x, y];
return hitTestPointAgainstElement({
element,
point,
threshold,
check,
frameNameBoundsCache: null,
});
return hitTestPointAgainstElement({ element, point, threshold, check });
};
export const maxBindingGap = (
@@ -242,7 +177,6 @@ type HitTestArgs = {
point: Point;
threshold: number;
check: (distance: number, threshold: number) => boolean;
frameNameBoundsCache: FrameNameBoundsCache | null;
};
const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
@@ -274,27 +208,6 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
"This should not happen, we need to investigate why it does.",
);
return false;
case "frame": {
// check distance to frame element first
if (
args.check(
distanceToBindableElement(args.element, args.point),
args.threshold,
)
) {
return true;
}
const frameNameBounds = args.frameNameBoundsCache?.get(args.element);
if (frameNameBounds) {
return args.check(
distanceToRectangleBox(frameNameBounds, args.point),
args.threshold,
);
}
return false;
}
}
};
@@ -306,7 +219,6 @@ export const distanceToBindableElement = (
case "rectangle":
case "image":
case "text":
case "frame":
return distanceToRectangle(element, point);
case "diamond":
return distanceToDiamond(element, point);
@@ -336,8 +248,7 @@ const distanceToRectangle = (
| ExcalidrawRectangleElement
| ExcalidrawTextElement
| ExcalidrawFreeDrawElement
| ExcalidrawImageElement
| ExcalidrawFrameElement,
| ExcalidrawImageElement,
point: Point,
): number => {
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
@@ -347,14 +258,6 @@ const distanceToRectangle = (
);
};
const distanceToRectangleBox = (box: RectangleBox, point: Point): number => {
const [, pointRel, hwidth, hheight] = pointRelativeToDivElement(point, box);
return Math.max(
GAPoint.distanceToLine(pointRel, GALine.equation(0, 1, -hheight)),
GAPoint.distanceToLine(pointRel, GALine.equation(1, 0, -hwidth)),
);
};
const distanceToDiamond = (
element: ExcalidrawDiamondElement,
point: Point,
@@ -554,7 +457,8 @@ const pointRelativeToElement = (
): [GA.Point, GA.Point, number, number] => {
const point = GAPoint.from(pointTuple);
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const center = coordsCenter(x1, y1, x2, y2);
const elementCoords = getElementAbsoluteCoords(element);
const center = coordsCenter([x1, y1, x2, y2]);
// GA has angle orientation opposite to `rotate`
const rotate = GATransform.rotation(center, element.angle);
const pointRotated = GATransform.apply(rotate, point);
@@ -562,26 +466,9 @@ const pointRelativeToElement = (
const pointRelToCenterAbs = GAPoint.abs(pointRelToCenter);
const elementPos = GA.offset(element.x, element.y);
const pointRelToPos = GA.sub(pointRotated, elementPos);
const halfWidth = (x2 - x1) / 2;
const halfHeight = (y2 - y1) / 2;
return [pointRelToPos, pointRelToCenterAbs, halfWidth, halfHeight];
};
const pointRelativeToDivElement = (
pointTuple: Point,
rectangle: RectangleBox,
): [GA.Point, GA.Point, number, number] => {
const point = GAPoint.from(pointTuple);
const [x1, y1, x2, y2] = getRectangleBoxAbsoluteCoords(rectangle);
const center = coordsCenter(x1, y1, x2, y2);
const rotate = GATransform.rotation(center, rectangle.angle);
const pointRotated = GATransform.apply(rotate, point);
const pointRelToCenter = GA.sub(pointRotated, GADirection.from(center));
const pointRelToCenterAbs = GAPoint.abs(pointRelToCenter);
const elementPos = GA.offset(rectangle.x, rectangle.y);
const pointRelToPos = GA.sub(pointRotated, elementPos);
const halfWidth = (x2 - x1) / 2;
const halfHeight = (y2 - y1) / 2;
const [ax, ay, bx, by] = elementCoords;
const halfWidth = (bx - ax) / 2;
const halfHeight = (by - ay) / 2;
return [pointRelToPos, pointRelToCenterAbs, halfWidth, halfHeight];
};
@@ -603,7 +490,7 @@ const relativizationToElementCenter = (
element: ExcalidrawElement,
): GA.Transform => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const center = coordsCenter(x1, y1, x2, y2);
const center = coordsCenter([x1, y1, x2, y2]);
// GA has angle orientation opposite to `rotate`
const rotate = GATransform.rotation(center, element.angle);
const translate = GA.reverse(
@@ -612,13 +499,8 @@ const relativizationToElementCenter = (
return GATransform.compose(rotate, translate);
};
const coordsCenter = (
x1: number,
y1: number,
x2: number,
y2: number,
): GA.Point => {
return GA.point((x1 + x2) / 2, (y1 + y2) / 2);
const coordsCenter = ([ax, ay, bx, by]: Bounds): GA.Point => {
return GA.point((ax + bx) / 2, (ay + by) / 2);
};
// The focus distance is the oriented ratio between the size of
@@ -649,7 +531,6 @@ export const determineFocusDistance = (
case "rectangle":
case "image":
case "text":
case "frame":
return c / (hwidth * (nabs + q * mabs));
case "diamond":
return mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight);
@@ -667,7 +548,7 @@ export const determineFocusPoint = (
): Point => {
if (focus === 0) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const center = coordsCenter(x1, y1, x2, y2);
const center = coordsCenter([x1, y1, x2, y2]);
return GAPoint.toTuple(center);
}
const relateToCenter = relativizationToElementCenter(element);
@@ -682,7 +563,6 @@ export const determineFocusPoint = (
case "image":
case "text":
case "diamond":
case "frame":
point = findFocusPointForRectangulars(element, focus, adjecentPointRel);
break;
case "ellipse":
@@ -733,7 +613,6 @@ const getSortedElementLineIntersections = (
case "image":
case "text":
case "diamond":
case "frame":
const corners = getCorners(element);
intersections = corners
.flatMap((point, i) => {
@@ -767,8 +646,7 @@ const getCorners = (
| ExcalidrawRectangleElement
| ExcalidrawImageElement
| ExcalidrawDiamondElement
| ExcalidrawTextElement
| ExcalidrawFrameElement,
| ExcalidrawTextElement,
scale: number = 1,
): GA.Point[] => {
const hx = (scale * element.width) / 2;
@@ -777,7 +655,6 @@ const getCorners = (
case "rectangle":
case "image":
case "text":
case "frame":
return [
GA.point(hx, hy),
GA.point(hx, -hy),
@@ -925,8 +802,7 @@ export const findFocusPointForRectangulars = (
| ExcalidrawRectangleElement
| ExcalidrawImageElement
| ExcalidrawDiamondElement
| ExcalidrawTextElement
| ExcalidrawFrameElement,
| ExcalidrawTextElement,
// Between -1 and 1 for how far away should the focus point be relative
// to the size of the element. Sign determines orientation.
relativeDistance: number,
+3 -32
View File
@@ -6,8 +6,6 @@ import { NonDeletedExcalidrawElement } from "./types";
import { AppState, PointerDownState } from "../types";
import { getBoundTextElement } from "./textElement";
import { isSelectedViaGroup } from "../groups";
import Scene from "../scene/Scene";
import { isFrameElement } from "./typeChecks";
export const dragSelectedElements = (
pointerDownState: PointerDownState,
@@ -18,31 +16,10 @@ export const dragSelectedElements = (
distanceX: number = 0,
distanceY: number = 0,
appState: AppState,
scene: Scene,
) => {
const [x1, y1] = getCommonBounds(selectedElements);
const offset = { x: pointerX - x1, y: pointerY - y1 };
// we do not want a frame and its elements to be selected at the same time
// but when it happens (due to some bug), we want to avoid updating element
// in the frame twice, hence the use of set
const elementsToUpdate = new Set<NonDeletedExcalidrawElement>(
selectedElements,
);
const frames = selectedElements
.filter((e) => isFrameElement(e))
.map((f) => f.id);
if (frames.length > 0) {
const elementsInFrames = scene
.getNonDeletedElements()
.filter((e) => e.frameId !== null)
.filter((e) => frames.includes(e.frameId!));
elementsInFrames.forEach((element) => elementsToUpdate.add(element));
}
elementsToUpdate.forEach((element) => {
selectedElements.forEach((element) => {
updateElementCoords(
lockDirection,
distanceX,
@@ -61,13 +38,7 @@ export const dragSelectedElements = (
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
) {
const textElement = getBoundTextElement(element);
if (
textElement &&
// when container is added to a frame, so will its bound text
// so the text is already in `elementsToUpdate` and we should avoid
// updating its coords again
(!textElement.frameId || !frames.includes(textElement.frameId))
) {
if (textElement) {
updateElementCoords(
lockDirection,
distanceX,
@@ -79,7 +50,7 @@ export const dragSelectedElements = (
}
}
updateBoundElements(element, {
simultaneouslyUpdated: Array.from(elementsToUpdate),
simultaneouslyUpdated: selectedElements,
});
});
};
+1 -13
View File
@@ -2,7 +2,6 @@ import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
NonDeleted,
ExcalidrawFrameElement,
} from "./types";
import { isInvisiblySmallElement } from "./sizeHelpers";
import { isLinearElementType } from "./typeChecks";
@@ -50,11 +49,7 @@ export {
getDragOffsetXY,
dragNewElement,
} from "./dragElements";
export {
isTextElement,
isExcalidrawElement,
isFrameElement,
} from "./typeChecks";
export { isTextElement, isExcalidrawElement } from "./typeChecks";
export { textWysiwyg } from "./textWysiwyg";
export { redrawTextBoundingBox } from "./textElement";
export {
@@ -79,13 +74,6 @@ export const getNonDeletedElements = (elements: readonly ExcalidrawElement[]) =>
(element) => !element.isDeleted,
) as readonly NonDeletedExcalidrawElement[];
export const getNonDeletedFrames = (
frames: readonly ExcalidrawFrameElement[],
) =>
frames.filter(
(frame) => !frame.isDeleted,
) as readonly NonDeleted<ExcalidrawFrameElement>[];
export const isNonDeletedElement = <T extends ExcalidrawElement>(
element: T,
): element is NonDeleted<T> => !element.isDeleted;
+1 -1
View File
@@ -594,7 +594,7 @@ export class LinearElementEditor {
}
static handlePointerDown(
event: React.PointerEvent<HTMLElement>,
event: React.PointerEvent<HTMLCanvasElement>,
appState: AppState,
history: History,
scenePointer: { x: number; y: number },
-25
View File
@@ -12,7 +12,6 @@ import {
ExcalidrawFreeDrawElement,
FontFamilyValues,
ExcalidrawTextContainer,
ExcalidrawFrameElement,
} from "../element/types";
import {
arrayToMap,
@@ -51,7 +50,6 @@ type ElementConstructorOpts = MarkOptional<
| "height"
| "angle"
| "groupIds"
| "frameId"
| "boundElements"
| "seed"
| "version"
@@ -84,7 +82,6 @@ const _newElementBase = <T extends ExcalidrawElement>(
height = 0,
angle = 0,
groupIds = [],
frameId = null,
roundness = null,
boundElements = null,
link = null,
@@ -109,7 +106,6 @@ const _newElementBase = <T extends ExcalidrawElement>(
roughness,
opacity,
groupIds,
frameId,
roundness,
seed: rest.seed ?? randomInteger(),
version: rest.version || 1,
@@ -130,21 +126,6 @@ export const newElement = (
): NonDeleted<ExcalidrawGenericElement> =>
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
export const newFrameElement = (
opts: ElementConstructorOpts,
): NonDeleted<ExcalidrawFrameElement> => {
const frameElement = newElementWith(
{
..._newElementBase<ExcalidrawFrameElement>("frame", opts),
type: "frame",
name: null,
},
{},
);
return frameElement;
};
/** computes element x/y offset based on textAlign/verticalAlign */
const getTextElementPositionOffsets = (
opts: {
@@ -177,7 +158,6 @@ export const newTextElement = (
containerId?: ExcalidrawTextContainer["id"];
lineHeight?: ExcalidrawTextElement["lineHeight"];
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
isFrameName?: boolean;
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawTextElement> => {
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
@@ -212,7 +192,6 @@ export const newTextElement = (
containerId: opts.containerId || null,
originalText: text,
lineHeight,
isFrameName: opts.isFrameName || false,
},
{},
);
@@ -633,10 +612,6 @@ export const duplicateElements = (
: null;
}
if (clonedElement.frameId) {
clonedElement.frameId = maybeGetNewId(clonedElement.frameId);
}
clonedElements.push(clonedElement);
}
+37 -45
View File
@@ -27,7 +27,6 @@ import {
import {
isArrowElement,
isBoundToContainer,
isFrameElement,
isFreeDrawElement,
isImageElement,
isLinearElement,
@@ -161,17 +160,12 @@ const rotateSingleElement = (
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
let angle: number;
if (isFrameElement(element)) {
angle = 0;
} else {
angle = (5 * Math.PI) / 2 + Math.atan2(pointerY - cy, pointerX - cx);
if (shouldRotateWithDiscreteAngle) {
angle += SHIFT_LOCKING_ANGLE / 2;
angle -= angle % SHIFT_LOCKING_ANGLE;
}
angle = normalizeAngle(angle);
let angle = (5 * Math.PI) / 2 + Math.atan2(pointerY - cy, pointerX - cx);
if (shouldRotateWithDiscreteAngle) {
angle += SHIFT_LOCKING_ANGLE / 2;
angle -= angle % SHIFT_LOCKING_ANGLE;
}
angle = normalizeAngle(angle);
const boundTextElementId = getBoundTextElementId(element);
mutateElement(element, { angle });
@@ -883,47 +877,45 @@ const rotateMultipleElements = (
centerAngle += SHIFT_LOCKING_ANGLE / 2;
centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
}
elements.forEach((element) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
const origAngle =
pointerDownState.originalElements.get(element.id)?.angle ?? element.angle;
const [rotatedCX, rotatedCY] = rotate(
cx,
cy,
centerX,
centerY,
centerAngle + origAngle - element.angle,
);
elements
.filter((element) => element.type !== "frame")
.forEach((element) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
const origAngle =
pointerDownState.originalElements.get(element.id)?.angle ??
element.angle;
const [rotatedCX, rotatedCY] = rotate(
cx,
cy,
centerX,
centerY,
centerAngle + origAngle - element.angle,
);
mutateElement(
element,
{
x: element.x + (rotatedCX - cx),
y: element.y + (rotatedCY - cy),
angle: normalizeAngle(centerAngle + origAngle),
},
false,
);
updateBoundElements(element, { simultaneouslyUpdated: elements });
const boundText = getBoundTextElement(element);
if (boundText && !isArrowElement(element)) {
mutateElement(
element,
boundText,
{
x: element.x + (rotatedCX - cx),
y: element.y + (rotatedCY - cy),
x: boundText.x + (rotatedCX - cx),
y: boundText.y + (rotatedCY - cy),
angle: normalizeAngle(centerAngle + origAngle),
},
false,
);
updateBoundElements(element, { simultaneouslyUpdated: elements });
const boundText = getBoundTextElement(element);
if (boundText && !isArrowElement(element)) {
mutateElement(
boundText,
{
x: boundText.x + (rotatedCX - cx),
y: boundText.y + (rotatedCY - cy),
angle: normalizeAngle(centerAngle + origAngle),
},
false,
);
}
});
}
});
Scene.getScene(elements[0])?.informMutation();
};
+4 -6
View File
@@ -840,12 +840,10 @@ export const getTextBindableContainerAtPosition = (
const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]);
if (
isArrowElement(elements[index]) &&
isHittingElementNotConsideringBoundingBox(
elements[index],
appState,
null,
[x, y],
)
isHittingElementNotConsideringBoundingBox(elements[index], appState, [
x,
y,
])
) {
hitElement = elements[index];
break;
+1 -13
View File
@@ -8,7 +8,7 @@ import { getElementAbsoluteCoords } from "./bounds";
import { rotate } from "../math";
import { AppState, Zoom } from "../types";
import { isTextElement } from ".";
import { isFrameElement, isLinearElement } from "./typeChecks";
import { isLinearElement } from "./typeChecks";
import { DEFAULT_SPACING } from "../renderer/renderScene";
export type TransformHandleDirection =
@@ -44,14 +44,6 @@ export const OMIT_SIDES_FOR_MULTIPLE_ELEMENTS = {
w: true,
};
export const OMIT_SIDES_FOR_FRAME = {
e: true,
s: true,
n: true,
w: true,
rotation: true,
};
const OMIT_SIDES_FOR_TEXT_ELEMENT = {
e: true,
s: true,
@@ -257,10 +249,6 @@ export const getTransformHandles = (
}
} else if (isTextElement(element)) {
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
} else if (isFrameElement(element)) {
omitSides = {
rotation: true,
};
}
const dashedLineMargin = isLinearElement(element)
? DEFAULT_SPACING + 8
-7
View File
@@ -12,7 +12,6 @@ import {
ExcalidrawImageElement,
ExcalidrawTextElementWithContainer,
ExcalidrawTextContainer,
ExcalidrawFrameElement,
RoundnessType,
} from "./types";
@@ -46,12 +45,6 @@ export const isTextElement = (
return element != null && element.type === "text";
};
export const isFrameElement = (
element: ExcalidrawElement | null,
): element is ExcalidrawFrameElement => {
return element != null && element.type === "frame";
};
export const isFreeDrawElement = (
element?: ExcalidrawElement | null,
): element is ExcalidrawFreeDrawElement => {
+2 -10
View File
@@ -53,7 +53,6 @@ type _ExcalidrawElementBase = Readonly<{
/** List of groups the element belongs to.
Ordered from deepest to shallowest. */
groupIds: readonly GroupId[];
frameId: string | null;
/** other elements that are bound to this element */
boundElements:
| readonly Readonly<{
@@ -99,11 +98,6 @@ export type InitializedExcalidrawImageElement = MarkNonNullable<
"fileId"
>;
export type ExcalidrawFrameElement = _ExcalidrawElementBase & {
type: "frame";
name: string | null;
};
/**
* These are elements that don't have any additional properties.
*/
@@ -123,8 +117,7 @@ export type ExcalidrawElement =
| ExcalidrawTextElement
| ExcalidrawLinearElement
| ExcalidrawFreeDrawElement
| ExcalidrawImageElement
| ExcalidrawFrameElement;
| ExcalidrawImageElement;
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
isDeleted: boolean;
@@ -155,8 +148,7 @@ export type ExcalidrawBindableElement =
| ExcalidrawDiamondElement
| ExcalidrawEllipseElement
| ExcalidrawTextElement
| ExcalidrawImageElement
| ExcalidrawFrameElement;
| ExcalidrawImageElement;
export type ExcalidrawTextContainer =
| ExcalidrawRectangleElement
+1 -9
View File
@@ -157,8 +157,6 @@ class Collab extends PureComponent<Props, CollabState> {
window.addEventListener("offline", this.onOfflineStatusToggle);
window.addEventListener(EVENT.UNLOAD, this.onUnload);
this.onOfflineStatusToggle();
const collabAPI: CollabAPI = {
isCollaborating: this.isCollaborating,
onPointerUpdate: this.onPointerUpdate,
@@ -170,6 +168,7 @@ class Collab extends PureComponent<Props, CollabState> {
};
appJotaiStore.set(collabAPIAtom, collabAPI);
this.onOfflineStatusToggle();
if (
process.env.NODE_ENV === ENV.TEST ||
@@ -381,13 +380,6 @@ class Collab extends PureComponent<Props, CollabState> {
startCollaboration = async (
existingRoomLinkData: null | { roomId: string; roomKey: string },
): Promise<ImportedDataState | null> => {
if (!this.state.username) {
import("@excalidraw/random-username").then(({ getRandomUsername }) => {
const username = getRandomUsername();
this.onUsernameChange(username);
});
}
if (this.portal.socket) {
return null;
}
@@ -6,7 +6,6 @@ import { LanguageList } from "./LanguageList";
export const AppMainMenu: React.FC<{
setCollabDialogShown: (toggle: boolean) => any;
isCollaborating: boolean;
isCollabEnabled: boolean;
}> = React.memo((props) => {
return (
<MainMenu>
@@ -14,12 +13,10 @@ export const AppMainMenu: React.FC<{
<MainMenu.DefaultItems.SaveToActiveFile />
<MainMenu.DefaultItems.Export />
<MainMenu.DefaultItems.SaveAsImage />
{props.isCollabEnabled && (
<MainMenu.DefaultItems.LiveCollaborationTrigger
isCollaborating={props.isCollaborating}
onSelect={() => props.setCollabDialogShown(true)}
/>
)}
<MainMenu.DefaultItems.LiveCollaborationTrigger
isCollaborating={props.isCollaborating}
onSelect={() => props.setCollabDialogShown(true)}
/>
<MainMenu.DefaultItems.Help />
<MainMenu.DefaultItems.ClearCanvas />
@@ -6,7 +6,6 @@ import { isExcalidrawPlusSignedUser } from "../app_constants";
export const AppWelcomeScreen: React.FC<{
setCollabDialogShown: (toggle: boolean) => any;
isCollabEnabled: boolean;
}> = React.memo((props) => {
const { t } = useI18n();
let headingContent;
@@ -47,11 +46,9 @@ export const AppWelcomeScreen: React.FC<{
<WelcomeScreen.Center.Menu>
<WelcomeScreen.Center.MenuItemLoadScene />
<WelcomeScreen.Center.MenuItemHelp />
{props.isCollabEnabled && (
<WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
onSelect={() => props.setCollabDialogShown(true)}
/>
)}
<WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
onSelect={() => props.setCollabDialogShown(true)}
/>
{!isExcalidrawPlusSignedUser && (
<WelcomeScreen.Center.MenuItemLink
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
+11 -22
View File
@@ -42,7 +42,6 @@ import {
preventUnload,
ResolvablePromise,
resolvablePromise,
isRunningInIframe,
} from "../utils";
import {
FIREBASE_STORAGE_PREFIXES,
@@ -99,7 +98,7 @@ languageDetector.init({
});
const initializeScene = async (opts: {
collabAPI: CollabAPI | null;
collabAPI: CollabAPI;
excalidrawAPI: ExcalidrawImperativeAPI;
}): Promise<
{ scene: ExcalidrawInitialDataState | null } & (
@@ -184,7 +183,7 @@ const initializeScene = async (opts: {
}
}
if (roomLinkData && opts.collabAPI) {
if (roomLinkData) {
const { excalidrawAPI } = opts;
const scene = await opts.collabAPI.startCollaboration(roomLinkData);
@@ -238,7 +237,6 @@ export const appLangCodeAtom = atom(
const ExcalidrawWrapper = () => {
const [errorMessage, setErrorMessage] = useState("");
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
const isCollabDisabled = isRunningInIframe();
// initial state
// ---------------------------------------------------------------------------
@@ -274,7 +272,7 @@ const ExcalidrawWrapper = () => {
});
useEffect(() => {
if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
if (!collabAPI || !excalidrawAPI) {
return;
}
@@ -285,7 +283,7 @@ const ExcalidrawWrapper = () => {
if (!data.scene) {
return;
}
if (collabAPI?.isCollaborating()) {
if (collabAPI.isCollaborating()) {
if (data.scene.elements) {
collabAPI
.fetchImageFilesFromFirebase({
@@ -355,7 +353,7 @@ const ExcalidrawWrapper = () => {
const libraryUrlTokens = parseLibraryTokensFromUrl();
if (!libraryUrlTokens) {
if (
collabAPI?.isCollaborating() &&
collabAPI.isCollaborating() &&
!isCollaborationLink(window.location.href)
) {
collabAPI.stopCollaboration(false);
@@ -384,10 +382,7 @@ const ExcalidrawWrapper = () => {
if (isTestEnv()) {
return;
}
if (
!document.hidden &&
((collabAPI && !collabAPI.isCollaborating()) || isCollabDisabled)
) {
if (!document.hidden && !collabAPI.isCollaborating()) {
// don't sync if local state is newer or identical to browser state
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
const localDataState = importFromLocalStorage();
@@ -403,7 +398,7 @@ const ExcalidrawWrapper = () => {
excalidrawAPI.updateLibrary({
libraryItems: getLibraryItemsFromStorage(),
});
collabAPI?.setUsername(username || "");
collabAPI.setUsername(username || "");
}
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) {
@@ -471,7 +466,7 @@ const ExcalidrawWrapper = () => {
);
clearTimeout(titleTimeout);
};
}, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]);
}, [collabAPI, excalidrawAPI, setLangCode]);
useEffect(() => {
const unloadHandler = (event: BeforeUnloadEvent) => {
@@ -654,7 +649,7 @@ const ExcalidrawWrapper = () => {
autoFocus={true}
theme={theme}
renderTopRightUI={(isMobile) => {
if (isMobile || !collabAPI || isCollabDisabled) {
if (isMobile) {
return null;
}
return (
@@ -668,21 +663,15 @@ const ExcalidrawWrapper = () => {
<AppMainMenu
setCollabDialogShown={setCollabDialogShown}
isCollaborating={isCollaborating}
isCollabEnabled={!isCollabDisabled}
/>
<AppWelcomeScreen
setCollabDialogShown={setCollabDialogShown}
isCollabEnabled={!isCollabDisabled}
/>
<AppWelcomeScreen setCollabDialogShown={setCollabDialogShown} />
<AppFooter />
{isCollaborating && isOffline && (
<div className="collab-offline-warning">
{t("alerts.collabOfflineWarning")}
</div>
)}
{excalidrawAPI && !isCollabDisabled && (
<Collab excalidrawAPI={excalidrawAPI} />
)}
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
</Excalidraw>
{errorMessage && (
<ErrorDialog onClose={() => setErrorMessage("")}>
-705
View File
@@ -1,705 +0,0 @@
import {
getCommonBounds,
getElementAbsoluteCoords,
isTextElement,
} from "./element";
import {
ExcalidrawElement,
ExcalidrawFrameElement,
NonDeleted,
NonDeletedExcalidrawElement,
} from "./element/types";
import { isPointWithinBounds } from "./math";
import {
getBoundTextElement,
getContainerElement,
} from "./element/textElement";
import { arrayToMap, findIndex } from "./utils";
import { mutateElement } from "./element/mutateElement";
import { AppState } from "./types";
import { getElementsWithinSelection, getSelectedElements } from "./scene";
import { isFrameElement } from "./element";
import { moveOneRight } from "./zindex";
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
import { getElementLineSegments } from "./element/bounds";
// --------------------------- Frame State ------------------------------------
export const bindElementsToFramesAfterDuplication = (
nextElements: ExcalidrawElement[],
oldElements: readonly ExcalidrawElement[],
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
) => {
const nextElementMap = arrayToMap(nextElements) as Map<
ExcalidrawElement["id"],
ExcalidrawElement
>;
for (const element of oldElements) {
if (element.frameId) {
// use its frameId to get the new frameId
const nextElementId = oldIdToDuplicatedId.get(element.id);
const nextFrameId = oldIdToDuplicatedId.get(element.frameId);
if (nextElementId) {
const nextElement = nextElementMap.get(nextElementId);
if (nextElement) {
mutateElement(
nextElement,
{
frameId: nextFrameId ?? element.frameId,
},
false,
);
}
}
}
}
};
// --------------------------- Frame Geometry ---------------------------------
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
class LineSegment {
first: Point;
second: Point;
constructor(pointA: Point, pointB: Point) {
this.first = pointA;
this.second = pointB;
}
public getBoundingBox(): [Point, Point] {
return [
new Point(
Math.min(this.first.x, this.second.x),
Math.min(this.first.y, this.second.y),
),
new Point(
Math.max(this.first.x, this.second.x),
Math.max(this.first.y, this.second.y),
),
];
}
}
// https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/
class FrameGeometry {
private static EPSILON = 0.000001;
private static crossProduct(a: Point, b: Point) {
return a.x * b.y - b.x * a.y;
}
private static doBoundingBoxesIntersect(
a: [Point, Point],
b: [Point, Point],
) {
return (
a[0].x <= b[1].x &&
a[1].x >= b[0].x &&
a[0].y <= b[1].y &&
a[1].y >= b[0].y
);
}
private static isPointOnLine(a: LineSegment, b: Point) {
const aTmp = new LineSegment(
new Point(0, 0),
new Point(a.second.x - a.first.x, a.second.y - a.first.y),
);
const bTmp = new Point(b.x - a.first.x, b.y - a.first.y);
const r = this.crossProduct(aTmp.second, bTmp);
return Math.abs(r) < this.EPSILON;
}
private static isPointRightOfLine(a: LineSegment, b: Point) {
const aTmp = new LineSegment(
new Point(0, 0),
new Point(a.second.x - a.first.x, a.second.y - a.first.y),
);
const bTmp = new Point(b.x - a.first.x, b.y - a.first.y);
return this.crossProduct(aTmp.second, bTmp) < 0;
}
private static lineSegmentTouchesOrCrossesLine(
a: LineSegment,
b: LineSegment,
) {
return (
this.isPointOnLine(a, b.first) ||
this.isPointOnLine(a, b.second) ||
(this.isPointRightOfLine(a, b.first)
? !this.isPointRightOfLine(a, b.second)
: this.isPointRightOfLine(a, b.second))
);
}
private static doLineSegmentsIntersect(
a: [readonly [number, number], readonly [number, number]],
b: [readonly [number, number], readonly [number, number]],
) {
const aSegment = new LineSegment(
new Point(a[0][0], a[0][1]),
new Point(a[1][0], a[1][1]),
);
const bSegment = new LineSegment(
new Point(b[0][0], b[0][1]),
new Point(b[1][0], b[1][1]),
);
const box1 = aSegment.getBoundingBox();
const box2 = bSegment.getBoundingBox();
return (
this.doBoundingBoxesIntersect(box1, box2) &&
this.lineSegmentTouchesOrCrossesLine(aSegment, bSegment) &&
this.lineSegmentTouchesOrCrossesLine(bSegment, aSegment)
);
}
public static isElementIntersectingFrame(
element: ExcalidrawElement,
frame: ExcalidrawFrameElement,
) {
const frameLineSegments = getElementLineSegments(frame);
const elementLineSegments = getElementLineSegments(element);
const intersecting = frameLineSegments.some((frameLineSegment) =>
elementLineSegments.some((elementLineSegment) =>
this.doLineSegmentsIntersect(frameLineSegment, elementLineSegment),
),
);
return intersecting;
}
}
export const getElementsCompletelyInFrame = (
elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameElement,
) =>
omitGroupsContainingFrames(
getElementsWithinSelection(elements, frame, false),
).filter(
(element) =>
(element.type !== "frame" && !element.frameId) ||
element.frameId === frame.id,
);
export const isElementContainingFrame = (
elements: readonly ExcalidrawElement[],
element: ExcalidrawElement,
frame: ExcalidrawFrameElement,
) => {
return getElementsWithinSelection(elements, element).some(
(e) => e.id === frame.id,
);
};
export const getElementsIntersectingFrame = (
elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameElement,
) =>
elements.filter((element) =>
FrameGeometry.isElementIntersectingFrame(element, frame),
);
export const elementsAreInFrameBounds = (
elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameElement,
) => {
const [selectionX1, selectionY1, selectionX2, selectionY2] =
getElementAbsoluteCoords(frame);
const [elementX1, elementY1, elementX2, elementY2] =
getCommonBounds(elements);
return (
selectionX1 <= elementX1 &&
selectionY1 <= elementY1 &&
selectionX2 >= elementX2 &&
selectionY2 >= elementY2
);
};
export const elementOverlapsWithFrame = (
element: ExcalidrawElement,
frame: ExcalidrawFrameElement,
) => {
return (
elementsAreInFrameBounds([element], frame) ||
FrameGeometry.isElementIntersectingFrame(element, frame) ||
isElementContainingFrame([frame], element, frame)
);
};
export const isCursorInFrame = (
cursorCoords: {
x: number;
y: number;
},
frame: NonDeleted<ExcalidrawFrameElement>,
) => {
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame);
return isPointWithinBounds(
[fx1, fy1],
[cursorCoords.x, cursorCoords.y],
[fx2, fy2],
);
};
export const groupsAreAtLeastIntersectingTheFrame = (
elements: readonly NonDeletedExcalidrawElement[],
groupIds: readonly string[],
frame: ExcalidrawFrameElement,
) => {
const elementsInGroup = groupIds.flatMap((groupId) =>
getElementsInGroup(elements, groupId),
);
if (elementsInGroup.length === 0) {
return true;
}
return !!elementsInGroup.find(
(element) =>
elementsAreInFrameBounds([element], frame) ||
FrameGeometry.isElementIntersectingFrame(element, frame),
);
};
export const groupsAreCompletelyOutOfFrame = (
elements: readonly NonDeletedExcalidrawElement[],
groupIds: readonly string[],
frame: ExcalidrawFrameElement,
) => {
const elementsInGroup = groupIds.flatMap((groupId) =>
getElementsInGroup(elements, groupId),
);
if (elementsInGroup.length === 0) {
return true;
}
return (
elementsInGroup.find(
(element) =>
elementsAreInFrameBounds([element], frame) ||
FrameGeometry.isElementIntersectingFrame(element, frame),
) === undefined
);
};
// --------------------------- Frame Utils ------------------------------------
/**
* Returns a map of frameId to frame elements. Includes empty frames.
*/
export const groupByFrames = (elements: ExcalidrawElementsIncludingDeleted) => {
const frameElementsMap = new Map<
ExcalidrawElement["id"],
ExcalidrawElement[]
>();
for (const element of elements) {
const frameId = isFrameElement(element) ? element.id : element.frameId;
if (frameId && !frameElementsMap.has(frameId)) {
frameElementsMap.set(frameId, getFrameElements(elements, frameId));
}
}
return frameElementsMap;
};
export const getFrameElements = (
allElements: ExcalidrawElementsIncludingDeleted,
frameId: string,
) => allElements.filter((element) => element.frameId === frameId);
export const getElementsInResizingFrame = (
allElements: ExcalidrawElementsIncludingDeleted,
frame: ExcalidrawFrameElement,
appState: AppState,
): ExcalidrawElement[] => {
const prevElementsInFrame = getFrameElements(allElements, frame.id);
const nextElementsInFrame = new Set<ExcalidrawElement>(prevElementsInFrame);
const elementsCompletelyInFrame = new Set([
...getElementsCompletelyInFrame(allElements, frame),
...prevElementsInFrame.filter((element) =>
isElementContainingFrame(allElements, element, frame),
),
]);
const elementsNotCompletelyInFrame = prevElementsInFrame.filter(
(element) => !elementsCompletelyInFrame.has(element),
);
// for elements that are completely in the frame
// if they are part of some groups, then those groups are still
// considered to belong to the frame
const groupsToKeep = new Set<string>(
Array.from(elementsCompletelyInFrame).flatMap(
(element) => element.groupIds,
),
);
for (const element of elementsNotCompletelyInFrame) {
if (!FrameGeometry.isElementIntersectingFrame(element, frame)) {
if (element.groupIds.length === 0) {
nextElementsInFrame.delete(element);
}
} else if (element.groupIds.length > 0) {
// group element intersects with the frame, we should keep the groups
// that this element is part of
for (const id of element.groupIds) {
groupsToKeep.add(id);
}
}
}
for (const element of elementsNotCompletelyInFrame) {
if (element.groupIds.length > 0) {
let shouldRemoveElement = true;
for (const id of element.groupIds) {
if (groupsToKeep.has(id)) {
shouldRemoveElement = false;
}
}
if (shouldRemoveElement) {
nextElementsInFrame.delete(element);
}
}
}
const individualElementsCompletelyInFrame = Array.from(
elementsCompletelyInFrame,
).filter((element) => element.groupIds.length === 0);
for (const element of individualElementsCompletelyInFrame) {
nextElementsInFrame.add(element);
}
const newGroupElementsCompletelyInFrame = Array.from(
elementsCompletelyInFrame,
).filter((element) => element.groupIds.length > 0);
const groupIds = selectGroupsFromGivenElements(
newGroupElementsCompletelyInFrame,
appState,
);
// new group elements
for (const [id, isSelected] of Object.entries(groupIds)) {
if (isSelected) {
const elementsInGroup = getElementsInGroup(allElements, id);
if (elementsAreInFrameBounds(elementsInGroup, frame)) {
for (const element of elementsInGroup) {
nextElementsInFrame.add(element);
}
}
}
}
return [...nextElementsInFrame].filter((element) => {
return !(isTextElement(element) && element.containerId);
});
};
export const getElementsInNewFrame = (
allElements: ExcalidrawElementsIncludingDeleted,
frame: ExcalidrawFrameElement,
) => {
return omitGroupsContainingFrames(
allElements,
getElementsCompletelyInFrame(allElements, frame),
);
};
export const getContainingFrame = (
element: ExcalidrawElement,
/**
* Optionally an elements map, in case the elements aren't in the Scene yet.
* Takes precedence over Scene elements, even if the element exists
* in Scene elements and not the supplied elements map.
*/
elementsMap?: Map<string, ExcalidrawElement>,
) => {
if (element.frameId) {
if (elementsMap) {
return (elementsMap.get(element.frameId) ||
null) as null | ExcalidrawFrameElement;
}
return (
(Scene.getScene(element)?.getElement(
element.frameId,
) as ExcalidrawFrameElement) || null
);
}
return null;
};
// --------------------------- Frame Operations -------------------------------
export const addElementsToFrame = (
allElements: ExcalidrawElementsIncludingDeleted,
elementsToAdd: NonDeletedExcalidrawElement[],
frame: ExcalidrawFrameElement,
) => {
const _elementsToAdd: ExcalidrawElement[] = [];
for (const element of elementsToAdd) {
_elementsToAdd.push(element);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
_elementsToAdd.push(boundTextElement);
}
}
let nextElements = allElements.slice();
const frameBoundary = findIndex(nextElements, (e) => e.frameId === frame.id);
for (const element of omitGroupsContainingFrames(
allElements,
_elementsToAdd,
)) {
if (element.frameId !== frame.id && !isFrameElement(element)) {
mutateElement(
element,
{
frameId: frame.id,
},
false,
);
const frameIndex = findIndex(nextElements, (e) => e.id === frame.id);
const elementIndex = findIndex(nextElements, (e) => e.id === element.id);
if (elementIndex < frameBoundary) {
nextElements = [
...nextElements.slice(0, elementIndex),
...nextElements.slice(elementIndex + 1, frameBoundary),
element,
...nextElements.slice(frameBoundary),
];
} else if (elementIndex > frameIndex) {
nextElements = [
...nextElements.slice(0, frameIndex),
element,
...nextElements.slice(frameIndex, elementIndex),
...nextElements.slice(elementIndex + 1),
];
}
}
}
return nextElements;
};
export const removeElementsFromFrame = (
allElements: ExcalidrawElementsIncludingDeleted,
elementsToRemove: NonDeletedExcalidrawElement[],
appState: AppState,
) => {
const _elementsToRemove: ExcalidrawElement[] = [];
for (const element of elementsToRemove) {
if (element.frameId) {
_elementsToRemove.push(element);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
_elementsToRemove.push(boundTextElement);
}
}
}
for (const element of _elementsToRemove) {
mutateElement(
element,
{
frameId: null,
},
false,
);
}
const nextElements = moveOneRight(
allElements,
appState,
Array.from(_elementsToRemove),
);
return nextElements;
};
export const removeAllElementsFromFrame = (
allElements: ExcalidrawElementsIncludingDeleted,
frame: ExcalidrawFrameElement,
appState: AppState,
) => {
const elementsInFrame = getFrameElements(allElements, frame.id);
return removeElementsFromFrame(allElements, elementsInFrame, appState);
};
export const replaceAllElementsInFrame = (
allElements: ExcalidrawElementsIncludingDeleted,
nextElementsInFrame: ExcalidrawElement[],
frame: ExcalidrawFrameElement,
appState: AppState,
) => {
return addElementsToFrame(
removeAllElementsFromFrame(allElements, frame, appState),
nextElementsInFrame,
frame,
);
};
/** does not mutate elements, but return new ones */
export const updateFrameMembershipOfSelectedElements = (
allElements: ExcalidrawElementsIncludingDeleted,
appState: AppState,
) => {
const selectedElements = getSelectedElements(allElements, appState);
const elementsToFilter = new Set<ExcalidrawElement>(selectedElements);
if (appState.editingGroupId) {
for (const element of selectedElements) {
if (element.groupIds.length === 0) {
elementsToFilter.add(element);
} else {
element.groupIds
.flatMap((gid) => getElementsInGroup(allElements, gid))
.forEach((element) => elementsToFilter.add(element));
}
}
}
const elementsToRemove = new Set<ExcalidrawElement>();
elementsToFilter.forEach((element) => {
if (
!isFrameElement(element) &&
!isElementInFrame(element, allElements, appState)
) {
elementsToRemove.add(element);
}
});
return removeElementsFromFrame(allElements, [...elementsToRemove], appState);
};
/**
* filters out elements that are inside groups that contain a frame element
* anywhere in the group tree
*/
export const omitGroupsContainingFrames = (
allElements: ExcalidrawElementsIncludingDeleted,
/** subset of elements you want to filter. Optional perf optimization so we
* don't have to filter all elements unnecessarily
*/
selectedElements?: readonly ExcalidrawElement[],
) => {
const uniqueGroupIds = new Set<string>();
for (const el of selectedElements || allElements) {
const topMostGroupId = el.groupIds[el.groupIds.length - 1];
if (topMostGroupId) {
uniqueGroupIds.add(topMostGroupId);
}
}
const rejectedGroupIds = new Set<string>();
for (const groupId of uniqueGroupIds) {
if (
getElementsInGroup(allElements, groupId).some((el) => isFrameElement(el))
) {
rejectedGroupIds.add(groupId);
}
}
return (selectedElements || allElements).filter(
(el) => !rejectedGroupIds.has(el.groupIds[el.groupIds.length - 1]),
);
};
/**
* depending on the appState, return target frame, which is the frame the given element
* is going to be added to or remove from
*/
export const getTargetFrame = (
element: ExcalidrawElement,
appState: AppState,
) => {
const _element = isTextElement(element)
? getContainerElement(element) || element
: element;
return appState.selectedElementIds[_element.id] &&
appState.selectedElementsAreBeingDragged
? appState.frameToHighlight
: getContainingFrame(_element);
};
// given an element, return if the element is in some frame
export const isElementInFrame = (
element: ExcalidrawElement,
allElements: ExcalidrawElementsIncludingDeleted,
appState: AppState,
) => {
const frame = getTargetFrame(element, appState);
const _element = isTextElement(element)
? getContainerElement(element) || element
: element;
if (frame) {
if (_element.groupIds.length === 0) {
return elementOverlapsWithFrame(_element, frame);
}
const allElementsInGroup = new Set(
_element.groupIds.flatMap((gid) => getElementsInGroup(allElements, gid)),
);
if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) {
const selectedElements = new Set(
getSelectedElements(allElements, appState),
);
const editingGroupOverlapsFrame = appState.frameToHighlight !== null;
if (editingGroupOverlapsFrame) {
return true;
}
selectedElements.forEach((selectedElement) => {
allElementsInGroup.delete(selectedElement);
});
}
for (const elementInGroup of allElementsInGroup) {
if (isFrameElement(elementInGroup)) {
return false;
}
}
for (const elementInGroup of allElementsInGroup) {
if (elementOverlapsWithFrame(elementInGroup, frame)) {
return true;
}
}
}
return false;
};
+4
View File
@@ -120,3 +120,7 @@ declare module "image-blob-reduce" {
const reduce: ImageBlobReduce.ImageBlobReduceStatic;
export = reduce;
}
declare module "pngjs/browser" {
export { PNG } from "pngjs";
}
-40
View File
@@ -94,31 +94,6 @@ export const selectGroupsForSelectedElements = (
return nextAppState;
};
// given a list of elements, return the the actual group ids that should be selected
// or used to update the elements
export const selectGroupsFromGivenElements = (
elements: readonly NonDeleted<ExcalidrawElement>[],
appState: AppState,
) => {
let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
for (const element of elements) {
let groupIds = element.groupIds;
if (appState.editingGroupId) {
const indexOfEditingGroup = groupIds.indexOf(appState.editingGroupId);
if (indexOfEditingGroup > -1) {
groupIds = groupIds.slice(0, indexOfEditingGroup);
}
}
if (groupIds.length > 0) {
const groupId = groupIds[groupIds.length - 1];
nextAppState = selectGroup(groupId, nextAppState, elements);
}
}
return nextAppState.selectedGroupIds;
};
export const editGroupForSelectedElement = (
appState: AppState,
element: NonDeleted<ExcalidrawElement>,
@@ -211,18 +186,3 @@ export const getMaximumGroups = (
return Array.from(groups.values());
};
export const elementsAreInSameGroup = (elements: ExcalidrawElement[]) => {
const allGroups = elements.flatMap((element) => element.groupIds);
const groupCount = new Map<string, number>();
let maxGroup = 0;
for (const group of allGroups) {
groupCount.set(group, (groupCount.get(group) ?? 0) + 1);
if (groupCount.get(group)! > maxGroup) {
maxGroup = groupCount.get(group)!;
}
}
return maxGroup === elements.length;
};
-1
View File
@@ -42,7 +42,6 @@ export const KEYS = {
CHEVRON_RIGHT: ">",
PERIOD: ".",
COMMA: ",",
SUBTRACT: "-",
A: "a",
C: "c",
-449
View File
@@ -1,449 +0,0 @@
{
"labels": {
"paste": "Yapışdır",
"pasteAsPlaintext": "Düz mətn kimi yapışdırın",
"pasteCharts": "Diaqramları yapışdırın",
"selectAll": "Hamısını seç",
"multiSelect": "Seçimə element əlavə edin",
"moveCanvas": "Kanvası köçürün",
"cut": "Kəs",
"copy": "Kopyala",
"copyAsPng": "PNG olaraq panoya kopyala",
"copyAsSvg": "SVG olaraq panoya kopyala",
"copyText": "Mətn olaraq panoya kopyala",
"bringForward": "Önə daşı",
"sendToBack": "Geriyə göndərin",
"bringToFront": "Önə gətirin",
"sendBackward": "Geriyə göndərin",
"delete": "Sil",
"copyStyles": "Stilləri kopyalayın",
"pasteStyles": "Stilləri yapışdırın",
"stroke": "Strok rəngi",
"background": "Arxa fon",
"fill": "Doldur",
"strokeWidth": "Strok eni",
"strokeStyle": "Strok stili",
"strokeStyle_solid": "Solid",
"strokeStyle_dashed": "Kəsik",
"strokeStyle_dotted": "Nöqtəli",
"sloppiness": "Səliqəsizlik",
"opacity": "Şəffaflıq",
"textAlign": "Mətni uyğunlaşdır",
"edges": "Kənarlar",
"sharp": "Kəskin",
"round": "Dəyirmi",
"arrowheads": "Ox ucları",
"arrowhead_none": "Heç biri",
"arrowhead_arrow": "Ox",
"arrowhead_bar": "Çubuq",
"arrowhead_dot": "Nöqtə",
"arrowhead_triangle": "Üçbucaq",
"fontSize": "Şrift ölçüsü",
"fontFamily": "Şrift qrupu",
"addWatermark": "\"Made with Excalidraw\" əlavə et",
"handDrawn": "Əllə çəkilmiş",
"normal": "Normal",
"code": "Kod",
"small": "Kiçik",
"medium": "Orta",
"large": "Böyük",
"veryLarge": "Çox böyük",
"solid": "Solid",
"hachure": "Ştrix",
"zigzag": "Ziqzaq",
"crossHatch": "Çarpaz dəlik",
"thin": "İncə",
"bold": "Qalın",
"left": "Sol",
"center": "Mərkəz",
"right": "Sağ",
"extraBold": "Ekstra qalın",
"architect": "Memar",
"artist": "Rəssam",
"cartoonist": "Karikaturaçı",
"fileTitle": "Fayl adı",
"colorPicker": "Rəng seçən",
"canvasColors": "Kanvas üzərində istifadə olunur",
"canvasBackground": "Kanvas arxa fonu",
"drawingCanvas": "Kanvas çəkmək",
"layers": "Qatlar",
"actions": "Hərəkətlər",
"language": "Dil",
"liveCollaboration": "Canlı əməkdaşlıq...",
"duplicateSelection": "Dublikat",
"untitled": "Başlıqsız",
"name": "Ad",
"yourName": "Adınız",
"madeWithExcalidraw": "Excalidraw ilə hazırlanmışdır",
"group": "Qrup şəklində seçim",
"ungroup": "Qrupsuz seçim",
"collaborators": "",
"showGrid": "",
"addToLibrary": "",
"removeFromLibrary": "",
"libraryLoadingMessage": "",
"libraries": "",
"loadingScene": "",
"align": "",
"alignTop": "",
"alignBottom": "",
"alignLeft": "",
"alignRight": "",
"centerVertically": "",
"centerHorizontally": "",
"distributeHorizontally": "",
"distributeVertically": "",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "",
"share": "",
"showStroke": "",
"showBackground": "",
"toggleTheme": "",
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"bindText": "",
"createContainerFromText": "",
"link": {
"edit": "",
"create": "",
"label": ""
},
"lineEditor": {
"edit": "",
"exit": ""
},
"elementLock": {
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "",
"sidebarLock": "",
"eyeDropper": ""
},
"library": {
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
},
"buttons": {
"clearReset": "",
"exportJSON": "",
"exportImage": "",
"export": "",
"copyToClipboard": "",
"save": "",
"saveAs": "",
"load": "",
"getShareableLink": "",
"close": "",
"selectLanguage": "",
"scrollBackToContent": "",
"zoomIn": "",
"zoomOut": "",
"resetZoom": "",
"menu": "",
"done": "",
"edit": "",
"undo": "",
"redo": "",
"resetLibrary": "",
"createNewRoom": "",
"fullScreen": "",
"darkMode": "",
"lightMode": "",
"zenMode": "",
"exitZenMode": "",
"cancel": "",
"clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
},
"alerts": {
"clearReset": "",
"couldNotCreateShareableLink": "",
"couldNotCreateShareableLinkTooBig": "",
"couldNotLoadInvalidFile": "",
"importBackendFailed": "",
"cannotExportEmptyCanvas": "",
"couldNotCopyToClipboard": "",
"decryptFailed": "",
"uploadedSecurly": "",
"loadSceneOverridePrompt": "",
"collabStopOverridePrompt": "",
"errorAddingToLibrary": "",
"errorRemovingFromLibrary": "",
"confirmAddLibrary": "",
"imageDoesNotContainScene": "",
"cannotRestoreFromImage": "",
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"collabOfflineWarning": ""
},
"errors": {
"unsupportedFileType": "",
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": "",
"cannotResolveCollabServer": "",
"importLibraryError": "",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"line1": "",
"line2": "",
"line3": "",
"line4": ""
}
},
"toolBar": {
"selection": "",
"image": "",
"rectangle": "",
"diamond": "",
"ellipse": "",
"arrow": "",
"line": "",
"freedraw": "",
"text": "",
"library": "",
"lock": "",
"penMode": "",
"link": "",
"eraser": "",
"hand": ""
},
"headings": {
"canvasActions": "",
"selectedShapeActions": "",
"shapes": ""
},
"hints": {
"canvasPanning": "",
"linearElement": "",
"freeDraw": "",
"text": "",
"text_selected": "",
"text_editing": "",
"linearElementMulti": "",
"lockAngle": "",
"resize": "",
"resizeImage": "",
"rotate": "",
"lineEditor_info": "",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "",
"canvasTooBig": "",
"canvasTooBigTip": ""
},
"errorSplash": {
"headingMain": "",
"clearCanvasMessage": "",
"clearCanvasCaveat": "",
"trackedToSentry": "",
"openIssueMessage": "",
"sceneContent": ""
},
"roomDialog": {
"desc_intro": "",
"desc_privacy": "",
"button_startSession": "",
"button_stopSession": "",
"desc_inProgressIntro": "",
"desc_shareLink": "",
"desc_exitSession": "",
"shareTitle": ""
},
"errorDialog": {
"title": ""
},
"exportDialog": {
"disk_title": "",
"disk_details": "",
"disk_button": "",
"link_title": "",
"link_details": "",
"link_button": "",
"excalidrawplus_description": "",
"excalidrawplus_button": "",
"excalidrawplus_exportError": ""
},
"helpDialog": {
"blog": "",
"click": "",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "",
"curvedLine": "",
"documentation": "",
"doubleClick": "",
"drag": "",
"editor": "",
"editLineArrowPoints": "",
"editText": "",
"github": "",
"howto": "",
"or": "",
"preventBinding": "",
"tools": "",
"shortcuts": "",
"textFinish": "",
"textNewLine": "",
"title": "",
"view": "",
"zoomToFit": "",
"zoomToSelection": "",
"toggleElementLock": "",
"movePageUpDown": "",
"movePageLeftRight": ""
},
"clearCanvasDialog": {
"title": ""
},
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": "",
"noteGuidelines": "",
"noteLicense": "",
"noteItems": "",
"atleastOneLibItem": "",
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "",
"content": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"imageExportDialog": {
"header": "",
"label": {
"withBackground": "",
"onlySelected": "",
"darkMode": "",
"embedScene": "",
"scale": "",
"padding": ""
},
"tooltip": {
"embedScene": ""
},
"title": {
"exportToPng": "",
"exportToSvg": "",
"copyPngToClipboard": ""
},
"button": {
"exportToPng": "",
"exportToSvg": "",
"copyPngToClipboard": ""
}
},
"encrypted": {
"tooltip": "",
"link": ""
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"version": "",
"versionCopy": "",
"versionNotAvailable": "",
"width": ""
},
"toast": {
"addedToLibrary": "",
"copyStyles": "",
"copyToClipboard": "",
"copyToClipboardAsPng": "",
"fileSaved": "",
"fileSavedToFilename": "",
"canvas": "",
"selection": "",
"pasteAsSingleElement": ""
},
"colors": {
"transparent": "",
"black": "",
"white": "",
"red": "",
"pink": "",
"grape": "",
"violet": "",
"gray": "",
"blue": "",
"cyan": "",
"teal": "",
"green": "",
"yellow": "",
"orange": "",
"bronze": ""
},
"welcomeScreen": {
"app": {
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
},
"colorPicker": {
"mostUsedCustomColors": "",
"colors": "",
"shades": "",
"hexCode": "",
"noShades": ""
}
}
+133 -133
View File
@@ -1,7 +1,7 @@
{
"labels": {
"paste": "Vložit",
"pasteAsPlaintext": "Vložit jako prostý text",
"pasteAsPlaintext": "",
"pasteCharts": "Vložit grafy",
"selectAll": "Vybrat vše",
"multiSelect": "Přidat prvek do výběru",
@@ -49,9 +49,9 @@
"large": "Velké",
"veryLarge": "Velmi velké",
"solid": "Plný",
"hachure": "Hachure",
"zigzag": "Klikatě",
"crossHatch": "Křížový šrafování",
"hachure": "",
"zigzag": "",
"crossHatch": "",
"thin": "Tenký",
"bold": "Tlustý",
"left": "Vlevo",
@@ -60,7 +60,7 @@
"extraBold": "Extra tlustý",
"architect": "Architekt",
"artist": "Umělec",
"cartoonist": "Kartoonista",
"cartoonist": "",
"fileTitle": "Název souboru",
"colorPicker": "Výběr barvy",
"canvasColors": "Použito na plátně",
@@ -106,7 +106,7 @@
"increaseFontSize": "Zvětšit písmo",
"unbindText": "Zrušit vazbu textu",
"bindText": "Vázat text s kontejnerem",
"createContainerFromText": "Zabalit text do kontejneru",
"createContainerFromText": "",
"link": {
"edit": "Upravit odkaz",
"create": "Vytvořit odkaz",
@@ -124,7 +124,7 @@
},
"statusPublished": "Zveřejněno",
"sidebarLock": "Ponechat postranní panel otevřený",
"eyeDropper": "Vyberte barvu z plátna"
"eyeDropper": ""
},
"library": {
"noItems": "Dosud neexistují žádné položky...",
@@ -177,38 +177,38 @@
"decryptFailed": "Nelze dešifrovat data.",
"uploadedSecurly": "Nahrávání je zabezpečeno koncovým šifrováním, což znamená, že server Excalidraw ani třetí strany nemohou obsah přečíst.",
"loadSceneOverridePrompt": "Načítání externího výkresu nahradí váš existující obsah. Přejete si pokračovat?",
"collabStopOverridePrompt": "Zastavení relace přepíše vaše předchozí, lokálně uložené kresby. Jste si jisti?\n\n(Pokud chcete zachovat místní kresbu, jednoduše zavřete kartu prohlížeče)",
"collabStopOverridePrompt": "",
"errorAddingToLibrary": "Položku nelze přidat do knihovny",
"errorRemovingFromLibrary": "Položku nelze odstranit z knihovny",
"confirmAddLibrary": "Tímto přidáte {{numShapes}} tvarů do tvé knihovny. Jste si jisti?",
"imageDoesNotContainScene": "Zdá se, že tento obrázek neobsahuje žádná data o scéně. Zapnuli jste při exportu vkládání scény?",
"cannotRestoreFromImage": "Scénu nelze obnovit z tohoto souboru obrázku",
"invalidSceneUrl": "Nelze importovat scénu z zadané URL. Je buď poškozená, nebo neobsahuje platná JSON data Excalidraw.",
"resetLibrary": "Tímto vymažete vaši knihovnu. Jste si jisti?",
"removeItemsFromsLibrary": "Smazat {{count}} položek z knihovny?",
"invalidEncryptionKey": "Šifrovací klíč musí mít 22 znaků. Live spolupráce je zakázána.",
"collabOfflineWarning": "Není k dispozici žádné internetové připojení.\nVaše změny nebudou uloženy!"
"cannotRestoreFromImage": "",
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"collabOfflineWarning": ""
},
"errors": {
"unsupportedFileType": "Nepodporovaný typ souboru.",
"imageInsertError": "Nelze vložit obrázek. Zkuste to později...",
"fileTooBig": "Soubor je příliš velký. Maximální povolená velikost je {{maxSize}}.",
"svgImageInsertError": "Nelze vložit SVG obrázek. Značení SVG je neplatné.",
"invalidSVGString": "Neplatný SVG.",
"cannotResolveCollabServer": "Nelze se připojit ke sdílenému serveru. Prosím obnovte stránku a zkuste to znovu.",
"importLibraryError": "Nelze načíst knihovnu",
"collabSaveFailed": "Nelze uložit do databáze na serveru. Pokud problémy přetrvávají, měli byste uložit soubor lokálně, abyste se ujistili, že neztratíte svou práci.",
"collabSaveFailed_sizeExceeded": "Nelze uložit do databáze na serveru, plátno se zdá být příliš velké. Měli byste uložit soubor lokálně, abyste se ujistili, že neztratíte svou práci.",
"unsupportedFileType": "",
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": "",
"cannotResolveCollabServer": "",
"importLibraryError": "",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"line1": "Vypadá to, že používáte Brave prohlížeč s povoleným nastavením <bold>Aggressively Block Fingerprinting</bold>.",
"line2": "To by mohlo vést k narušení <bold>Textových elementů</bold> ve vašich výkresech.",
"line3": "Důrazně doporučujeme zakázat toto nastavení. Můžete sledovat <link>tyto kroky</link> jak to udělat.",
"line4": "Pokud vypnutí tohoto nastavení neopravuje zobrazení textových prvků, prosím, otevřete <issueLink>problém</issueLink> na našem GitHubu, nebo nám napište na <discordLink>Discord</discordLink>"
"line1": "",
"line2": "",
"line3": "",
"line4": ""
}
},
"toolBar": {
"selection": "Výběr",
"image": "Vložit obrázek",
"image": "",
"rectangle": "Obdélník",
"diamond": "Diamant",
"ellipse": "Elipsa",
@@ -216,69 +216,69 @@
"line": "Čára",
"freedraw": "Kreslení",
"text": "Text",
"library": "Knihovna",
"lock": "Po kreslení ponechat vybraný nástroj aktivní",
"penMode": "Režim Pera - zabránit dotyku",
"link": "Přidat/aktualizovat odkaz pro vybraný tvar",
"library": "",
"lock": "",
"penMode": "",
"link": "",
"eraser": "Guma",
"hand": "Ruka (nástroj pro posouvání)"
"hand": ""
},
"headings": {
"canvasActions": "Akce plátna",
"selectedShapeActions": "Akce vybraného tvaru",
"canvasActions": "",
"selectedShapeActions": "",
"shapes": "Tvary"
},
"hints": {
"canvasPanning": "Chcete-li přesunout plátno, podržte kolečko nebo mezerník při tažení nebo použijte nástroj Ruka",
"linearElement": "Kliknutím pro více bodů, táhnutím pro jednu čáru",
"freeDraw": "Klikněte a táhněte, pro ukončení pusťte",
"text": "Tip: Text můžete také přidat dvojitým kliknutím kdekoli pomocí nástroje pro výběr",
"text_selected": "Dvojklikem nebo stisknutím klávesy ENTER upravíte text",
"text_editing": "Stiskněte Escape nebo Ctrl/Cmd+ENTER pro dokončení úprav",
"linearElementMulti": "Klikněte na poslední bod nebo stiskněte Escape anebo Enter pro dokončení",
"lockAngle": "Úhel můžete omezit podržením SHIFT",
"resize": "Můžete omezit proporce podržením SHIFT při změně velikosti,\npodržte ALT pro změnu velikosti od středu",
"resizeImage": "Můžete volně změnit velikost podržením SHIFT,\npodržením klávesy ALT změníte velikosti od středu",
"rotate": "Úhly můžete omezit podržením SHIFT při otáčení",
"lineEditor_info": "Podržte Ctrl/Cmd a dvakrát klikněte nebo stiskněte Ctrl/Cmd + Enter pro úpravu bodů",
"lineEditor_pointSelected": "Stisknutím tlačítka Delete odstraňte bod(y),\nCtrl/Cmd+D pro duplicitu nebo táhnutím pro přesun",
"lineEditor_nothingSelected": "Vyberte bod, který chcete upravit (podržením klávesy SHIFT vyberete více položek),\nnebo podržením klávesy Alt a kliknutím přidáte nové body",
"placeImage": "Kliknutím umístěte obrázek, nebo klepnutím a přetažením ručně nastavíte jeho velikost",
"publishLibrary": "Publikovat vlastní knihovnu",
"bindTextToElement": "Stiskněte Enter pro přidání textu",
"deepBoxSelect": "Podržte Ctrl/Cmd pro hluboký výběr a pro zabránění táhnutí",
"eraserRevert": "Podržením klávesy Alt vrátíte prvky označené pro smazání",
"firefox_clipboard_write": "Tato funkce může být povolena nastavením vlajky \"dom.events.asyncClipboard.clipboardItem\" na \"true\". Chcete-li změnit vlajky prohlížeče ve Firefoxu, navštivte stránku \"about:config\"."
"canvasPanning": "",
"linearElement": "",
"freeDraw": "",
"text": "",
"text_selected": "",
"text_editing": "",
"linearElementMulti": "",
"lockAngle": "",
"resize": "",
"resizeImage": "",
"rotate": "",
"lineEditor_info": "",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Náhled nelze zobrazit",
"canvasTooBig": "Plátno je možná příliš velké.",
"canvasTooBigTip": "Tip: zkus posunout nejvzdálenější prvky trochu blíže k sobě."
"cannotShowPreview": "",
"canvasTooBig": "",
"canvasTooBigTip": ""
},
"errorSplash": {
"headingMain": "Chyba. Zkuste <button>znovu načíst stránku</button>.",
"clearCanvasMessage": "Pokud opětovné načtení nefunguje, zkuste <button>vymazat plátno</button>.",
"clearCanvasCaveat": " To povede ke ztrátě dat ",
"headingMain": "",
"clearCanvasMessage": "",
"clearCanvasCaveat": "",
"trackedToSentry": "Chyba identifikátoru {{eventId}} byl zaznamenán v našem systému.",
"openIssueMessage": "Byli jsme velmi opatrní, abychom neuváděli informace o Vaší scéně. Pokud vaše scéna není soukromá, zvažte prosím sledování na našem <button>bug trackeru</button>. Uveďte prosím níže uvedené informace kopírováním a vložením do problému na GitHubu.",
"sceneContent": "Obsah scény:"
"openIssueMessage": "",
"sceneContent": ""
},
"roomDialog": {
"desc_intro": "Můžete pozvat lidi na vaši aktuální scénu ke spolupráci s vámi.",
"desc_privacy": "Nebojte se, relace používá end-to-end šifrování, takže cokoliv nakreslíte zůstane soukromé. Ani náš server nebude schopen vidět, s čím budete pracovat.",
"button_startSession": "Zahájit relaci",
"button_stopSession": "Ukončit relaci",
"desc_inProgressIntro": "Živá spolupráce právě probíhá.",
"desc_shareLink": "Sdílejte tento odkaz s každým, s kým chcete spolupracovat:",
"desc_exitSession": "Zastavením relace se odpojíte od místnosti, ale budete moci pokračovat v práci s touto scénou lokálně. Všimněte si, že to nebude mít vliv na ostatní lidi a budou stále moci spolupracovat na jejich verzi.",
"shareTitle": "Připojte se k aktivní spolupráci na Excalidraw"
"desc_intro": "",
"desc_privacy": "",
"button_startSession": "",
"button_stopSession": "",
"desc_inProgressIntro": "",
"desc_shareLink": "",
"desc_exitSession": "",
"shareTitle": ""
},
"errorDialog": {
"title": "Chyba"
"title": ""
},
"exportDialog": {
"disk_title": "Uložit na disk",
"disk_details": "Exportovat data scény do souboru, ze kterého můžete importovat později.",
"disk_title": "",
"disk_details": "",
"disk_button": "Uložit do souboru",
"link_title": "Odkaz pro sdílení",
"link_details": "Exportovat jako odkaz pouze pro čtení.",
@@ -290,18 +290,18 @@
"helpDialog": {
"blog": "Přečtěte si náš blog",
"click": "kliknutí",
"deepSelect": "Hluboký výběr",
"deepBoxSelect": "Hluboký výběr uvnitř boxu a zabránění táhnnutí",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "Zakřivená šipka",
"curvedLine": "Zakřivená čára",
"documentation": "Dokumentace",
"doubleClick": "dvojklik",
"drag": "tažení",
"editor": "Editor",
"editLineArrowPoints": "Upravit body linií/šipek",
"editText": "Upravit text / přidat popis",
"github": "Našel jsi problém? Nahlaš ho",
"howto": "Sledujte naše návody",
"editLineArrowPoints": "",
"editText": "",
"github": "",
"howto": "",
"or": "nebo",
"preventBinding": "Zabránit vázání šipky",
"tools": "Nástroje",
@@ -313,8 +313,8 @@
"zoomToFit": "Přiblížit na zobrazení všech prvků",
"zoomToSelection": "Přiblížit na výběr",
"toggleElementLock": "Zamknout/odemknout výběr",
"movePageUpDown": "Posunout stránku nahoru/dolů",
"movePageLeftRight": "Přesunout stránku doleva/doprava"
"movePageUpDown": "",
"movePageLeftRight": ""
},
"clearCanvasDialog": {
"title": "Vymazat plátno"
@@ -342,46 +342,46 @@
},
"noteDescription": "Odešlete svou knihovnu, pro zařazení do <link>veřejného úložiště knihoven</link>, odkud ji budou moci při kreslení využít i ostatní uživatelé.",
"noteGuidelines": "Knihovna musí být nejdříve ručně schválena. Přečtěte si prosím <link>pokyny</link>",
"noteLicense": "Odesláním souhlasíte s tím, že knihovna bude zveřejněna pod <link>MIT licencí</link>, stručně řečeno, kdokoli ji může používat bez omezení.",
"noteItems": "Každá položka knihovny musí mít svůj vlastní název, aby byla filtrovatelná. Následující položky knihovny budou zahrnuty:",
"atleastOneLibItem": "Vyberte alespoň jednu položku knihovny, kterou chcete začít",
"republishWarning": "Poznámka: některé z vybraných položek jsou označeny jako již zveřejněné/odeslané. Položky byste měli znovu odeslat pouze při aktualizaci existující knihovny nebo podání."
"noteLicense": "",
"noteItems": "",
"atleastOneLibItem": "",
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "Knihovna byla odeslána",
"content": "Děkujeme vám {{authorName}}. Vaše knihovna byla odeslána k posouzení. Stav můžete sledovat <link>zde</link>"
"content": ""
},
"confirmDialog": {
"resetLibrary": "Resetovat knihovnu",
"removeItemsFromLib": "Odstranit vybrané položky z knihovny"
"resetLibrary": "",
"removeItemsFromLib": ""
},
"imageExportDialog": {
"header": "Exportovat obrázek",
"header": "",
"label": {
"withBackground": "Pozadí",
"onlySelected": "Pouze vybrané",
"darkMode": "Tmavý režim",
"embedScene": "Vložit scénu",
"scale": "Měřítko",
"padding": "Odsazení"
"withBackground": "",
"onlySelected": "",
"darkMode": "",
"embedScene": "",
"scale": "",
"padding": ""
},
"tooltip": {
"embedScene": "Data scény budou uložena do exportovaného souboru PNG/SVG tak, aby z něj mohla být scéna obnovena.\nZvýší se velikost exportovaného souboru."
"embedScene": ""
},
"title": {
"exportToPng": "Exportovat do PNG",
"exportToSvg": "Exportovat do SVG",
"copyPngToClipboard": "Kopírovat PNG do schránky"
"exportToPng": "",
"exportToSvg": "",
"copyPngToClipboard": ""
},
"button": {
"exportToPng": "PNG",
"exportToSvg": "SVG",
"copyPngToClipboard": "Kopírovat do schránky"
"exportToPng": "",
"exportToSvg": "",
"copyPngToClipboard": ""
}
},
"encrypted": {
"tooltip": "Vaše kresby jsou end-to-end šifrované, takže servery Excalidraw je nikdy neuvidí.",
"link": "Blog příspěvek na end-to-end šifrování v Excalidraw"
"tooltip": "",
"link": ""
},
"stats": {
"angle": "Úhel",
@@ -402,48 +402,48 @@
"addedToLibrary": "Přidáno do knihovny",
"copyStyles": "Styly byly zkopírovány.",
"copyToClipboard": "Zkopírováno do schránky.",
"copyToClipboardAsPng": "{{exportSelection}} zkopírován do schránky jako PNG\n({{exportColorScheme}})",
"copyToClipboardAsPng": "",
"fileSaved": "Soubor byl uložen.",
"fileSavedToFilename": "Uloženo do {filename}",
"canvas": "plátno",
"selection": "výběr",
"pasteAsSingleElement": "Pomocí {{shortcut}} vložte jako jeden prvek,\nnebo vložte do existujícího textového editoru"
"pasteAsSingleElement": ""
},
"colors": {
"transparent": "Průhledná",
"black": "Černá",
"white": "Bílá",
"red": "Červená",
"pink": "Růžová",
"grape": "Vínová",
"violet": "Fialová",
"gray": "Šedá",
"blue": "Modrá",
"cyan": "Azurová",
"teal": "Modrozelená",
"green": "Zelená",
"yellow": "Žlutá",
"orange": "Oranžová",
"bronze": "Bronzová"
"black": "",
"white": "",
"red": "",
"pink": "",
"grape": "",
"violet": "",
"gray": "",
"blue": "",
"cyan": "",
"teal": "",
"green": "",
"yellow": "",
"orange": "",
"bronze": ""
},
"welcomeScreen": {
"app": {
"center_heading": "Všechna vaše data jsou uložena lokálně ve vašem prohlížeči.",
"center_heading_plus": "Chcete místo toho přejít na Excalidraw+?",
"menuHint": "Export, nastavení, jazyky, ..."
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "Export, nastavení a další...",
"center_heading": "Diagramy. Vytvořeny. Jednoduše.",
"toolbarHint": "Vyberte nástroj a začněte kreslit!",
"helpHint": "Zkratky a pomoc"
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
},
"colorPicker": {
"mostUsedCustomColors": "Nejpoužívanější vlastní barvy",
"colors": "Barvy",
"shades": "Stíny",
"hexCode": "Hex kód",
"noShades": "Pro tuto barvu nejsou k dispozici žádné odstíny"
"mostUsedCustomColors": "",
"colors": "",
"shades": "",
"hexCode": "",
"noShades": ""
}
}
+1 -1
View File
@@ -124,7 +124,7 @@
},
"statusPublished": "Veröffentlicht",
"sidebarLock": "Seitenleiste offen lassen",
"eyeDropper": "Farbe von der Zeichenfläche auswählen"
"eyeDropper": ""
},
"library": {
"noItems": "Noch keine Elemente hinzugefügt...",
+1 -5
View File
@@ -124,8 +124,6 @@
},
"statusPublished": "Published",
"sidebarLock": "Keep sidebar open",
"selectAllElementsInFrame": "Select all elements in frame",
"removeAllElementsFromFrame": "Remove all elements from frame",
"eyeDropper": "Pick color from canvas"
},
"library": {
@@ -223,9 +221,7 @@
"penMode": "Pen mode - prevent touch",
"link": "Add/ Update link for a selected shape",
"eraser": "Eraser",
"frame": "Frame tool",
"hand": "Hand (panning tool)",
"extraTools": "More tools"
"hand": "Hand (panning tool)"
},
"headings": {
"canvasActions": "Canvas actions",
+1 -1
View File
@@ -124,7 +124,7 @@
},
"statusPublished": "प्रकाशित",
"sidebarLock": "साइडबार खुला रखे.",
"eyeDropper": "चित्रफलक से रंग चुने"
"eyeDropper": ""
},
"library": {
"noItems": "अभी तक कोई आइटम जोडा नहीं गया.",
+1 -1
View File
@@ -124,7 +124,7 @@
},
"statusPublished": "게시됨",
"sidebarLock": "사이드바 유지",
"eyeDropper": "캔버스에서 색상 고르기"
"eyeDropper": ""
},
"library": {
"noItems": "추가된 아이템 없음",
+1 -1
View File
@@ -124,7 +124,7 @@
},
"statusPublished": "प्रकाशित करा",
"sidebarLock": "साइडबार उघडं ठेवा",
"eyeDropper": "चित्रफलकातून रंग निवडा"
"eyeDropper": ""
},
"library": {
"noItems": "अजून कोणतेही आइटम जोडलेले नाही...",
+1 -1
View File
@@ -124,7 +124,7 @@
},
"statusPublished": "Publisert",
"sidebarLock": "Holde sidemenyen åpen",
"eyeDropper": "Velg farge fra lerretet"
"eyeDropper": ""
},
"library": {
"noItems": "Ingen elementer lagt til ennå...",
+14 -15
View File
@@ -1,12 +1,11 @@
{
"ar-SA": 80,
"az-AZ": 20,
"bg-BG": 54,
"bn-BD": 60,
"ca-ES": 88,
"cs-CZ": 100,
"cs-CZ": 64,
"da-DK": 33,
"de-DE": 100,
"de-DE": 99,
"el-GR": 93,
"en": 100,
"es-ES": 89,
@@ -25,32 +24,32 @@
"kab-KAB": 88,
"kk-KZ": 21,
"km-KH": 95,
"ko-KR": 100,
"ko-KR": 99,
"ku-TR": 90,
"lt-LT": 56,
"lv-LV": 89,
"mr-IN": 96,
"mr-IN": 95,
"my-MM": 41,
"nb-NO": 100,
"nb-NO": 99,
"nl-NL": 83,
"nn-NO": 77,
"oc-FR": 87,
"pa-IN": 90,
"pl-PL": 100,
"pl-PL": 99,
"pt-BR": 99,
"pt-PT": 95,
"ro-RO": 100,
"ru-RU": 100,
"ro-RO": 99,
"ru-RU": 99,
"si-LK": 9,
"sk-SK": 100,
"sl-SI": 100,
"sv-SE": 100,
"ta-IN": 85,
"sk-SK": 99,
"sl-SI": 99,
"sv-SE": 99,
"ta-IN": 83,
"th-TH": 39,
"tr-TR": 87,
"uk-UA": 97,
"vi-VN": 56,
"zh-CN": 100,
"zh-CN": 95,
"zh-HK": 26,
"zh-TW": 100
"zh-TW": 99
}
+1 -1
View File
@@ -124,7 +124,7 @@
},
"statusPublished": "Opublikowano",
"sidebarLock": "Panel boczny zawsze otwarty",
"eyeDropper": "Wybierz kolor z płótna"
"eyeDropper": ""
},
"library": {
"noItems": "Nie dodano jeszcze żadnych elementów...",
+1 -1
View File
@@ -363,7 +363,7 @@
"darkMode": "",
"embedScene": "Cena embutida",
"scale": "",
"padding": "Espaçamento"
"padding": ""
},
"tooltip": {
"embedScene": ""
+1 -1
View File
@@ -124,7 +124,7 @@
},
"statusPublished": "Publicat",
"sidebarLock": "Păstrează deschisă bara laterală",
"eyeDropper": "Alegere culoare din pânză"
"eyeDropper": ""
},
"library": {
"noItems": "Niciun element adăugat încă...",
+1 -1
View File
@@ -124,7 +124,7 @@
},
"statusPublished": "Опубликовано",
"sidebarLock": "Держать боковую панель открытой",
"eyeDropper": "Взять образец цвета с холста"
"eyeDropper": ""
},
"library": {
"noItems": "Пока ничего не добавлено...",
+1 -1
View File
@@ -124,7 +124,7 @@
},
"statusPublished": "Zverejnené",
"sidebarLock": "Nechať bočný panel otvorený",
"eyeDropper": "Vybrať farbu z plátna"
"eyeDropper": ""
},
"library": {
"noItems": "Zatiaľ neboli pridané žiadne položky...",
+1 -1
View File
@@ -124,7 +124,7 @@
},
"statusPublished": "Objavljeno",
"sidebarLock": "Obdrži stransko vrstico odprto",
"eyeDropper": "Izberi barvo s platna"
"eyeDropper": ""
},
"library": {
"noItems": "Dodan še ni noben element...",
+1 -1
View File
@@ -124,7 +124,7 @@
},
"statusPublished": "Publicerad",
"sidebarLock": "Håll sidofältet öppet",
"eyeDropper": "Välj färg från canvas"
"eyeDropper": ""
},
"library": {
"noItems": "Inga objekt tillagda ännu...",
+10 -10
View File
@@ -50,7 +50,7 @@
"veryLarge": "மிகப் பெரிய",
"solid": "திடமான",
"hachure": "மலைக்குறிக்கோடு",
"zigzag": "கோணல்மாணல்",
"zigzag": "",
"crossHatch": "குறுக்குகதவு",
"thin": "மெல்லிய",
"bold": "பட்டை",
@@ -105,8 +105,8 @@
"decreaseFontSize": "எழுத்துரு அளவைக் குறை",
"increaseFontSize": "எழுத்துரு அளவை அதிகரி",
"unbindText": "உரையைப் பிணைவவிழ்",
"bindText": "உரையைக் கொள்கலனுக்குப் பிணை",
"createContainerFromText": "உரையைக் கொள்கலனுள் சுருட்டு",
"bindText": "",
"createContainerFromText": "",
"link": {
"edit": "தொடுப்பைத் திருத்து",
"create": "தொடுப்பைப் படை",
@@ -114,7 +114,7 @@
},
"lineEditor": {
"edit": "தொடுப்பைத் திருத்து",
"exit": "வரி திருத்தியிலிருந்து வெளியேறு"
"exit": ""
},
"elementLock": {
"lock": "பூட்டு",
@@ -124,7 +124,7 @@
},
"statusPublished": "வெளியிடப்பட்டது",
"sidebarLock": "பக்கப்பட்டையைத் திறந்தே வை",
"eyeDropper": "கித்தானிலிருந்து நிறம் தேர்ந்தெடு"
"eyeDropper": ""
},
"library": {
"noItems": "இதுவரை உருப்படிகள் சேரக்கப்படவில்லை...",
@@ -134,7 +134,7 @@
"buttons": {
"clearReset": "கித்தானை அகரமாக்கு",
"exportJSON": "கோப்புக்கு ஏற்றுமதிசெய்",
"exportImage": "படத்தை ஏற்றுமதிசெய்...",
"exportImage": "",
"export": "இதில் சேமி...",
"copyToClipboard": "நகலகத்திற்கு நகலெடு",
"save": "தற்போதைய கோப்புக்குச் சேமி",
@@ -187,7 +187,7 @@
"resetLibrary": "இது உங்கள் நுலகத்தைத் துடைக்கும். நீங்கள் உறுதியா?",
"removeItemsFromsLibrary": "{{count}} உருப்படி(கள்)-ஐ உம் நூலகத்திலிருந்து அழிக்கவா?",
"invalidEncryptionKey": "மறையாக்க விசை 22 வரியுருக்கள் கொண்டிருக்கவேண்டும். நேரடி கூட்டுப்பணி முடக்கப்பட்டது.",
"collabOfflineWarning": "இணைய இணைப்பு இல்லை.\nஉமது மாற்றங்கள் சேமிக்கப்படா!"
"collabOfflineWarning": ""
},
"errors": {
"unsupportedFileType": "ஆதரிக்கப்படா கோப்பு வகை.",
@@ -195,10 +195,10 @@
"fileTooBig": "கோப்பு மிகப்பெரிது. அனுமதிக்கப்பட்ட அதிகபட்ச அளவு {{maxSize}}.",
"svgImageInsertError": "எஸ்விஜி படத்தைப் புகுத்தவியலா. எஸ்விஜியின் மார்க்அப் செல்லாததாக தெரிகிறது.",
"invalidSVGString": "செல்லாத SVG.",
"cannotResolveCollabServer": "கூட்டுப்பணிச் சேவையகத்துடன் இணைக்க முடியவில்லை. பக்கத்தை மீளேற்றி மீண்டும் முயலவும்.",
"cannotResolveCollabServer": "",
"importLibraryError": "நூலகத்தை ஏற்ற முடியவில்லை",
"collabSaveFailed": "பின்முனை தரவுத்தளத்தில் சேமிக்க முடியவில்லை. சிக்கல்கள் நீடித்தால், உமது வேலைகளை இழக்காமலிருப்பதை உறுதிசெய்ய உமது கோப்பை உள்ளகத்தில் சேமிக்க வேண்டும்.",
"collabSaveFailed_sizeExceeded": "பின்முனை தரவுத்தளத்தில் சேமிக்க முடியவில்லை, கித்தான் மிகப்பெரிதாகத் தெரிகிறது. உமது வேலைகளை இழக்காமலிருப்பதை உறுதிசெய்ய உமது கோப்பை உள்ளகத்தில் சேமிக்க வேண்டும்.",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"line1": "",
"line2": "",
+20 -20
View File
@@ -124,7 +124,7 @@
},
"statusPublished": "已发布",
"sidebarLock": "侧边栏常驻",
"eyeDropper": "从画布上取色"
"eyeDropper": ""
},
"library": {
"noItems": "尚未添加任何项目……",
@@ -266,12 +266,12 @@
"roomDialog": {
"desc_intro": "你可以邀请其他人到目前的画面中与你协作。",
"desc_privacy": "别担心,该会话使用端到端加密,无论绘制什么都将保持私密,甚至连我们的服务器也无法查看。",
"button_startSession": "开始会话",
"button_stopSession": "结束会",
"desc_inProgressIntro": "实时协作会进行。",
"button_startSession": "启动会议",
"button_stopSession": "结束会",
"desc_inProgressIntro": "实时协作会议正在进行。",
"desc_shareLink": "分享此链接给你要协作的用户",
"desc_exitSession": "停止会话将中断您与房间的连接,但您依然可以在本地继续使用画布请注意,这不会影响到其他用户他们仍可以在他们的版本上继续协作。",
"shareTitle": "加入 Excalidraw 实时协作会"
"desc_exitSession": "停止会话将中断您与房间的连接,但您依然可以在本地继续使用画布. 请注意,这不会影响到其他用户, 他们仍可以在他们的版本上继续协作。",
"shareTitle": "加入 Excalidraw 实时协作会"
},
"errorDialog": {
"title": "错误"
@@ -356,27 +356,27 @@
"removeItemsFromLib": "从素材库中删除选中的项目"
},
"imageExportDialog": {
"header": "导出图片",
"header": "",
"label": {
"withBackground": "背景",
"onlySelected": "仅选中",
"darkMode": "深色模式",
"embedScene": "包含画布数据",
"scale": "缩放比例",
"padding": "内边距"
"withBackground": "",
"onlySelected": "",
"darkMode": "",
"embedScene": "",
"scale": "",
"padding": ""
},
"tooltip": {
"embedScene": "画布数据将被保存到导出的 PNG/SVG 文件,以便恢复。\n将会增加导出文件的大小。"
"embedScene": ""
},
"title": {
"exportToPng": "导出为 PNG",
"exportToSvg": "导出为 SVG",
"copyPngToClipboard": "复制 PNG 到剪切板"
"exportToPng": "",
"exportToSvg": "",
"copyPngToClipboard": ""
},
"button": {
"exportToPng": "PNG",
"exportToSvg": "SVG",
"copyPngToClipboard": "复制到剪贴板"
"exportToPng": "",
"exportToSvg": "",
"copyPngToClipboard": ""
}
},
"encrypted": {
+1 -1
View File
@@ -124,7 +124,7 @@
},
"statusPublished": "已發布",
"sidebarLock": "側欄維持開啟",
"eyeDropper": "從畫布中選取顏色"
"eyeDropper": ""
},
"library": {
"noItems": "尚未加入任何物件...",
+1 -1
View File
@@ -206,7 +206,7 @@ export const isPointInPolygon = (
// Returns whether `q` lies inside the segment/rectangle defined by `p` and `r`.
// This is an approximation to "does `q` lie on a segment `pr`" check.
export const isPointWithinBounds = (p: Point, q: Point, r: Point) => {
const isPointWithinBounds = (p: Point, q: Point, r: Point) => {
return (
q[0] <= Math.max(p[0], r[0]) &&
q[0] >= Math.min(p[0], r[0]) &&
-1
View File
@@ -205,7 +205,6 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
height: 141.9765625,
seed: 1968410350,
groupIds: [],
frameId: null,
boundElements: null,
locked: false,
link: null,
@@ -20,7 +20,6 @@ export default {
height: 141.9765625,
seed: 1968410350,
groupIds: [],
frameId: null,
},
{
id: "-xMIs_0jIFqvpx-R9UnaG",
@@ -38,7 +37,6 @@ export default {
roughness: 1,
opacity: 100,
groupIds: [],
frameId: null,
seed: 957947807,
version: 47,
versionNonce: 1128618623,
@@ -60,7 +58,6 @@ export default {
roughness: 1,
opacity: 100,
groupIds: [],
frameId: null,
strokeSharpness: "round",
seed: 707269846,
version: 143,
@@ -97,7 +94,6 @@ export default {
height: 103.65107323746608,
seed: 1445523839,
groupIds: [],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
startBinding: null,
@@ -137,7 +133,6 @@ export default {
height: 113.8575037534261,
seed: 1513238033,
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
startBinding: null,
@@ -187,7 +182,6 @@ export default {
height: 9.797916664247975,
seed: 683951089,
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
startBinding: null,
@@ -226,7 +220,6 @@ export default {
height: 9.797916664247975,
seed: 1817746897,
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
startBinding: null,
@@ -265,7 +258,6 @@ export default {
height: 17.72670397681366,
seed: 1409727409,
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: ["bxuMGTzXLn7H-uBCptINx"],
},
@@ -289,7 +281,6 @@ export default {
height: 13.941904362416096,
seed: 1073094033,
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -313,7 +304,6 @@ export default {
height: 13.941904362416096,
seed: 526271345,
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -337,7 +327,6 @@ export default {
height: 13.941904362416096,
seed: 243707217,
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -363,7 +352,6 @@ export default {
height: 36.77344700318558,
seed: 511870335,
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -387,7 +375,6 @@ export default {
height: 36.77344700318558,
seed: 1283079231,
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -411,7 +398,6 @@ export default {
height: 36.77344700318558,
seed: 996251633,
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -435,7 +421,6 @@ export default {
height: 36.77344700318558,
seed: 1764842481,
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -461,7 +446,6 @@ export default {
height: 154.56722543646003,
seed: 1424381745,
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
startBinding: null,
@@ -511,7 +495,6 @@ export default {
height: 12.698053371678215,
seed: 726657713,
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
startBinding: null,
@@ -550,7 +533,6 @@ export default {
height: 10.178760037658167,
seed: 1977326481,
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
startBinding: null,
@@ -589,7 +571,6 @@ export default {
height: 22.797152568995934,
seed: 1774660383,
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: ["bxuMGTzXLn7H-uBCptINx"],
},
@@ -615,7 +596,6 @@ export default {
height: 107.25081879410921,
seed: 371096063,
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [
"CFu0B4Mw_1wC1Hbgx8Fs0",
@@ -643,7 +623,6 @@ export default {
height: 107.25081879410921,
seed: 685932433,
groupIds: ["0RJwA-yKP5dqk5oMiSeot", "9ppmKFUbA4iKjt8FaDFox"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [
"CFu0B4Mw_1wC1Hbgx8Fs0",
@@ -671,7 +650,6 @@ export default {
height: 107.25081879410921,
seed: 58634943,
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [
"CFu0B4Mw_1wC1Hbgx8Fs0",
@@ -699,7 +677,6 @@ export default {
height: 3.249953844290203,
seed: 1673003743,
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
points: [
@@ -731,7 +708,6 @@ export default {
height: 2.8032978840147194,
seed: 1821527807,
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
points: [
@@ -763,7 +739,6 @@ export default {
height: 4.280657518731036,
seed: 1485707039,
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
points: [
@@ -796,7 +771,6 @@ export default {
height: 2.9096445412231735,
seed: 1042012991,
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
points: [
@@ -830,7 +804,6 @@ export default {
height: 2.4757501798128,
seed: 295443295,
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
points: [
@@ -862,7 +835,6 @@ export default {
height: 2.4757501798128,
seed: 1734301567,
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
points: [
@@ -897,7 +869,6 @@ export default {
height: 76.53703389977764,
seed: 106569279,
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -921,7 +892,6 @@ export default {
height: 0,
seed: 73916127,
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
startBinding: null,
@@ -954,7 +924,6 @@ export default {
height: 5.001953125,
seed: 387857791,
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -978,7 +947,6 @@ export default {
height: 5.001953125,
seed: 1486370207,
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -1002,7 +970,6 @@ export default {
height: 5.001953125,
seed: 610150847,
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -1026,7 +993,6 @@ export default {
height: 42.72020253937572,
seed: 144280593,
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -1050,7 +1016,6 @@ export default {
height: 24.44112284281997,
seed: 29167967,
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
startBinding: null,
@@ -1103,7 +1068,6 @@ export default {
height: 0,
seed: 1443027377,
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
startBinding: null,
@@ -1136,7 +1100,6 @@ export default {
height: 5.711199931375845,
seed: 244310513,
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
startBinding: null,
@@ -1175,7 +1138,6 @@ export default {
height: 44.82230388130942,
seed: 683572113,
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -1199,7 +1161,6 @@ export default {
height: 5.896061363392446,
seed: 318798801,
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
frameId: null,
strokeSharpness: "round",
boundElementIds: [],
startBinding: null,
@@ -1239,7 +1200,6 @@ export default {
height: 108.30428902193904,
seed: 1914896753,
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -1263,7 +1223,6 @@ export default {
height: 82.83278895375764,
seed: 1306468145,
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -1287,7 +1246,6 @@ export default {
height: 11.427824006438863,
seed: 93422161,
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -1311,7 +1269,6 @@ export default {
height: 19.889460471185775,
seed: 11646495,
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -1335,7 +1292,6 @@ export default {
height: 19.889460471185775,
seed: 291717649,
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
frameId: null,
strokeSharpness: "sharp",
boundElementIds: [],
},
@@ -44,8 +44,7 @@ module.exports = {
},
{
test: /\.(ts|tsx|js|jsx|mjs)$/,
exclude:
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
exclude: /node_modules\/(?!browser-fs-access)/,
use: [
{
loader: "ts-loader",
@@ -46,9 +46,7 @@ module.exports = {
},
{
test: /\.(ts|tsx|js|jsx|mjs)$/,
exclude:
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
exclude: /node_modules\/(?!browser-fs-access)/,
use: [
{
loader: "ts-loader",
+29 -134
View File
@@ -34,7 +34,6 @@ import { AppState, BinaryFiles, Zoom } from "../types";
import { getDefaultAppState } from "../appState";
import {
BOUND_TEXT_PADDING,
FRAME_STYLE,
MAX_DECIMALS_FOR_SVG_EXPORT,
MIME_TYPES,
SVG_NS,
@@ -49,7 +48,6 @@ import {
getBoundTextMaxWidth,
} from "../element/textElement";
import { LinearElementEditor } from "../element/linearElementEditor";
import { getContainingFrame } from "../frame";
// 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
@@ -94,7 +92,6 @@ export interface ExcalidrawElementWithCanvas {
canvasOffsetX: number;
canvasOffsetY: number;
boundTextElementVersion: number | null;
containingFrameOpacity: number;
}
const cappedElementCanvasSize = (
@@ -210,7 +207,6 @@ const generateElementCanvas = (
canvasOffsetX,
canvasOffsetY,
boundTextElementVersion: getBoundTextElement(element)?.version || null,
containingFrameOpacity: getContainingFrame(element)?.opacity || 100,
};
};
@@ -257,8 +253,7 @@ const drawElementOnCanvas = (
context: CanvasRenderingContext2D,
renderConfig: RenderConfig,
) => {
context.globalAlpha =
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
context.globalAlpha = element.opacity / 100;
switch (element.type) {
case "rectangle":
case "diamond":
@@ -474,7 +469,7 @@ const generateElementShape = (
elementWithCanvasCache.delete(element);
switch (element.type) {
case "rectangle": {
case "rectangle":
if (element.roundness) {
const w = element.width;
const h = element.height;
@@ -499,7 +494,6 @@ const generateElementShape = (
setShapeForElement(element, shape);
break;
}
case "diamond": {
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
getDiamondPoints(element);
@@ -723,14 +717,12 @@ const generateElementWithCanvas = (
prevElementWithCanvas.zoomValue !== zoom.value &&
!renderConfig?.shouldCacheIgnoreZoom;
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
const containingFrameOpacity = getContainingFrame(element)?.opacity || 100;
if (
!prevElementWithCanvas ||
shouldRegenerateBecauseZoom ||
prevElementWithCanvas.theme !== renderConfig.theme ||
prevElementWithCanvas.boundTextElementVersion !== boundTextElementVersion ||
prevElementWithCanvas.containingFrameOpacity !== containingFrameOpacity
prevElementWithCanvas.boundTextElementVersion !== boundTextElementVersion
) {
const elementWithCanvas = generateElementCanvas(
element,
@@ -905,59 +897,25 @@ export const renderElement = (
const generator = rc.generator;
switch (element.type) {
case "selection": {
// do not render selection when exporting
if (!renderConfig.isExporting) {
context.save();
context.translate(
element.x + renderConfig.scrollX,
element.y + renderConfig.scrollY,
);
context.fillStyle = "rgba(0, 0, 200, 0.04)";
context.save();
context.translate(
element.x + renderConfig.scrollX,
element.y + renderConfig.scrollY,
);
context.fillStyle = "rgba(0, 0, 200, 0.04)";
// render from 0.5px offset to get 1px wide line
// https://stackoverflow.com/questions/7530593/html5-canvas-and-line-width/7531540#7531540
// TODO can be be improved by offseting to the negative when user selects
// from right to left
const offset = 0.5 / renderConfig.zoom.value;
// render from 0.5px offset to get 1px wide line
// https://stackoverflow.com/questions/7530593/html5-canvas-and-line-width/7531540#7531540
// TODO can be be improved by offseting to the negative when user selects
// from right to left
const offset = 0.5 / renderConfig.zoom.value;
context.fillRect(offset, offset, element.width, element.height);
context.lineWidth = 1 / renderConfig.zoom.value;
context.strokeStyle = " rgb(105, 101, 219)";
context.strokeRect(offset, offset, element.width, element.height);
context.fillRect(offset, offset, element.width, element.height);
context.lineWidth = 1 / renderConfig.zoom.value;
context.strokeStyle = "rgb(105, 101, 219)";
context.strokeRect(offset, offset, element.width, element.height);
context.restore();
}
break;
}
case "frame": {
if (!renderConfig.isExporting && appState.shouldRenderFrames) {
context.save();
context.translate(
element.x + renderConfig.scrollX,
element.y + renderConfig.scrollY,
);
context.fillStyle = "rgba(0, 0, 200, 0.04)";
context.lineWidth = 2 / renderConfig.zoom.value;
context.strokeStyle = FRAME_STYLE.strokeColor;
if (FRAME_STYLE.radius && context.roundRect) {
context.beginPath();
context.roundRect(
0,
0,
element.width,
element.height,
FRAME_STYLE.radius / renderConfig.zoom.value,
);
context.stroke();
context.closePath();
} else {
context.strokeRect(0, 0, element.width, element.height);
}
context.restore();
}
context.restore();
break;
}
case "freedraw": {
@@ -1149,23 +1107,6 @@ const roughSVGDrawWithPrecision = (
return rsvg.draw(pshape);
};
const maybeWrapNodesInFrameClipPath = (
element: NonDeletedExcalidrawElement,
root: SVGElement,
nodes: SVGElement[],
exportedFrameId?: string | null,
) => {
const frame = getContainingFrame(element);
if (frame && frame.id === exportedFrameId) {
const g = root.ownerDocument!.createElementNS(SVG_NS, "g");
g.setAttributeNS(SVG_NS, "clip-path", `url(#${frame.id})`);
nodes.forEach((node) => g.appendChild(node));
return g;
}
return null;
};
export const renderElementToSvg = (
element: NonDeletedExcalidrawElement,
rsvg: RoughSVG,
@@ -1174,7 +1115,6 @@ export const renderElementToSvg = (
offsetX: number,
offsetY: number,
exportWithDarkMode?: boolean,
exportingFrameId?: string | null,
) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
let cx = (x2 - x1) / 2 - (element.x - x1);
@@ -1208,9 +1148,6 @@ export const renderElementToSvg = (
root = anchorTag;
}
const opacity =
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
switch (element.type) {
case "selection": {
// Since this is used only during editing experience, which is canvas based,
@@ -1226,6 +1163,7 @@ export const renderElementToSvg = (
getShapeForElement(element)!,
MAX_DECIMALS_FOR_SVG_EXPORT,
);
const opacity = element.opacity / 100;
if (opacity !== 1) {
node.setAttribute("stroke-opacity", `${opacity}`);
node.setAttribute("fill-opacity", `${opacity}`);
@@ -1237,15 +1175,7 @@ export const renderElementToSvg = (
offsetY || 0
}) rotate(${degree} ${cx} ${cy})`,
);
const g = maybeWrapNodesInFrameClipPath(
element,
root,
[node],
exportingFrameId,
);
g ? root.appendChild(g) : root.appendChild(node);
root.appendChild(node);
break;
}
case "line":
@@ -1298,6 +1228,7 @@ export const renderElementToSvg = (
if (boundText) {
group.setAttribute("mask", `url(#mask-${element.id})`);
}
const opacity = element.opacity / 100;
group.setAttribute("stroke-linecap", "round");
getShapeForElement(element)!.forEach((shape) => {
@@ -1325,24 +1256,14 @@ export const renderElementToSvg = (
}
group.appendChild(node);
});
const g = maybeWrapNodesInFrameClipPath(
element,
root,
[group, maskPath],
exportingFrameId,
);
if (g) {
root.appendChild(g);
} else {
root.appendChild(group);
root.append(maskPath);
}
root.appendChild(group);
root.append(maskPath);
break;
}
case "freedraw": {
generateElementShape(element, generator);
generateFreeDrawShape(element);
const opacity = element.opacity / 100;
const shape = getShapeForElement(element);
const node = shape
? roughSVGDrawWithPrecision(rsvg, shape, MAX_DECIMALS_FOR_SVG_EXPORT)
@@ -1362,15 +1283,7 @@ export const renderElementToSvg = (
path.setAttribute("fill", element.strokeColor);
path.setAttribute("d", getFreeDrawSvgPath(element));
node.appendChild(path);
const g = maybeWrapNodesInFrameClipPath(
element,
root,
[node],
exportingFrameId,
);
g ? root.appendChild(g) : root.appendChild(node);
root.appendChild(node);
break;
}
case "image": {
@@ -1406,7 +1319,6 @@ export const renderElementToSvg = (
use.setAttribute("width", `${width}`);
use.setAttribute("height", `${height}`);
use.setAttribute("opacity", `${opacity}`);
// We first apply `scale` transforms (horizontal/vertical mirroring)
// on the <use> element, then apply translation and rotation
@@ -1432,22 +1344,13 @@ export const renderElementToSvg = (
}) rotate(${degree} ${cx} ${cy})`,
);
const clipG = maybeWrapNodesInFrameClipPath(
element,
root,
[g],
exportingFrameId,
);
clipG ? root.appendChild(clipG) : root.appendChild(g);
root.appendChild(g);
}
break;
}
// frames are not rendered and only acts as a container
case "frame": {
break;
}
default: {
if (isTextElement(element)) {
const opacity = element.opacity / 100;
const node = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
if (opacity !== 1) {
node.setAttribute("stroke-opacity", `${opacity}`);
@@ -1492,15 +1395,7 @@ export const renderElementToSvg = (
text.setAttribute("dominant-baseline", "text-before-edge");
node.appendChild(text);
}
const g = maybeWrapNodesInFrameClipPath(
element,
root,
[node],
exportingFrameId,
);
g ? root.appendChild(g) : root.appendChild(node);
root.appendChild(node);
} else {
// @ts-ignore
throw new Error(`Unimplemented type ${element.type}`);
+69 -261
View File
@@ -10,7 +10,6 @@ import {
NonDeleted,
GroupId,
ExcalidrawBindableElement,
ExcalidrawFrameElement,
} from "../element/types";
import {
getElementAbsoluteCoords,
@@ -31,13 +30,12 @@ import {
import { getSelectedElements } from "../scene/selection";
import { renderElement, renderElementToSvg } from "./renderElement";
import { getClientColor } from "../clients";
import { getClientColors } from "../clients";
import { LinearElementEditor } from "../element/linearElementEditor";
import {
isSelectedViaGroup,
getSelectedGroupIds,
getElementsInGroup,
selectGroupsFromGivenElements,
} from "../groups";
import { maxBindingGap } from "../element/collision";
import {
@@ -46,30 +44,24 @@ import {
isBindingEnabled,
} from "../element/binding";
import {
OMIT_SIDES_FOR_FRAME,
shouldShowBoundingBox,
TransformHandles,
TransformHandleType,
} from "../element/transformHandles";
import {
viewportCoordsToSceneCoords,
supportsEmoji,
throttleRAF,
isOnlyExportingSingleFrame,
} from "../utils";
import { UserIdleState } from "../types";
import { FRAME_STYLE, THEME_FILTER } from "../constants";
import { THEME_FILTER } from "../constants";
import {
EXTERNAL_LINK_IMG,
getLinkHandleFromCoords,
} from "../element/Hyperlink";
import { isFrameElement, isLinearElement } from "../element/typeChecks";
import {
elementOverlapsWithFrame,
getTargetFrame,
isElementInFrame,
} from "../frame";
import "canvas-roundrect-polyfill";
import { isLinearElement } from "../element/typeChecks";
const hasEmojiSupport = supportsEmoji();
export const DEFAULT_SPACING = 2;
const strokeRectWithRotation = (
@@ -82,8 +74,6 @@ const strokeRectWithRotation = (
cy: number,
angle: number,
fill: boolean = false,
/** should account for zoom */
radius: number = 0,
) => {
context.save();
context.translate(cx, cy);
@@ -91,14 +81,7 @@ const strokeRectWithRotation = (
if (fill) {
context.fillRect(x - cx, y - cy, width, height);
}
if (radius && context.roundRect) {
context.beginPath();
context.roundRect(x - cx, y - cy, width, height, radius);
context.stroke();
context.closePath();
} else {
context.strokeRect(x - cx, y - cy, width, height);
}
context.strokeRect(x - cx, y - cy, width, height);
context.restore();
};
@@ -176,6 +159,7 @@ const strokeGrid = (
const renderSingleLinearPoint = (
context: CanvasRenderingContext2D,
appState: AppState,
renderConfig: RenderConfig,
point: Point,
radius: number,
@@ -222,7 +206,14 @@ const renderLinearPointHandles = (
const isSelected =
!!appState.editingLinearElement?.selectedPointsIndices?.includes(idx);
renderSingleLinearPoint(context, renderConfig, point, radius, isSelected);
renderSingleLinearPoint(
context,
appState,
renderConfig,
point,
radius,
isSelected,
);
});
//Rendering segment mid points
@@ -246,6 +237,7 @@ const renderLinearPointHandles = (
if (appState.editingLinearElement) {
renderSingleLinearPoint(
context,
appState,
renderConfig,
segmentMidPoint,
radius,
@@ -256,6 +248,7 @@ const renderLinearPointHandles = (
highlightPoint(segmentMidPoint, context, renderConfig);
renderSingleLinearPoint(
context,
appState,
renderConfig,
segmentMidPoint,
radius,
@@ -265,6 +258,7 @@ const renderLinearPointHandles = (
} else if (appState.editingLinearElement || points.length === 2) {
renderSingleLinearPoint(
context,
appState,
renderConfig,
segmentMidPoint,
POINT_HANDLE_SIZE / 2,
@@ -320,34 +314,6 @@ const renderLinearElementPointHighlight = (
context.restore();
};
const frameClip = (
frame: ExcalidrawFrameElement,
context: CanvasRenderingContext2D,
renderConfig: RenderConfig,
) => {
context.translate(
frame.x + renderConfig.scrollX,
frame.y + renderConfig.scrollY,
);
context.beginPath();
if (context.roundRect && !renderConfig.isExporting) {
context.roundRect(
0,
0,
frame.width,
frame.height,
FRAME_STYLE.radius / renderConfig.zoom.value,
);
} else {
context.rect(0, 0, frame.width, frame.height);
}
context.clip();
context.translate(
-(frame.x + renderConfig.scrollX),
-(frame.y + renderConfig.scrollY),
);
};
export const _renderScene = ({
elements,
appState,
@@ -439,51 +405,11 @@ export const _renderScene = ({
}),
);
const groupsToBeAddedToFrame = new Set<string>();
visibleElements.forEach((element) => {
if (
element.groupIds.length > 0 &&
appState.frameToHighlight &&
appState.selectedElementIds[element.id] &&
(elementOverlapsWithFrame(element, appState.frameToHighlight) ||
element.groupIds.find((groupId) =>
groupsToBeAddedToFrame.has(groupId),
))
) {
element.groupIds.forEach((groupId) =>
groupsToBeAddedToFrame.add(groupId),
);
}
});
let editingLinearElement: NonDeleted<ExcalidrawLinearElement> | undefined =
undefined;
visibleElements.forEach((element) => {
try {
// - when exporting the whole canvas, we DO NOT apply clipping
// - when we are exporting a particular frame, apply clipping
// if the containing frame is not selected, apply clipping
const frameId = element.frameId || appState.frameToHighlight?.id;
if (
frameId &&
((renderConfig.isExporting && isOnlyExportingSingleFrame(elements)) ||
(!renderConfig.isExporting && appState.shouldRenderFrames))
) {
context.save();
const frame = getTargetFrame(element, appState);
if (frame && isElementInFrame(element, elements, appState)) {
frameClip(frame, context, renderConfig);
}
renderElement(element, rc, context, renderConfig, appState);
context.restore();
} else {
renderElement(element, rc, context, renderConfig, appState);
}
renderElement(element, rc, context, renderConfig, appState);
// Getting the element using LinearElementEditor during collab mismatches version - being one head of visible elements due to
// ShapeCache returns empty hence making sure that we get the
// correct element from visible elements
@@ -532,24 +458,7 @@ export const _renderScene = ({
renderBindingHighlight(context, renderConfig, suggestedBinding!);
});
}
if (appState.frameToHighlight) {
renderFrameHighlight(context, renderConfig, appState.frameToHighlight);
}
if (appState.elementsToHighlight) {
renderElementsBoxHighlight(
context,
renderConfig,
appState.elementsToHighlight,
appState,
);
}
const locallySelectedElements = getSelectedElements(elements, appState);
const isFrameSelected = locallySelectedElements.some((element) =>
isFrameElement(element),
);
// Getting the element using LinearElementEditor during collab mismatches version - being one head of visible elements due to
// ShapeCache returns empty hence making sure that we get the
@@ -618,7 +527,7 @@ export const _renderScene = ({
selectionColors.push(
...renderConfig.remoteSelectedElementIds[element.id].map(
(socketId) => {
const background = getClientColor(socketId);
const { background } = getClientColors(socketId, appState);
return background;
},
),
@@ -719,9 +628,7 @@ export const _renderScene = ({
0,
renderConfig.zoom,
"mouse",
isFrameSelected
? OMIT_SIDES_FOR_FRAME
: OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
);
if (locallySelectedElements.some((element) => !element.locked)) {
renderTransformHandles(context, renderConfig, transformHandles, 0);
@@ -740,7 +647,7 @@ export const _renderScene = ({
x -= appState.offsetLeft;
y -= appState.offsetTop;
const width = 11;
const width = 9;
const height = 14;
const isOutOfBounds =
@@ -754,20 +661,15 @@ export const _renderScene = ({
y = Math.max(y, 0);
y = Math.min(y, normalizedCanvasHeight - height);
const background = getClientColor(clientId);
const { background, stroke } = getClientColors(clientId, appState);
context.save();
context.strokeStyle = background;
context.strokeStyle = stroke;
context.fillStyle = background;
const userState = renderConfig.remotePointerUserStates[clientId];
const isInactive =
isOutOfBounds ||
userState === UserIdleState.IDLE ||
userState === UserIdleState.AWAY;
if (isInactive) {
context.globalAlpha = 0.3;
if (isOutOfBounds || userState === UserIdleState.AWAY) {
context.globalAlpha = 0.48;
}
if (
@@ -784,86 +686,73 @@ export const _renderScene = ({
context.beginPath();
context.arc(x, y, 15, 0, 2 * Math.PI, false);
context.lineWidth = 1;
context.strokeStyle = background;
context.strokeStyle = stroke;
context.stroke();
context.closePath();
}
// Background (white outline) for arrow
context.fillStyle = oc.white;
context.strokeStyle = oc.white;
context.lineWidth = 6;
context.lineJoin = "round";
context.beginPath();
context.moveTo(x, y);
context.lineTo(x + 0, y + 14);
context.lineTo(x + 1, y + 14);
context.lineTo(x + 4, y + 9);
context.lineTo(x + 11, y + 8);
context.closePath();
context.stroke();
context.lineTo(x + 9, y + 10);
context.lineTo(x, y);
context.fill();
context.stroke();
// Arrow
context.fillStyle = background;
context.strokeStyle = background;
context.lineWidth = 2;
context.lineJoin = "round";
context.beginPath();
if (isInactive) {
context.moveTo(x - 1, y - 1);
context.lineTo(x - 1, y + 15);
context.lineTo(x + 5, y + 10);
context.lineTo(x + 12, y + 9);
context.closePath();
context.fill();
} else {
context.moveTo(x, y);
context.lineTo(x + 0, y + 14);
context.lineTo(x + 4, y + 9);
context.lineTo(x + 11, y + 8);
context.closePath();
context.fill();
context.stroke();
const username = renderConfig.remotePointerUsernames[clientId];
let idleState = "";
if (userState === UserIdleState.AWAY) {
idleState = hasEmojiSupport ? "⚫️" : ` (${UserIdleState.AWAY})`;
} else if (userState === UserIdleState.IDLE) {
idleState = hasEmojiSupport ? "💤" : ` (${UserIdleState.IDLE})`;
}
const username = renderConfig.remotePointerUsernames[clientId] || "";
const usernameAndIdleState = `${username || ""}${
idleState ? ` ${idleState}` : ""
}`;
if (!isOutOfBounds && username) {
context.font = "600 12px sans-serif"; // font has to be set before context.measureText()
const offsetX = x + width / 2;
const offsetY = y + height + 2;
const paddingHorizontal = 5;
const paddingVertical = 3;
const measure = context.measureText(username);
if (!isOutOfBounds && usernameAndIdleState) {
const offsetX = x + width;
const offsetY = y + height;
const paddingHorizontal = 4;
const paddingVertical = 4;
const measure = context.measureText(usernameAndIdleState);
const measureHeight =
measure.actualBoundingBoxDescent + measure.actualBoundingBoxAscent;
const finalHeight = Math.max(measureHeight, 12);
const boxX = offsetX - 1;
const boxY = offsetY - 1;
const boxWidth = measure.width + 2 + paddingHorizontal * 2 + 2;
const boxHeight = finalHeight + 2 + paddingVertical * 2 + 2;
const boxWidth = measure.width + 2 * paddingHorizontal + 2;
const boxHeight = measureHeight + 2 * paddingVertical + 2;
if (context.roundRect) {
context.beginPath();
context.roundRect(boxX, boxY, boxWidth, boxHeight, 8);
context.roundRect(
boxX,
boxY,
boxWidth,
boxHeight,
4 / renderConfig.zoom.value,
);
context.fillStyle = background;
context.fill();
context.strokeStyle = oc.white;
context.fillStyle = stroke;
context.stroke();
} else {
roundRect(context, boxX, boxY, boxWidth, boxHeight, 8, oc.white);
// Border
context.fillStyle = stroke;
context.fillRect(boxX, boxY, boxWidth, boxHeight);
// Background
context.fillStyle = background;
context.fillRect(offsetX, offsetY, boxWidth - 2, boxHeight - 2);
}
context.fillStyle = oc.black;
context.fillStyle = oc.white;
context.fillText(
username,
offsetX + paddingHorizontal + 1,
offsetY +
paddingVertical +
measure.actualBoundingBoxAscent +
Math.floor((finalHeight - measureHeight) / 2) +
2,
usernameAndIdleState,
offsetX + paddingHorizontal,
offsetY + paddingVertical + measure.actualBoundingBoxAscent,
);
}
@@ -1082,7 +971,6 @@ const renderBindingHighlightForBindableElement = (
case "rectangle":
case "text":
case "image":
case "frame":
strokeRectWithRotation(
context,
x1 - padding,
@@ -1120,82 +1008,6 @@ const renderBindingHighlightForBindableElement = (
}
};
const renderFrameHighlight = (
context: CanvasRenderingContext2D,
renderConfig: RenderConfig,
frame: NonDeleted<ExcalidrawFrameElement>,
) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame);
const width = x2 - x1;
const height = y2 - y1;
context.strokeStyle = "rgb(0,118,255)";
context.lineWidth = (FRAME_STYLE.strokeWidth * 2) / renderConfig.zoom.value;
context.save();
context.translate(renderConfig.scrollX, renderConfig.scrollY);
strokeRectWithRotation(
context,
x1,
y1,
width,
height,
x1 + width / 2,
y1 + height / 2,
frame.angle,
false,
FRAME_STYLE.radius / renderConfig.zoom.value,
);
context.restore();
};
const renderElementsBoxHighlight = (
context: CanvasRenderingContext2D,
renderConfig: RenderConfig,
elements: NonDeleted<ExcalidrawElement>[],
appState: AppState,
) => {
const individualElements = elements.filter(
(element) => element.groupIds.length === 0,
);
const elementsInGroups = elements.filter(
(element) => element.groupIds.length > 0,
);
const getSelectionFromElements = (elements: ExcalidrawElement[]) => {
const [elementX1, elementY1, elementX2, elementY2] =
getCommonBounds(elements);
return {
angle: 0,
elementX1,
elementX2,
elementY1,
elementY2,
selectionColors: ["rgb(0,118,255)"],
dashed: false,
cx: elementX1 + (elementX2 - elementX1) / 2,
cy: elementY1 + (elementY2 - elementY1) / 2,
};
};
const getSelectionForGroupId = (groupId: GroupId) => {
const groupElements = getElementsInGroup(elements, groupId);
return getSelectionFromElements(groupElements);
};
Object.entries(selectGroupsFromGivenElements(elementsInGroups, appState))
.filter(([id, isSelected]) => isSelected)
.map(([id, isSelected]) => id)
.map((groupId) => getSelectionForGroupId(groupId))
.concat(
individualElements.map((element) => getSelectionFromElements([element])),
)
.forEach((selection) =>
renderSelectionBorder(context, renderConfig, selection),
);
};
const renderBindingHighlightForSuggestedPointBinding = (
context: CanvasRenderingContext2D,
suggestedBinding: SuggestedPointBinding,
@@ -1277,7 +1089,7 @@ const renderLinkIcon = (
}
};
export const isVisibleElement = (
const isVisibleElement = (
element: ExcalidrawElement,
canvasWidth: number,
canvasHeight: number,
@@ -1323,20 +1135,17 @@ export const renderSceneToSvg = (
offsetX = 0,
offsetY = 0,
exportWithDarkMode = false,
exportingFrameId = null,
}: {
offsetX?: number;
offsetY?: number;
exportWithDarkMode?: boolean;
exportingFrameId?: string | null;
} = {},
) => {
if (!svgRoot) {
return;
}
// render elements
elements.forEach((element) => {
elements.forEach((element, index) => {
if (!element.isDeleted) {
try {
renderElementToSvg(
@@ -1347,7 +1156,6 @@ export const renderSceneToSvg = (
element.x + offsetX,
element.y + offsetY,
exportWithDarkMode,
exportingFrameId,
);
} catch (error: any) {
console.error(error);
-4
View File
@@ -15,7 +15,6 @@ export const roundRect = (
width: number,
height: number,
radius: number,
strokeColor?: string,
) => {
context.beginPath();
context.moveTo(x + radius, y);
@@ -34,8 +33,5 @@ export const roundRect = (
context.quadraticCurveTo(x, y, x + radius, y);
context.closePath();
context.fill();
if (strokeColor) {
context.strokeStyle = strokeColor;
}
context.stroke();
};
+1 -51
View File
@@ -2,15 +2,9 @@ import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
NonDeleted,
ExcalidrawFrameElement,
} from "../element/types";
import {
getNonDeletedElements,
getNonDeletedFrames,
isNonDeletedElement,
} from "../element";
import { getNonDeletedElements, isNonDeletedElement } from "../element";
import { LinearElementEditor } from "../element/linearElementEditor";
import { isFrameElement } from "../element/typeChecks";
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
type ElementKey = ExcalidrawElement | ElementIdKey;
@@ -18,10 +12,6 @@ type ElementKey = ExcalidrawElement | ElementIdKey;
type SceneStateCallback = () => void;
type SceneStateCallbackRemover = () => void;
// ideally this would be a branded type but it'd be insanely hard to work with
// in our codebase
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
if (typeof elementKey === "string") {
return true;
@@ -65,8 +55,6 @@ class Scene {
private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
private elements: readonly ExcalidrawElement[] = [];
private nonDeletedFrames: readonly NonDeleted<ExcalidrawFrameElement>[] = [];
private frames: readonly ExcalidrawFrameElement[] = [];
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
getElementsIncludingDeleted() {
@@ -77,14 +65,6 @@ class Scene {
return this.nonDeletedElements;
}
getFramesIncludingDeleted() {
return this.frames;
}
getNonDeletedFrames(): readonly NonDeleted<ExcalidrawFrameElement>[] {
return this.nonDeletedFrames;
}
getElement<T extends ExcalidrawElement>(id: T["id"]): T | null {
return (this.elementsMap.get(id) as T | undefined) || null;
}
@@ -130,19 +110,12 @@ class Scene {
replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
this.elements = nextElements;
const nextFrames: ExcalidrawFrameElement[] = [];
this.elementsMap.clear();
nextElements.forEach((element) => {
if (isFrameElement(element)) {
nextFrames.push(element);
}
this.elementsMap.set(element.id, element);
Scene.mapElementToScene(element, this);
});
this.nonDeletedElements = getNonDeletedElements(this.elements);
this.frames = nextFrames;
this.nonDeletedFrames = getNonDeletedFrames(this.frames);
this.informMutation();
}
@@ -192,29 +165,6 @@ class Scene {
this.replaceAllElements(nextElements);
}
insertElementsAtIndex(elements: ExcalidrawElement[], index: number) {
if (!Number.isFinite(index) || index < 0) {
throw new Error(
"insertElementAtIndex can only be called with index >= 0",
);
}
const nextElements = [
...this.elements.slice(0, index),
...elements,
...this.elements.slice(index),
];
this.replaceAllElements(nextElements);
}
addNewElement = (element: ExcalidrawElement) => {
if (element.frameId) {
this.insertElementAtIndex(element, this.getElementIndex(element.frameId));
} else {
this.replaceAllElements([...this.elements, element]);
}
};
getElementIndex(elementId: string) {
return this.elements.findIndex((element) => element.id === elementId);
}
+1 -2
View File
@@ -7,8 +7,7 @@ export const hasBackground = (type: string) =>
type === "line" ||
type === "freedraw";
export const hasStrokeColor = (type: string) =>
type !== "image" && type !== "frame";
export const hasStrokeColor = (type: string) => type !== "image";
export const hasStrokeWidth = (type: string) =>
type === "rectangle" ||
+8 -75
View File
@@ -1,8 +1,8 @@
import rough from "roughjs/bin/rough";
import { NonDeletedExcalidrawElement } from "../element/types";
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
import { getCommonBounds } from "../element/bounds";
import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
import { distance, isOnlyExportingSingleFrame } from "../utils";
import { distance } from "../utils";
import { AppState, BinaryFiles } from "../types";
import { DEFAULT_EXPORT_PADDING, SVG_NS, THEME_FILTER } from "../constants";
import { getDefaultAppState } from "../appState";
@@ -11,7 +11,6 @@ import {
getInitializedImageElements,
updateImageCache,
} from "../element/image";
import Scene from "./Scene";
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
@@ -52,8 +51,6 @@ export const exportToCanvas = async (
files,
});
const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
renderScene({
elements,
appState,
@@ -62,8 +59,8 @@ export const exportToCanvas = async (
canvas,
renderConfig: {
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
scrollX: -minX + (onlyExportingSingleFrame ? 0 : exportPadding),
scrollY: -minY + (onlyExportingSingleFrame ? 0 : exportPadding),
scrollX: -minX + exportPadding,
scrollY: -minY + exportPadding,
zoom: defaultAppState.zoom,
remotePointerViewportCoords: {},
remoteSelectedElementIds: {},
@@ -91,7 +88,6 @@ export const exportToSvg = async (
viewBackgroundColor: string;
exportWithDarkMode?: boolean;
exportEmbedScene?: boolean;
renderFrame?: boolean;
},
files: BinaryFiles | null,
opts?: {
@@ -144,39 +140,6 @@ export const exportToSvg = async (
}
assetPath = `${assetPath}/dist/excalidraw-assets/`;
}
// do not apply clipping when we're exporting the whole scene
const isExportingWholeCanvas =
Scene.getScene(elements[0])?.getNonDeletedElements()?.length ===
elements.length;
const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
const offsetX = -minX + (onlyExportingSingleFrame ? 0 : exportPadding);
const offsetY = -minY + (onlyExportingSingleFrame ? 0 : exportPadding);
const exportingFrame =
isExportingWholeCanvas || !onlyExportingSingleFrame
? undefined
: elements.find((element) => element.type === "frame");
let exportingFrameClipPath = "";
if (exportingFrame) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(exportingFrame);
const cx = (x2 - x1) / 2 - (exportingFrame.x - x1);
const cy = (y2 - y1) / 2 - (exportingFrame.y - y1);
exportingFrameClipPath = `<clipPath id=${exportingFrame.id}>
<rect transform="translate(${exportingFrame.x + offsetX} ${
exportingFrame.y + offsetY
}) rotate(${exportingFrame.angle} ${cx} ${cy})"
width="${exportingFrame.width}"
height="${exportingFrame.height}"
>
</rect>
</clipPath>`;
}
svgRoot.innerHTML = `
${SVG_EXPORT_TAG}
${metadata}
@@ -191,10 +154,8 @@ export const exportToSvg = async (
src: url("${assetPath}Cascadia.woff2");
}
</style>
${exportingFrameClipPath}
</defs>
`;
// render background rect
if (appState.exportBackground && viewBackgroundColor) {
const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
@@ -208,10 +169,9 @@ export const exportToSvg = async (
const rsvg = rough.svg(svgRoot);
renderSceneToSvg(elements, rsvg, svgRoot, files || {}, {
offsetX,
offsetY,
offsetX: -minX + exportPadding,
offsetY: -minY + exportPadding,
exportWithDarkMode: appState.exportWithDarkMode,
exportingFrameId: exportingFrame?.id || null,
});
return svgRoot;
@@ -222,36 +182,9 @@ const getCanvasSize = (
elements: readonly NonDeletedExcalidrawElement[],
exportPadding: number,
): [number, number, number, number] => {
// we should decide if we are exporting the whole canvas
// if so, we are not clipping elements in the frame
// and therefore, we should not do anything special
const isExportingWholeCanvas =
Scene.getScene(elements[0])?.getNonDeletedElements()?.length ===
elements.length;
const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
if (!isExportingWholeCanvas || onlyExportingSingleFrame) {
const frames = elements.filter((element) => element.type === "frame");
const exportedFrameIds = frames.reduce((acc, frame) => {
acc[frame.id] = true;
return acc;
}, {} as Record<string, true>);
// elements in a frame do not affect the canvas size if we're not exporting
// the whole canvas
elements = elements.filter(
(element) => !exportedFrameIds[element.frameId ?? ""],
);
}
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
const width =
distance(minX, maxX) + (onlyExportingSingleFrame ? 0 : exportPadding * 2);
const height =
distance(minY, maxY) + (onlyExportingSingleFrame ? 0 : exportPadding * 2);
const width = distance(minX, maxX) + exportPadding * 2;
const height = distance(minY, maxY) + exportPadding + exportPadding;
return [minX, minY, width, height];
};
+7 -89
View File
@@ -5,61 +5,17 @@ import {
import { getElementAbsoluteCoords, getElementBounds } from "../element";
import { AppState } from "../types";
import { isBoundToContainer } from "../element/typeChecks";
import {
elementOverlapsWithFrame,
getContainingFrame,
getFrameElements,
} from "../frame";
/**
* Frames and their containing elements are not to be selected at the same time.
* Given an array of selected elements, if there are frames and their containing elements
* we only keep the frames.
* @param selectedElements
*/
export const excludeElementsInFramesFromSelection = <
T extends ExcalidrawElement,
>(
selectedElements: readonly T[],
) => {
const framesInSelection = new Set<T["id"]>();
selectedElements.forEach((element) => {
if (element.type === "frame") {
framesInSelection.add(element.id);
}
});
return selectedElements.filter((element) => {
if (element.frameId && framesInSelection.has(element.frameId)) {
return false;
}
return true;
});
};
export const getElementsWithinSelection = (
elements: readonly NonDeletedExcalidrawElement[],
selection: NonDeletedExcalidrawElement,
excludeElementsInFrames: boolean = true,
) => {
const [selectionX1, selectionY1, selectionX2, selectionY2] =
getElementAbsoluteCoords(selection);
let elementsInSelection = elements.filter((element) => {
let [elementX1, elementY1, elementX2, elementY2] =
return elements.filter((element) => {
const [elementX1, elementY1, elementX2, elementY2] =
getElementBounds(element);
const containingFrame = getContainingFrame(element);
if (containingFrame) {
const [fx1, fy1, fx2, fy2] = getElementBounds(containingFrame);
elementX1 = Math.max(fx1, elementX1);
elementY1 = Math.max(fy1, elementY1);
elementX2 = Math.min(fx2, elementX2);
elementY2 = Math.min(fy2, elementY2);
}
return (
element.locked === false &&
element.type !== "selection" &&
@@ -70,22 +26,6 @@ export const getElementsWithinSelection = (
selectionY2 >= elementY2
);
});
elementsInSelection = excludeElementsInFrames
? excludeElementsInFramesFromSelection(elementsInSelection)
: elementsInSelection;
elementsInSelection = elementsInSelection.filter((element) => {
const containingFrame = getContainingFrame(element);
if (containingFrame) {
return elementOverlapsWithFrame(element, containingFrame);
}
return true;
});
return elementsInSelection;
};
export const isSomeElementSelected = (
@@ -116,17 +56,14 @@ export const getCommonAttributeOfSelectedElements = <T>(
export const getSelectedElements = (
elements: readonly NonDeletedExcalidrawElement[],
appState: Pick<AppState, "selectedElementIds">,
opts?: {
includeBoundTextElement?: boolean;
includeElementsInFrames?: boolean;
},
) => {
const selectedElements = elements.filter((element) => {
includeBoundTextElement: boolean = false,
) =>
elements.filter((element) => {
if (appState.selectedElementIds[element.id]) {
return element;
}
if (
opts?.includeBoundTextElement &&
includeBoundTextElement &&
isBoundToContainer(element) &&
appState.selectedElementIds[element?.containerId]
) {
@@ -135,29 +72,10 @@ export const getSelectedElements = (
return null;
});
if (opts?.includeElementsInFrames) {
const elementsToInclude: ExcalidrawElement[] = [];
selectedElements.forEach((element) => {
if (element.type === "frame") {
getFrameElements(elements, element.id).forEach((e) =>
elementsToInclude.push(e),
);
}
elementsToInclude.push(element);
});
return elementsToInclude;
}
return selectedElements;
};
export const getTargetElements = (
elements: readonly NonDeletedExcalidrawElement[],
appState: Pick<AppState, "selectedElementIds" | "editingElement">,
) =>
appState.editingElement
? [appState.editingElement]
: getSelectedElements(elements, appState, {
includeBoundTextElement: true,
});
: getSelectedElements(elements, appState, true);
-8
View File
@@ -83,14 +83,6 @@ export const SHAPES = [
numericKey: KEYS["0"],
fillable: false,
},
// TODO: frame, create icon and set up numeric key
// {
// icon: RectangleIcon,
// value: "frame",
// key: KEYS.F,
// numericKey: KEYS.SUBTRACT,
// fillable: false,
// },
] as const;
export const findShapeByKey = (key: string) => {
File diff suppressed because it is too large Load Diff
@@ -10,7 +10,6 @@ Object {
"endArrowhead": "arrow",
"endBinding": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"height": 50,
"id": "id0",
@@ -57,7 +56,6 @@ Object {
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"height": 50,
"id": "id0",
@@ -91,7 +89,6 @@ Object {
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"height": 50,
"id": "id0",
@@ -125,7 +122,6 @@ Object {
"endArrowhead": null,
"endBinding": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"height": 50,
"id": "id0",
@@ -172,7 +168,6 @@ Object {
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"height": 50,
"id": "id0",
+1 -2
View File
@@ -15,7 +15,6 @@ exports[`export exporting svg containing transformed images: svg export output 1
src: url(\\"https://excalidraw.com/Cascadia.woff2\\");
}
</style>
</defs>
<g transform=\\"translate(30.710678118654755 30.710678118654755) rotate(315 50 50)\\"><use href=\\"#image-file_A\\" width=\\"100\\" height=\\"100\\" opacity=\\"1\\"></use></g><g transform=\\"translate(130.71067811865476 30.710678118654755) rotate(45 25 25)\\"><use href=\\"#image-file_A\\" width=\\"50\\" height=\\"50\\" opacity=\\"1\\" transform=\\"scale(-1, 1) translate(-50 0)\\"></use></g><g transform=\\"translate(30.710678118654755 130.71067811865476) rotate(45 50 50)\\"><use href=\\"#image-file_A\\" width=\\"100\\" height=\\"100\\" opacity=\\"1\\" transform=\\"scale(1, -1) translate(0 -100)\\"></use></g><g transform=\\"translate(130.71067811865476 130.71067811865476) rotate(315 25 25)\\"><use href=\\"#image-file_A\\" width=\\"50\\" height=\\"50\\" opacity=\\"1\\" transform=\\"scale(-1, -1) translate(-50 -50)\\"></use></g></svg>"
<g transform=\\"translate(30.710678118654755 30.710678118654755) rotate(315 50 50)\\"><use href=\\"#image-file_A\\" width=\\"100\\" height=\\"100\\"></use></g><g transform=\\"translate(130.71067811865476 30.710678118654755) rotate(45 25 25)\\"><use href=\\"#image-file_A\\" width=\\"50\\" height=\\"50\\" transform=\\"scale(-1, 1) translate(-50 0)\\"></use></g><g transform=\\"translate(30.710678118654755 130.71067811865476) rotate(45 50 50)\\"><use href=\\"#image-file_A\\" width=\\"100\\" height=\\"100\\" transform=\\"scale(1, -1) translate(0 -100)\\"></use></g><g transform=\\"translate(130.71067811865476 130.71067811865476) rotate(315 25 25)\\"><use href=\\"#image-file_A\\" width=\\"50\\" height=\\"50\\" transform=\\"scale(-1, -1) translate(-50 -50)\\"></use></g></svg>"
`;
@@ -6,7 +6,6 @@ Object {
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"height": 50,
"id": "id0_copy",
@@ -38,7 +37,6 @@ Object {
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"height": 50,
"id": "id0",
@@ -70,7 +68,6 @@ Object {
"backgroundColor": "transparent",
"boundElements": null,
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"height": 50,
"id": "id0",
@@ -107,7 +104,6 @@ Object {
},
],
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"height": 100,
"id": "id0",
@@ -144,7 +140,6 @@ Object {
},
],
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"height": 300,
"id": "id1",
@@ -182,7 +177,6 @@ Object {
"gap": 10,
},
"fillStyle": "hachure",
"frameId": null,
"groupIds": Array [],
"height": 81.48231043525051,
"id": "id2",

Some files were not shown because too many files have changed in this diff Show More