Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e828da559 | |||
| 8336edb4a0 | |||
| 81ebf82979 | |||
| 4d7d96eb7b | |||
| 1747e93957 | |||
| 3bd5d87cac | |||
| 74d2fc6406 | |||
| ce9acfbc55 | |||
| 16c7945ca0 | |||
| 5ca3613cc3 | |||
| b4abfad638 | |||
| a39640ead1 | |||
| 84bd9bd4ff | |||
| 27e2888347 | |||
| e192538267 | |||
| 1d3652a96c | |||
| bb96f322c6 | |||
| e6fb7e3016 | |||
| e385066b4b | |||
| a5bd54b86d | |||
| 01432813a6 | |||
| 6e3b575fa5 | |||
| 333cc53797 | |||
| 8e5d376b49 |
+3
-1
@@ -19,6 +19,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@excalidraw/random-username": "1.0.0",
|
||||||
"@radix-ui/react-popover": "1.0.3",
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
"@radix-ui/react-tabs": "1.0.2",
|
"@radix-ui/react-tabs": "1.0.2",
|
||||||
"@sentry/browser": "6.2.5",
|
"@sentry/browser": "6.2.5",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"@testing-library/react": "12.1.5",
|
"@testing-library/react": "12.1.5",
|
||||||
"@tldraw/vec": "1.7.1",
|
"@tldraw/vec": "1.7.1",
|
||||||
"browser-fs-access": "0.29.1",
|
"browser-fs-access": "0.29.1",
|
||||||
|
"canvas-roundrect-polyfill": "0.0.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"fake-indexeddb": "3.1.7",
|
"fake-indexeddb": "3.1.7",
|
||||||
@@ -106,7 +108,7 @@
|
|||||||
"<rootDir>/src/packages/excalidraw/example"
|
"<rootDir>/src/packages/excalidraw/example"
|
||||||
],
|
],
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access)/)"
|
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access|canvas-roundrect-polyfill)/)"
|
||||||
],
|
],
|
||||||
"resetMocks": false
|
"resetMocks": false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ export const actionAddToLibrary = register({
|
|||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
true,
|
{
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (selectedElements.some((element) => element.type === "image")) {
|
if (selectedElements.some((element) => element.type === "image")) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
+28
-12
@@ -10,6 +10,7 @@ import {
|
|||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
@@ -17,10 +18,20 @@ import { AppState } from "../types";
|
|||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
const enableActionGroup = (
|
const alignActionsPredicate = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => getSelectedElements(getNonDeletedElements(elements), appState).length > 1;
|
) => {
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
selectedElements.length > 1 &&
|
||||||
|
// TODO enable aligning frames when implemented properly
|
||||||
|
!selectedElements.some((el) => el.type === "frame")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const alignSelectedElements = (
|
const alignSelectedElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
@@ -36,14 +47,16 @@ const alignSelectedElements = (
|
|||||||
|
|
||||||
const updatedElementsMap = arrayToMap(updatedElements);
|
const updatedElementsMap = arrayToMap(updatedElements);
|
||||||
|
|
||||||
return elements.map(
|
return updateFrameMembershipOfSelectedElements(
|
||||||
(element) => updatedElementsMap.get(element.id) || element,
|
elements.map((element) => updatedElementsMap.get(element.id) || element),
|
||||||
|
appState,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actionAlignTop = register({
|
export const actionAlignTop = register({
|
||||||
name: "alignTop",
|
name: "alignTop",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
@@ -58,7 +71,7 @@ export const actionAlignTop = register({
|
|||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignTopIcon}
|
icon={AlignTopIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -74,6 +87,7 @@ export const actionAlignTop = register({
|
|||||||
export const actionAlignBottom = register({
|
export const actionAlignBottom = register({
|
||||||
name: "alignBottom",
|
name: "alignBottom",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
@@ -88,7 +102,7 @@ export const actionAlignBottom = register({
|
|||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignBottomIcon}
|
icon={AlignBottomIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -104,6 +118,7 @@ export const actionAlignBottom = register({
|
|||||||
export const actionAlignLeft = register({
|
export const actionAlignLeft = register({
|
||||||
name: "alignLeft",
|
name: "alignLeft",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
@@ -118,7 +133,7 @@ export const actionAlignLeft = register({
|
|||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignLeftIcon}
|
icon={AlignLeftIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -134,7 +149,7 @@ export const actionAlignLeft = register({
|
|||||||
export const actionAlignRight = register({
|
export const actionAlignRight = register({
|
||||||
name: "alignRight",
|
name: "alignRight",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
@@ -149,7 +164,7 @@ export const actionAlignRight = register({
|
|||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={AlignRightIcon}
|
icon={AlignRightIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -165,7 +180,7 @@ export const actionAlignRight = register({
|
|||||||
export const actionAlignVerticallyCentered = register({
|
export const actionAlignVerticallyCentered = register({
|
||||||
name: "alignVerticallyCentered",
|
name: "alignVerticallyCentered",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
@@ -178,7 +193,7 @@ export const actionAlignVerticallyCentered = register({
|
|||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={CenterVerticallyIcon}
|
icon={CenterVerticallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
@@ -192,6 +207,7 @@ export const actionAlignVerticallyCentered = register({
|
|||||||
export const actionAlignHorizontallyCentered = register({
|
export const actionAlignHorizontallyCentered = register({
|
||||||
name: "alignHorizontallyCentered",
|
name: "alignHorizontallyCentered",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
|
predicate: alignActionsPredicate,
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
appState,
|
appState,
|
||||||
@@ -204,7 +220,7 @@ export const actionAlignHorizontallyCentered = register({
|
|||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState)}
|
hidden={!alignActionsPredicate(elements, appState)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={CenterHorizontallyIcon}
|
icon={CenterHorizontallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ export const actionWrapTextInContainer = register({
|
|||||||
"rectangle",
|
"rectangle",
|
||||||
),
|
),
|
||||||
groupIds: textElement.groupIds,
|
groupIds: textElement.groupIds,
|
||||||
|
frameId: textElement.frameId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// update bindings
|
// update bindings
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import {
|
|||||||
isHandToolActive,
|
isHandToolActive,
|
||||||
} from "../appState";
|
} from "../appState";
|
||||||
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
|
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
|
||||||
|
import { excludeElementsInFramesFromSelection } from "../scene/selection";
|
||||||
|
import { Bounds } from "../element/bounds";
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register({
|
export const actionChangeViewBackgroundColor = register({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
@@ -206,7 +208,7 @@ export const actionResetZoom = register({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const zoomValueToFitBoundsOnViewport = (
|
const zoomValueToFitBoundsOnViewport = (
|
||||||
bounds: [number, number, number, number],
|
bounds: Bounds,
|
||||||
viewportDimensions: { width: number; height: number },
|
viewportDimensions: { width: number; height: number },
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = bounds;
|
const [x1, y1, x2, y2] = bounds;
|
||||||
@@ -234,8 +236,10 @@ export const zoomToFitElements = (
|
|||||||
|
|
||||||
const commonBounds =
|
const commonBounds =
|
||||||
zoomToSelection && selectedElements.length > 0
|
zoomToSelection && selectedElements.length > 0
|
||||||
? getCommonBounds(selectedElements)
|
? getCommonBounds(excludeElementsInFramesFromSelection(selectedElements))
|
||||||
: getCommonBounds(nonDeletedElements);
|
: getCommonBounds(
|
||||||
|
excludeElementsInFramesFromSelection(nonDeletedElements),
|
||||||
|
);
|
||||||
|
|
||||||
const newZoom = {
|
const newZoom = {
|
||||||
value: zoomValueToFitBoundsOnViewport(commonBounds, {
|
value: zoomValueToFitBoundsOnViewport(commonBounds, {
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ export const actionCopy = register({
|
|||||||
name: "copy",
|
name: "copy",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState, true);
|
const elementsToCopy = getSelectedElements(elements, appState, {
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
});
|
||||||
|
|
||||||
copyToClipboard(selectedElements, app.files);
|
copyToClipboard(elementsToCopy, app.files);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
@@ -75,7 +78,10 @@ export const actionCopyAsSvg = register({
|
|||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
true,
|
{
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await exportCanvas(
|
await exportCanvas(
|
||||||
@@ -119,7 +125,10 @@ export const actionCopyAsPng = register({
|
|||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
true,
|
{
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await exportCanvas(
|
await exportCanvas(
|
||||||
@@ -172,7 +181,9 @@ export const copyText = register({
|
|||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
true,
|
{
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const text = selectedElements
|
const text = selectedElements
|
||||||
@@ -191,7 +202,9 @@ export const copyText = register({
|
|||||||
predicate: (elements, appState) => {
|
predicate: (elements, appState) => {
|
||||||
return (
|
return (
|
||||||
probablySupportsClipboardWriteText &&
|
probablySupportsClipboardWriteText &&
|
||||||
getSelectedElements(elements, appState, true).some(isTextElement)
|
getSelectedElements(elements, appState, {
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
}).some(isTextElement)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.copyText",
|
contextItemLabel: "labels.copyText",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
@@ -18,11 +18,23 @@ const deleteSelectedElements = (
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
|
const framesToBeDeleted = new Set(
|
||||||
|
getSelectedElements(
|
||||||
|
elements.filter((el) => el.type === "frame"),
|
||||||
|
appState,
|
||||||
|
).map((el) => el.id),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: elements.map((el) => {
|
elements: elements.map((el) => {
|
||||||
if (appState.selectedElementIds[el.id]) {
|
if (appState.selectedElementIds[el.id]) {
|
||||||
return newElementWith(el, { isDeleted: true });
|
return newElementWith(el, { isDeleted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (el.frameId && framesToBeDeleted.has(el.frameId)) {
|
||||||
|
return newElementWith(el, { isDeleted: true });
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isBoundToContainer(el) &&
|
isBoundToContainer(el) &&
|
||||||
appState.selectedElementIds[el.containerId]
|
appState.selectedElementIds[el.containerId]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ToolButton } from "../components/ToolButton";
|
|||||||
import { distributeElements, Distribution } from "../distribute";
|
import { distributeElements, Distribution } from "../distribute";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
@@ -16,7 +17,17 @@ import { register } from "./register";
|
|||||||
const enableActionGroup = (
|
const enableActionGroup = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => getSelectedElements(getNonDeletedElements(elements), appState).length > 1;
|
) => {
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
selectedElements.length > 1 &&
|
||||||
|
// TODO enable distributing frames when implemented properly
|
||||||
|
!selectedElements.some((el) => el.type === "frame")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const distributeSelectedElements = (
|
const distributeSelectedElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
@@ -32,8 +43,9 @@ const distributeSelectedElements = (
|
|||||||
|
|
||||||
const updatedElementsMap = arrayToMap(updatedElements);
|
const updatedElementsMap = arrayToMap(updatedElements);
|
||||||
|
|
||||||
return elements.map(
|
return updateFrameMembershipOfSelectedElements(
|
||||||
(element) => updatedElementsMap.get(element.id) || element,
|
elements.map((element) => updatedElementsMap.get(element.id) || element),
|
||||||
|
appState,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { KEYS } from "../keys";
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { duplicateElement, getNonDeletedElements } from "../element";
|
import { duplicateElement, getNonDeletedElements } from "../element";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { arrayToMap, getShortcutKey } from "../utils";
|
import { arrayToMap, getShortcutKey } from "../utils";
|
||||||
@@ -20,9 +20,17 @@ import {
|
|||||||
bindTextToShapeAfterDuplication,
|
bindTextToShapeAfterDuplication,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer, isFrameElement } from "../element/typeChecks";
|
||||||
import { normalizeElementOrder } from "../element/sortElements";
|
import { normalizeElementOrder } from "../element/sortElements";
|
||||||
import { DuplicateIcon } from "../components/icons";
|
import { DuplicateIcon } from "../components/icons";
|
||||||
|
import {
|
||||||
|
bindElementsToFramesAfterDuplication,
|
||||||
|
getFrameElements,
|
||||||
|
} from "../frame";
|
||||||
|
import {
|
||||||
|
excludeElementsInFramesFromSelection,
|
||||||
|
getSelectedElements,
|
||||||
|
} from "../scene/selection";
|
||||||
|
|
||||||
export const actionDuplicateSelection = register({
|
export const actionDuplicateSelection = register({
|
||||||
name: "duplicateSelection",
|
name: "duplicateSelection",
|
||||||
@@ -94,8 +102,11 @@ const duplicateElements = (
|
|||||||
return newElement;
|
return newElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedElementIds = arrayToMap(
|
const idsOfElementsToDuplicate = arrayToMap(
|
||||||
getSelectedElements(sortedElements, appState, true),
|
getSelectedElements(sortedElements, appState, {
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ids of elements that have already been processed so we don't push them
|
// Ids of elements that have already been processed so we don't push them
|
||||||
@@ -129,12 +140,25 @@ const duplicateElements = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (selectedElementIds.get(element.id)) {
|
const isElementAFrame = isFrameElement(element);
|
||||||
// if a group or a container/bound-text, duplicate atomically
|
|
||||||
if (element.groupIds.length || boundTextElement) {
|
if (idsOfElementsToDuplicate.get(element.id)) {
|
||||||
|
// if a group or a container/bound-text or frame, duplicate atomically
|
||||||
|
if (element.groupIds.length || boundTextElement || isElementAFrame) {
|
||||||
const groupId = getSelectedGroupForElement(appState, element);
|
const groupId = getSelectedGroupForElement(appState, element);
|
||||||
if (groupId) {
|
if (groupId) {
|
||||||
const groupElements = getElementsInGroup(sortedElements, 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],
|
||||||
|
);
|
||||||
|
|
||||||
elementsWithClones.push(
|
elementsWithClones.push(
|
||||||
...markAsProcessed([
|
...markAsProcessed([
|
||||||
...groupElements,
|
...groupElements,
|
||||||
@@ -156,10 +180,34 @@ const duplicateElements = (
|
|||||||
);
|
);
|
||||||
continue;
|
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 {
|
} else {
|
||||||
elementsWithClones.push(...markAsProcessed([element]));
|
elementsWithClones.push(...markAsProcessed([element]));
|
||||||
}
|
}
|
||||||
@@ -200,6 +248,14 @@ const duplicateElements = (
|
|||||||
oldElements,
|
oldElements,
|
||||||
oldIdToDuplicatedId,
|
oldIdToDuplicatedId,
|
||||||
);
|
);
|
||||||
|
bindElementsToFramesAfterDuplication(
|
||||||
|
finalElements,
|
||||||
|
oldElements,
|
||||||
|
oldIdToDuplicatedId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const nextElementsToSelect =
|
||||||
|
excludeElementsInFramesFromSelection(newElements);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: finalElements,
|
elements: finalElements,
|
||||||
@@ -207,7 +263,7 @@ const duplicateElements = (
|
|||||||
{
|
{
|
||||||
...appState,
|
...appState,
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
selectedElementIds: newElements.reduce(
|
selectedElementIds: nextElementsToSelect.reduce(
|
||||||
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||||
if (!isBoundToContainer(element)) {
|
if (!isBoundToContainer(element)) {
|
||||||
acc[element.id] = true;
|
acc[element.id] = true;
|
||||||
|
|||||||
@@ -11,8 +11,17 @@ const shouldLock = (elements: readonly ExcalidrawElement[]) =>
|
|||||||
export const actionToggleElementLock = register({
|
export const actionToggleElementLock = register({
|
||||||
name: "toggleElementLock",
|
name: "toggleElementLock",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
|
predicate: (elements, appState) => {
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
return !selectedElements.some(
|
||||||
|
(element) => element.locked && element.frameId,
|
||||||
|
);
|
||||||
|
},
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState, true);
|
const selectedElements = getSelectedElements(elements, appState, {
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
});
|
||||||
|
|
||||||
if (!selectedElements.length) {
|
if (!selectedElements.length) {
|
||||||
return false;
|
return false;
|
||||||
@@ -38,8 +47,10 @@ export const actionToggleElementLock = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: (elements, appState) => {
|
contextItemLabel: (elements, appState) => {
|
||||||
const selected = getSelectedElements(elements, appState, false);
|
const selected = getSelectedElements(elements, appState, {
|
||||||
if (selected.length === 1) {
|
includeBoundTextElement: false,
|
||||||
|
});
|
||||||
|
if (selected.length === 1 && selected[0].type !== "frame") {
|
||||||
return selected[0].locked
|
return selected[0].locked
|
||||||
? "labels.elementLock.unlock"
|
? "labels.elementLock.unlock"
|
||||||
: "labels.elementLock.lock";
|
: "labels.elementLock.lock";
|
||||||
@@ -54,7 +65,9 @@ export const actionToggleElementLock = register({
|
|||||||
event.key.toLocaleLowerCase() === KEYS.L &&
|
event.key.toLocaleLowerCase() === KEYS.L &&
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
getSelectedElements(elements, appState, false).length > 0
|
getSelectedElements(elements, appState, {
|
||||||
|
includeBoundTextElement: false,
|
||||||
|
}).length > 0
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,13 +12,17 @@ import {
|
|||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
unbindLinearElements,
|
unbindLinearElements,
|
||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
|
import { updateFrameMembershipOfSelectedElements } from "../frame";
|
||||||
|
|
||||||
export const actionFlipHorizontal = register({
|
export const actionFlipHorizontal = register({
|
||||||
name: "flipHorizontal",
|
name: "flipHorizontal",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
elements: flipSelectedElements(elements, appState, "horizontal"),
|
elements: updateFrameMembershipOfSelectedElements(
|
||||||
|
flipSelectedElements(elements, appState, "horizontal"),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
appState,
|
appState,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
@@ -32,7 +36,10 @@ export const actionFlipVertical = register({
|
|||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState) => {
|
||||||
return {
|
return {
|
||||||
elements: flipSelectedElements(elements, appState, "vertical"),
|
elements: updateFrameMembershipOfSelectedElements(
|
||||||
|
flipSelectedElements(elements, appState, "vertical"),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
appState,
|
appState,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
@@ -50,6 +57,9 @@ const flipSelectedElements = (
|
|||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
|
{
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const updatedElements = flipElements(
|
const updatedElements = flipElements(
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
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,
|
||||||
|
});
|
||||||
+70
-16
@@ -17,9 +17,19 @@ import {
|
|||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawFrameElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
} from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
|
import {
|
||||||
|
getElementsInResizingFrame,
|
||||||
|
groupByFrames,
|
||||||
|
removeElementsFromFrame,
|
||||||
|
replaceAllElementsInFrame,
|
||||||
|
} from "../frame";
|
||||||
|
|
||||||
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
|
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
|
||||||
if (elements.length >= 2) {
|
if (elements.length >= 2) {
|
||||||
@@ -45,7 +55,9 @@ const enableActionGroup = (
|
|||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
true,
|
{
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
|
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
|
||||||
@@ -55,11 +67,13 @@ const enableActionGroup = (
|
|||||||
export const actionGroup = register({
|
export const actionGroup = register({
|
||||||
name: "group",
|
name: "group",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
true,
|
{
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (selectedElements.length < 2) {
|
if (selectedElements.length < 2) {
|
||||||
// nothing to group
|
// nothing to group
|
||||||
@@ -86,9 +100,31 @@ export const actionGroup = register({
|
|||||||
return { appState, elements, commitToHistory: false };
|
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 newGroupId = randomId();
|
||||||
const selectElementIds = arrayToMap(selectedElements);
|
const selectElementIds = arrayToMap(selectedElements);
|
||||||
const updatedElements = elements.map((element) => {
|
|
||||||
|
nextElements = nextElements.map((element) => {
|
||||||
if (!selectElementIds.get(element.id)) {
|
if (!selectElementIds.get(element.id)) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
@@ -102,17 +138,16 @@ export const actionGroup = register({
|
|||||||
});
|
});
|
||||||
// keep the z order within the group the same, but move them
|
// keep the z order within the group the same, but move them
|
||||||
// to the z order of the highest element in the layer stack
|
// to the z order of the highest element in the layer stack
|
||||||
const elementsInGroup = getElementsInGroup(updatedElements, newGroupId);
|
const elementsInGroup = getElementsInGroup(nextElements, newGroupId);
|
||||||
const lastElementInGroup = elementsInGroup[elementsInGroup.length - 1];
|
const lastElementInGroup = elementsInGroup[elementsInGroup.length - 1];
|
||||||
const lastGroupElementIndex =
|
const lastGroupElementIndex = nextElements.lastIndexOf(lastElementInGroup);
|
||||||
updatedElements.lastIndexOf(lastElementInGroup);
|
const elementsAfterGroup = nextElements.slice(lastGroupElementIndex + 1);
|
||||||
const elementsAfterGroup = updatedElements.slice(lastGroupElementIndex + 1);
|
const elementsBeforeGroup = nextElements
|
||||||
const elementsBeforeGroup = updatedElements
|
|
||||||
.slice(0, lastGroupElementIndex)
|
.slice(0, lastGroupElementIndex)
|
||||||
.filter(
|
.filter(
|
||||||
(updatedElement) => !isElementInGroup(updatedElement, newGroupId),
|
(updatedElement) => !isElementInGroup(updatedElement, newGroupId),
|
||||||
);
|
);
|
||||||
const updatedElementsInOrder = [
|
nextElements = [
|
||||||
...elementsBeforeGroup,
|
...elementsBeforeGroup,
|
||||||
...elementsInGroup,
|
...elementsInGroup,
|
||||||
...elementsAfterGroup,
|
...elementsAfterGroup,
|
||||||
@@ -122,9 +157,9 @@ export const actionGroup = register({
|
|||||||
appState: selectGroup(
|
appState: selectGroup(
|
||||||
newGroupId,
|
newGroupId,
|
||||||
{ ...appState, selectedGroupIds: {} },
|
{ ...appState, selectedGroupIds: {} },
|
||||||
getNonDeletedElements(updatedElementsInOrder),
|
getNonDeletedElements(nextElements),
|
||||||
),
|
),
|
||||||
elements: updatedElementsInOrder,
|
elements: nextElements,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -148,14 +183,23 @@ export const actionGroup = register({
|
|||||||
export const actionUngroup = register({
|
export const actionUngroup = register({
|
||||||
name: "ungroup",
|
name: "ungroup",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
const groupIds = getSelectedGroupIds(appState);
|
const groupIds = getSelectedGroupIds(appState);
|
||||||
if (groupIds.length === 0) {
|
if (groupIds.length === 0) {
|
||||||
return { appState, elements, commitToHistory: false };
|
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"][] = [];
|
const boundTextElementIds: ExcalidrawTextElement["id"][] = [];
|
||||||
const nextElements = elements.map((element) => {
|
nextElements = nextElements.map((element) => {
|
||||||
if (isBoundToContainer(element)) {
|
if (isBoundToContainer(element)) {
|
||||||
boundTextElementIds.push(element.id);
|
boundTextElementIds.push(element.id);
|
||||||
}
|
}
|
||||||
@@ -176,13 +220,23 @@ export const actionUngroup = register({
|
|||||||
getNonDeletedElements(nextElements),
|
getNonDeletedElements(nextElements),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
frames.forEach((frame) => {
|
||||||
|
if (frame) {
|
||||||
|
nextElements = replaceAllElementsInFrame(
|
||||||
|
nextElements,
|
||||||
|
getElementsInResizingFrame(nextElements, frame, appState),
|
||||||
|
frame,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// remove binded text elements from selection
|
// remove binded text elements from selection
|
||||||
boundTextElementIds.forEach(
|
boundTextElementIds.forEach(
|
||||||
(id) => (updateAppState.selectedElementIds[id] = false),
|
(id) => (updateAppState.selectedElementIds[id] = false),
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
appState: updateAppState,
|
appState: updateAppState,
|
||||||
|
|
||||||
elements: nextElements,
|
elements: nextElements,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ export const actionToggleLinearEditor = register({
|
|||||||
const selectedElement = getSelectedElements(
|
const selectedElement = getSelectedElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
true,
|
{
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
},
|
||||||
)[0] as ExcalidrawLinearElement;
|
)[0] as ExcalidrawLinearElement;
|
||||||
|
|
||||||
const editingLinearElement =
|
const editingLinearElement =
|
||||||
@@ -40,7 +42,9 @@ export const actionToggleLinearEditor = register({
|
|||||||
const selectedElement = getSelectedElements(
|
const selectedElement = getSelectedElements(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
true,
|
{
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
},
|
||||||
)[0] as ExcalidrawLinearElement;
|
)[0] as ExcalidrawLinearElement;
|
||||||
return appState.editingLinearElement?.elementId === selectedElement.id
|
return appState.editingLinearElement?.elementId === selectedElement.id
|
||||||
? "labels.lineEditor.exit"
|
? "labels.lineEditor.exit"
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ export const actionFullScreen = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event) => event.key === KEYS.F && !event[KEYS.CTRL_OR_CMD],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionShortcuts = register({
|
export const actionShortcuts = register({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getClientColors } from "../clients";
|
import { getClientColor } from "../clients";
|
||||||
import { Avatar } from "../components/Avatar";
|
import { Avatar } from "../components/Avatar";
|
||||||
import { centerScrollOn } from "../scene/scroll";
|
import { centerScrollOn } from "../scene/scroll";
|
||||||
import { Collaborator } from "../types";
|
import { Collaborator } from "../types";
|
||||||
@@ -31,15 +31,14 @@ export const actionGoToCollaborator = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ appState, updateData, data }) => {
|
PanelComponent: ({ updateData, data }) => {
|
||||||
const [clientId, collaborator] = data as [string, Collaborator];
|
const [clientId, collaborator] = data as [string, Collaborator];
|
||||||
|
|
||||||
const { background, stroke } = getClientColors(clientId, appState);
|
const background = getClientColor(clientId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
color={background}
|
color={background}
|
||||||
border={stroke}
|
|
||||||
onClick={() => updateData(collaborator.pointer)}
|
onClick={() => updateData(collaborator.pointer)}
|
||||||
name={collaborator.username || ""}
|
name={collaborator.username || ""}
|
||||||
src={collaborator.avatarUrl}
|
src={collaborator.avatarUrl}
|
||||||
|
|||||||
@@ -102,8 +102,11 @@ const changeProperty = (
|
|||||||
includeBoundText = false,
|
includeBoundText = false,
|
||||||
) => {
|
) => {
|
||||||
const selectedElementIds = arrayToMap(
|
const selectedElementIds = arrayToMap(
|
||||||
getSelectedElements(elements, appState, includeBoundText),
|
getSelectedElements(elements, appState, {
|
||||||
|
includeBoundTextElement: includeBoundText,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return elements.map((element) => {
|
return elements.map((element) => {
|
||||||
if (
|
if (
|
||||||
selectedElementIds.get(element.id) ||
|
selectedElementIds.get(element.id) ||
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getNonDeletedElements, isTextElement } from "../element";
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import { excludeElementsInFramesFromSelection } from "../scene/selection";
|
||||||
|
|
||||||
export const actionSelectAll = register({
|
export const actionSelectAll = register({
|
||||||
name: "selectAll",
|
name: "selectAll",
|
||||||
@@ -13,19 +14,18 @@ export const actionSelectAll = register({
|
|||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const selectedElementIds = elements.reduce(
|
|
||||||
(map: Record<ExcalidrawElement["id"], true>, element) => {
|
const selectedElementIds = excludeElementsInFramesFromSelection(
|
||||||
if (
|
elements.filter(
|
||||||
|
(element) =>
|
||||||
!element.isDeleted &&
|
!element.isDeleted &&
|
||||||
!(isTextElement(element) && element.containerId) &&
|
!(isTextElement(element) && element.containerId) &&
|
||||||
!element.locked
|
!element.locked,
|
||||||
) {
|
),
|
||||||
map[element.id] = true;
|
).reduce((map: Record<ExcalidrawElement["id"], true>, element) => {
|
||||||
}
|
map[element.id] = true;
|
||||||
return map;
|
return map;
|
||||||
},
|
}, {});
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: selectGroupsForSelectedElements(
|
appState: selectGroupsForSelectedElements(
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
canApplyRoundnessTypeToElement,
|
canApplyRoundnessTypeToElement,
|
||||||
getDefaultRoundnessTypeForElement,
|
getDefaultRoundnessTypeForElement,
|
||||||
|
isFrameElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
|
|
||||||
@@ -64,7 +65,9 @@ export const actionPasteStyles = register({
|
|||||||
return { elements, commitToHistory: false };
|
return { elements, commitToHistory: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(elements, appState, true);
|
const selectedElements = getSelectedElements(elements, appState, {
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
});
|
||||||
const selectedElementIds = selectedElements.map((element) => element.id);
|
const selectedElementIds = selectedElements.map((element) => element.id);
|
||||||
return {
|
return {
|
||||||
elements: elements.map((element) => {
|
elements: elements.map((element) => {
|
||||||
@@ -127,6 +130,13 @@ export const actionPasteStyles = register({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFrameElement(element)) {
|
||||||
|
newElement = newElementWith(newElement, {
|
||||||
|
roundness: null,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return newElement;
|
return newElement;
|
||||||
}
|
}
|
||||||
return element;
|
return element;
|
||||||
|
|||||||
+36
-11
@@ -2,10 +2,10 @@ import React from "react";
|
|||||||
import {
|
import {
|
||||||
Action,
|
Action,
|
||||||
UpdaterFn,
|
UpdaterFn,
|
||||||
ActionName,
|
|
||||||
ActionResult,
|
ActionResult,
|
||||||
PanelComponentProps,
|
PanelComponentProps,
|
||||||
ActionSource,
|
ActionSource,
|
||||||
|
ActionPredicateFn,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
@@ -40,7 +40,8 @@ const trackAction = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class ActionManager {
|
export class ActionManager {
|
||||||
actions = {} as Record<ActionName, Action>;
|
actions = {} as Record<Action["name"], Action>;
|
||||||
|
actionPredicates = [] as ActionPredicateFn[];
|
||||||
|
|
||||||
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
updater: (actionResult: ActionResult | Promise<ActionResult>) => void;
|
||||||
|
|
||||||
@@ -68,6 +69,12 @@ export class ActionManager {
|
|||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerActionPredicate(predicate: ActionPredicateFn) {
|
||||||
|
if (!this.actionPredicates.includes(predicate)) {
|
||||||
|
this.actionPredicates.push(predicate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
registerAction(action: Action) {
|
registerAction(action: Action) {
|
||||||
this.actions[action.name] = action;
|
this.actions[action.name] = action;
|
||||||
}
|
}
|
||||||
@@ -84,7 +91,7 @@ export class ActionManager {
|
|||||||
(action) =>
|
(action) =>
|
||||||
(action.name in canvasActions
|
(action.name in canvasActions
|
||||||
? canvasActions[action.name as keyof typeof canvasActions]
|
? canvasActions[action.name as keyof typeof canvasActions]
|
||||||
: true) &&
|
: this.isActionEnabled(action, { noPredicates: true })) &&
|
||||||
action.keyTest &&
|
action.keyTest &&
|
||||||
action.keyTest(
|
action.keyTest(
|
||||||
event,
|
event,
|
||||||
@@ -134,7 +141,7 @@ export class ActionManager {
|
|||||||
/**
|
/**
|
||||||
* @param data additional data sent to the PanelComponent
|
* @param data additional data sent to the PanelComponent
|
||||||
*/
|
*/
|
||||||
renderAction = (name: ActionName, data?: PanelComponentProps["data"]) => {
|
renderAction = (name: Action["name"], data?: PanelComponentProps["data"]) => {
|
||||||
const canvasActions = this.app.props.UIOptions.canvasActions;
|
const canvasActions = this.app.props.UIOptions.canvasActions;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -142,7 +149,7 @@ export class ActionManager {
|
|||||||
"PanelComponent" in this.actions[name] &&
|
"PanelComponent" in this.actions[name] &&
|
||||||
(name in canvasActions
|
(name in canvasActions
|
||||||
? canvasActions[name as keyof typeof canvasActions]
|
? canvasActions[name as keyof typeof canvasActions]
|
||||||
: true)
|
: this.isActionEnabled(this.actions[name], { noPredicates: true }))
|
||||||
) {
|
) {
|
||||||
const action = this.actions[name];
|
const action = this.actions[name];
|
||||||
const PanelComponent = action.PanelComponent!;
|
const PanelComponent = action.PanelComponent!;
|
||||||
@@ -176,13 +183,31 @@ export class ActionManager {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
isActionEnabled = (action: Action) => {
|
isActionEnabled = (
|
||||||
const elements = this.getElementsIncludingDeleted();
|
action: Action,
|
||||||
|
opts?: {
|
||||||
|
elements?: readonly ExcalidrawElement[];
|
||||||
|
data?: Record<string, any>;
|
||||||
|
noPredicates?: boolean;
|
||||||
|
},
|
||||||
|
): boolean => {
|
||||||
|
const elements = opts?.elements ?? this.getElementsIncludingDeleted();
|
||||||
const appState = this.getAppState();
|
const appState = this.getAppState();
|
||||||
|
const data = opts?.data;
|
||||||
|
|
||||||
return (
|
if (
|
||||||
!action.predicate ||
|
!opts?.noPredicates &&
|
||||||
action.predicate(elements, appState, this.app.props, this.app)
|
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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -32,6 +32,15 @@ type ActionFn = (
|
|||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
) => ActionResult | Promise<ActionResult>;
|
) => 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 UpdaterFn = (res: ActionResult) => void;
|
||||||
export type ActionFilterFn = (action: Action) => void;
|
export type ActionFilterFn = (action: Action) => void;
|
||||||
|
|
||||||
@@ -116,6 +125,11 @@ export type ActionName =
|
|||||||
| "toggleLinearEditor"
|
| "toggleLinearEditor"
|
||||||
| "toggleEraserTool"
|
| "toggleEraserTool"
|
||||||
| "toggleHandTool"
|
| "toggleHandTool"
|
||||||
|
| "selectAllElementsInFrame"
|
||||||
|
| "removeAllElementsFromFrame"
|
||||||
|
| "toggleFrameRendering"
|
||||||
|
| "setFrameAsActiveTool"
|
||||||
|
| "createContainerFromText"
|
||||||
| "wrapTextInContainer";
|
| "wrapTextInContainer";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
@@ -127,7 +141,7 @@ export type PanelComponentProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
name: ActionName;
|
name: string;
|
||||||
PanelComponent?: React.FC<PanelComponentProps>;
|
PanelComponent?: React.FC<PanelComponentProps>;
|
||||||
perform: ActionFn;
|
perform: ActionFn;
|
||||||
keyPriority?: number;
|
keyPriority?: number;
|
||||||
@@ -147,6 +161,7 @@ export interface Action {
|
|||||||
appState: AppState,
|
appState: AppState,
|
||||||
appProps: ExcalidrawProps,
|
appProps: ExcalidrawProps,
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
|
data?: Record<string, any>,
|
||||||
) => boolean;
|
) => boolean;
|
||||||
checked?: (appState: Readonly<AppState>) => boolean;
|
checked?: (appState: Readonly<AppState>) => boolean;
|
||||||
trackEvent:
|
trackEvent:
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { ExcalidrawElement } from "./element/types";
|
import { ExcalidrawElement } from "./element/types";
|
||||||
import { newElementWith } from "./element/mutateElement";
|
import { newElementWith } from "./element/mutateElement";
|
||||||
import { Box, getCommonBoundingBox } from "./element/bounds";
|
import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
|
||||||
import { getMaximumGroups } from "./groups";
|
import { getMaximumGroups } from "./groups";
|
||||||
|
|
||||||
export interface Alignment {
|
export interface Alignment {
|
||||||
@@ -33,7 +33,7 @@ export const alignElements = (
|
|||||||
|
|
||||||
const calculateTranslation = (
|
const calculateTranslation = (
|
||||||
group: ExcalidrawElement[],
|
group: ExcalidrawElement[],
|
||||||
selectionBoundingBox: Box,
|
selectionBoundingBox: BoundingBox,
|
||||||
{ axis, position }: Alignment,
|
{ axis, position }: Alignment,
|
||||||
): { x: number; y: number } => {
|
): { x: number; y: number } => {
|
||||||
const groupBoundingBox = getCommonBoundingBox(group);
|
const groupBoundingBox = getCommonBoundingBox(group);
|
||||||
|
|||||||
@@ -78,11 +78,16 @@ export const getDefaultAppState = (): Omit<
|
|||||||
scrollY: 0,
|
scrollY: 0,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
|
selectedElementsAreBeingDragged: false,
|
||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
showStats: false,
|
showStats: false,
|
||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
|
shouldRenderFrames: true,
|
||||||
|
frameToHighlight: null,
|
||||||
|
editingFrame: null,
|
||||||
|
elementsToHighlight: null,
|
||||||
toast: null,
|
toast: null,
|
||||||
viewBackgroundColor: COLOR_PALETTE.white,
|
viewBackgroundColor: COLOR_PALETTE.white,
|
||||||
zenModeEnabled: false,
|
zenModeEnabled: false,
|
||||||
@@ -176,11 +181,20 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
scrollY: { browser: true, export: false, server: false },
|
scrollY: { browser: true, export: false, server: false },
|
||||||
selectedElementIds: { browser: true, export: false, server: false },
|
selectedElementIds: { browser: true, export: false, server: false },
|
||||||
selectedGroupIds: { 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 },
|
selectionElement: { browser: false, export: false, server: false },
|
||||||
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
||||||
showStats: { browser: true, export: false, server: false },
|
showStats: { browser: true, export: false, server: false },
|
||||||
startBoundElement: { browser: false, export: false, server: false },
|
startBoundElement: { browser: false, export: false, server: false },
|
||||||
suggestedBindings: { 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 },
|
toast: { browser: false, export: false, server: false },
|
||||||
viewBackgroundColor: { browser: true, export: true, server: true },
|
viewBackgroundColor: { browser: true, export: true, server: true },
|
||||||
width: { browser: false, export: false, server: false },
|
width: { browser: false, export: false, server: false },
|
||||||
|
|||||||
+26
-26
@@ -1,31 +1,31 @@
|
|||||||
import {
|
function hashToInteger(id: string) {
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
|
let hash = 0;
|
||||||
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
|
if (id.length === 0) {
|
||||||
getAllColorsSpecificShade,
|
return hash;
|
||||||
} from "./colors";
|
|
||||||
import { AppState } from "./types";
|
|
||||||
|
|
||||||
const BG_COLORS = getAllColorsSpecificShade(
|
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
|
|
||||||
);
|
|
||||||
const STROKE_COLORS = getAllColorsSpecificShade(
|
|
||||||
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
|
|
||||||
);
|
|
||||||
|
|
||||||
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
|
for (let i = 0; i < id.length; i++) {
|
||||||
const sum = clientId.split("").reduce((a, str) => a + str.charCodeAt(0), 0);
|
const char = id.charCodeAt(i);
|
||||||
|
hash = (hash << 5) - hash + char;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
export const getClientColor = (
|
||||||
background: BG_COLORS[sum % BG_COLORS.length],
|
/**
|
||||||
stroke: STROKE_COLORS[sum % STROKE_COLORS.length],
|
* 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;
|
||||||
|
|
||||||
|
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+20
-1
@@ -7,6 +7,9 @@ import { SVG_EXPORT_TAG } from "./scene/export";
|
|||||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||||
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
||||||
import { isInitializedImageElement } from "./element/typeChecks";
|
import { isInitializedImageElement } from "./element/typeChecks";
|
||||||
|
import { deepCopyElement } from "./element/newElement";
|
||||||
|
import { mutateElement } from "./element/mutateElement";
|
||||||
|
import { getContainingFrame } from "./frame";
|
||||||
import { isPromiseLike, isTestEnv } from "./utils";
|
import { isPromiseLike, isTestEnv } from "./utils";
|
||||||
|
|
||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
@@ -57,6 +60,9 @@ export const copyToClipboard = async (
|
|||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
files: BinaryFiles | null,
|
files: BinaryFiles | null,
|
||||||
) => {
|
) => {
|
||||||
|
const framesToCopy = new Set(
|
||||||
|
elements.filter((element) => element.type === "frame"),
|
||||||
|
);
|
||||||
let foundFile = false;
|
let foundFile = false;
|
||||||
|
|
||||||
const _files = elements.reduce((acc, element) => {
|
const _files = elements.reduce((acc, element) => {
|
||||||
@@ -78,7 +84,20 @@ export const copyToClipboard = async (
|
|||||||
// select binded text elements when copying
|
// select binded text elements when copying
|
||||||
const contents: ElementsClipboard = {
|
const contents: ElementsClipboard = {
|
||||||
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
elements,
|
elements: elements.map((element) => {
|
||||||
|
if (
|
||||||
|
getContainingFrame(element) &&
|
||||||
|
!framesToCopy.has(getContainingFrame(element)!)
|
||||||
|
) {
|
||||||
|
const copiedElement = deepCopyElement(element);
|
||||||
|
mutateElement(copiedElement, {
|
||||||
|
frameId: null,
|
||||||
|
});
|
||||||
|
return copiedElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}),
|
||||||
files: files ? _files : undefined,
|
files: files ? _files : undefined,
|
||||||
};
|
};
|
||||||
const json = JSON.stringify(contents);
|
const json = JSON.stringify(contents);
|
||||||
|
|||||||
+114
-36
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement, PointerType } from "../element/types";
|
import { ExcalidrawElement, PointerType } from "../element/types";
|
||||||
@@ -35,6 +35,9 @@ import {
|
|||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
|
|
||||||
import "./Actions.scss";
|
import "./Actions.scss";
|
||||||
|
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||||
|
import { extraToolsIcon, frameToolIcon } from "./icons";
|
||||||
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
appState,
|
appState,
|
||||||
@@ -89,7 +92,8 @@ export const SelectedShapeActions = ({
|
|||||||
<div>
|
<div>
|
||||||
{((hasStrokeColor(appState.activeTool.type) &&
|
{((hasStrokeColor(appState.activeTool.type) &&
|
||||||
appState.activeTool.type !== "image" &&
|
appState.activeTool.type !== "image" &&
|
||||||
commonSelectedType !== "image") ||
|
commonSelectedType !== "image" &&
|
||||||
|
commonSelectedType !== "frame") ||
|
||||||
targetElements.some((element) => hasStrokeColor(element.type))) &&
|
targetElements.some((element) => hasStrokeColor(element.type))) &&
|
||||||
renderAction("changeStrokeColor")}
|
renderAction("changeStrokeColor")}
|
||||||
</div>
|
</div>
|
||||||
@@ -220,28 +224,78 @@ export const ShapesSwitcher = ({
|
|||||||
setAppState: React.Component<any, UIAppState>["setState"];
|
setAppState: React.Component<any, UIAppState>["setState"];
|
||||||
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
}) => (
|
}) => {
|
||||||
<>
|
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
||||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
const device = useDevice();
|
||||||
const label = t(`toolBar.${value}`);
|
return (
|
||||||
const letter =
|
<>
|
||||||
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
||||||
const shortcut = letter
|
const label = t(`toolBar.${value}`);
|
||||||
? `${letter} ${t("helpDialog.or")} ${numericKey}`
|
const letter =
|
||||||
: `${numericKey}`;
|
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
||||||
return (
|
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 ? (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
className={clsx("Shape", { fillable })}
|
className={clsx("Shape", { fillable: false })}
|
||||||
key={value}
|
|
||||||
type="radio"
|
type="radio"
|
||||||
icon={icon}
|
icon={frameToolIcon}
|
||||||
checked={activeTool.type === value}
|
checked={activeTool.type === "frame"}
|
||||||
name="editor-current-shape"
|
name="editor-current-shape"
|
||||||
title={`${capitalizeString(label)} — ${shortcut}`}
|
title={`${capitalizeString(
|
||||||
keyBindingLabel={numericKey || letter}
|
t("toolBar.frame"),
|
||||||
aria-label={capitalizeString(label)}
|
)} — ${KEYS.F.toLocaleUpperCase()}`}
|
||||||
aria-keyshortcuts={shortcut}
|
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
|
||||||
data-testid={`toolbar-${value}`}
|
aria-label={capitalizeString(t("toolBar.frame"))}
|
||||||
|
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
|
||||||
|
data-testid={`toolbar-frame`}
|
||||||
onPointerDown={({ pointerType }) => {
|
onPointerDown={({ pointerType }) => {
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
setAppState({
|
setAppState({
|
||||||
@@ -251,30 +305,54 @@ export const ShapesSwitcher = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onChange={({ pointerType }) => {
|
onChange={({ pointerType }) => {
|
||||||
if (appState.activeTool.type !== value) {
|
trackEvent("toolbar", "frame", "ui");
|
||||||
trackEvent("toolbar", value, "ui");
|
|
||||||
}
|
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
const nextActiveTool = updateActiveTool(appState, {
|
||||||
type: value,
|
type: "frame",
|
||||||
});
|
});
|
||||||
setAppState({
|
setAppState({
|
||||||
activeTool: nextActiveTool,
|
activeTool: nextActiveTool,
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
selectedElementIds: {},
|
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 = ({
|
export const ZoomActions = ({
|
||||||
renderAction,
|
renderAction,
|
||||||
|
|||||||
+919
-142
File diff suppressed because it is too large
Load Diff
@@ -10,10 +10,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: $oc-white;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.625rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 800;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
&-img {
|
&-img {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { getNameInitial } from "../clients";
|
|||||||
type AvatarProps = {
|
type AvatarProps = {
|
||||||
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
color: string;
|
color: string;
|
||||||
border: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
src?: string;
|
src?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isTransparent, isWritableElement } from "../../utils";
|
import { isInteractive, isTransparent, isWritableElement } from "../../utils";
|
||||||
import { ExcalidrawElement } from "../../element/types";
|
import { ExcalidrawElement } from "../../element/types";
|
||||||
import { AppState } from "../../types";
|
import { AppState } from "../../types";
|
||||||
import { TopPicks } from "./TopPicks";
|
import { TopPicks } from "./TopPicks";
|
||||||
@@ -121,11 +121,14 @@ const ColorPickerPopupContent = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onCloseAutoFocus={(e) => {
|
onCloseAutoFocus={(e) => {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
// prevents focusing the trigger
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
// return focus to excalidraw container
|
// return focus to excalidraw container unless
|
||||||
if (container) {
|
// user focuses an interactive element, such as a button, or
|
||||||
|
// enters the text editor by clicking on canvas with the text tool
|
||||||
|
if (container && !isInteractive(document.activeElement)) {
|
||||||
container.focus();
|
container.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("toolBar.eraser")}
|
label={t("toolBar.eraser")}
|
||||||
shortcuts={[KEYS.E, KEYS["0"]]}
|
shortcuts={[KEYS.E, KEYS["0"]]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut label={t("toolBar.frame")} shortcuts={[KEYS.F]} />
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.eyeDropper")}
|
label={t("labels.eyeDropper")}
|
||||||
shortcuts={[KEYS.I, "Shift+S", "Shift+G"]}
|
shortcuts={[KEYS.I, "Shift+S", "Shift+G"]}
|
||||||
|
|||||||
@@ -84,7 +84,10 @@ const ImageExportModal = ({
|
|||||||
const [renderError, setRenderError] = useState<Error | null>(null);
|
const [renderError, setRenderError] = useState<Error | null>(null);
|
||||||
|
|
||||||
const exportedElements = exportSelected
|
const exportedElements = exportSelected
|
||||||
? getSelectedElements(elements, appState, true)
|
? getSelectedElements(elements, appState, {
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
})
|
||||||
: elements;
|
: elements;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -204,12 +204,7 @@ const LayerUI = ({
|
|||||||
return (
|
return (
|
||||||
<FixedSideContainer side="top">
|
<FixedSideContainer side="top">
|
||||||
<div className="App-menu App-menu_top">
|
<div className="App-menu App-menu_top">
|
||||||
<Stack.Col
|
<Stack.Col gap={6} className={clsx("App-menu_top__left")}>
|
||||||
gap={6}
|
|
||||||
className={clsx("App-menu_top__left", {
|
|
||||||
"disable-pointerEvents": appState.zenModeEnabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{renderCanvasActions()}
|
{renderCanvasActions()}
|
||||||
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
@@ -254,7 +249,7 @@ const LayerUI = ({
|
|||||||
title={t("toolBar.lock")}
|
title={t("toolBar.lock")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="App-toolbar__divider"></div>
|
<div className="App-toolbar__divider" />
|
||||||
|
|
||||||
<HandButton
|
<HandButton
|
||||||
checked={isHandToolActive(appState)}
|
checked={isHandToolActive(appState)}
|
||||||
|
|||||||
@@ -148,7 +148,11 @@ const usePendingElementsMemo = (
|
|||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
) => {
|
) => {
|
||||||
const create = () => getSelectedElements(elements, appState, true);
|
const create = () =>
|
||||||
|
getSelectedElements(elements, appState, {
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
});
|
||||||
const val = useRef(create());
|
const val = useRef(create());
|
||||||
const prevAppState = useRef<UIAppState>(appState);
|
const prevAppState = useRef<UIAppState>(appState);
|
||||||
const prevElements = useRef(elements);
|
const prevElements = useRef(elements);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import "./ToolIcon.scss";
|
import "./ToolIcon.scss";
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { CSSProperties, useEffect, useRef, useState } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useExcalidrawContainer } from "./App";
|
import { useExcalidrawContainer } from "./App";
|
||||||
import { AbortError } from "../errors";
|
import { AbortError } from "../errors";
|
||||||
@@ -25,6 +25,7 @@ type ToolButtonBaseProps = {
|
|||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,6 +115,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
"ToolIcon--plain": props.type === "icon",
|
"ToolIcon--plain": props.type === "icon",
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
|
style={props.style}
|
||||||
data-testid={props["data-testid"]}
|
data-testid={props["data-testid"]}
|
||||||
hidden={props.hidden}
|
hidden={props.hidden}
|
||||||
title={props.title}
|
title={props.title}
|
||||||
|
|||||||
@@ -15,7 +15,24 @@
|
|||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
background-color: var(--default-border-color);
|
background-color: var(--default-border-color);
|
||||||
margin: 0 0.5rem;
|
margin: 0 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 clsx from "clsx";
|
||||||
import { useUIAppState } from "../../context/ui-appState";
|
|
||||||
import { useDevice } from "../App";
|
import { useDevice } from "../App";
|
||||||
|
|
||||||
const MenuTrigger = ({
|
const MenuTrigger = ({
|
||||||
className = "",
|
className = "",
|
||||||
children,
|
children,
|
||||||
onToggle,
|
onToggle,
|
||||||
|
title,
|
||||||
|
...rest
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
}) => {
|
title?: string;
|
||||||
const appState = useUIAppState();
|
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const classNames = clsx(
|
const classNames = clsx(
|
||||||
`dropdown-menu-button ${className}`,
|
`dropdown-menu-button ${className}`,
|
||||||
"zen-mode-transition",
|
"zen-mode-transition",
|
||||||
{
|
{
|
||||||
"transition-left": appState.zenModeEnabled,
|
|
||||||
"dropdown-menu-button--mobile": device.isMobile,
|
"dropdown-menu-button--mobile": device.isMobile,
|
||||||
},
|
},
|
||||||
).trim();
|
).trim();
|
||||||
@@ -28,6 +28,8 @@ const MenuTrigger = ({
|
|||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="dropdown-menu-button"
|
data-testid="dropdown-menu-button"
|
||||||
|
title={title}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -12,17 +12,17 @@ describe("Test internal component fallback rendering", () => {
|
|||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(queryAllByTestId(container, "dropdown-menu-button")?.length).toBe(2);
|
expect(queryAllByTestId(container, "main-menu-trigger")?.length).toBe(2);
|
||||||
|
|
||||||
const excalContainers = container.querySelectorAll<HTMLDivElement>(
|
const excalContainers = container.querySelectorAll<HTMLDivElement>(
|
||||||
".excalidraw-container",
|
".excalidraw-container",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
queryAllByTestId(excalContainers[0], "dropdown-menu-button")?.length,
|
queryAllByTestId(excalContainers[0], "main-menu-trigger")?.length,
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
queryAllByTestId(excalContainers[1], "dropdown-menu-button")?.length,
|
queryAllByTestId(excalContainers[1], "main-menu-trigger")?.length,
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -36,17 +36,17 @@ describe("Test internal component fallback rendering", () => {
|
|||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(queryAllByTestId(container, "dropdown-menu-button")?.length).toBe(2);
|
expect(queryAllByTestId(container, "main-menu-trigger")?.length).toBe(2);
|
||||||
|
|
||||||
const excalContainers = container.querySelectorAll<HTMLDivElement>(
|
const excalContainers = container.querySelectorAll<HTMLDivElement>(
|
||||||
".excalidraw-container",
|
".excalidraw-container",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
queryAllByTestId(excalContainers[0], "dropdown-menu-button")?.length,
|
queryAllByTestId(excalContainers[0], "main-menu-trigger")?.length,
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
queryAllByTestId(excalContainers[1], "dropdown-menu-button")?.length,
|
queryAllByTestId(excalContainers[1], "main-menu-trigger")?.length,
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,17 +62,17 @@ describe("Test internal component fallback rendering", () => {
|
|||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(queryAllByTestId(container, "dropdown-menu-button")?.length).toBe(2);
|
expect(queryAllByTestId(container, "main-menu-trigger")?.length).toBe(2);
|
||||||
|
|
||||||
const excalContainers = container.querySelectorAll<HTMLDivElement>(
|
const excalContainers = container.querySelectorAll<HTMLDivElement>(
|
||||||
".excalidraw-container",
|
".excalidraw-container",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
queryAllByTestId(excalContainers[0], "dropdown-menu-button")?.length,
|
queryAllByTestId(excalContainers[0], "main-menu-trigger")?.length,
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
queryAllByTestId(excalContainers[1], "dropdown-menu-button")?.length,
|
queryAllByTestId(excalContainers[1], "main-menu-trigger")?.length,
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,17 +84,17 @@ describe("Test internal component fallback rendering", () => {
|
|||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(queryAllByTestId(container, "dropdown-menu-button")?.length).toBe(2);
|
expect(queryAllByTestId(container, "main-menu-trigger")?.length).toBe(2);
|
||||||
|
|
||||||
const excalContainers = container.querySelectorAll<HTMLDivElement>(
|
const excalContainers = container.querySelectorAll<HTMLDivElement>(
|
||||||
".excalidraw-container",
|
".excalidraw-container",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
queryAllByTestId(excalContainers[0], "dropdown-menu-button")?.length,
|
queryAllByTestId(excalContainers[0], "main-menu-trigger")?.length,
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
queryAllByTestId(excalContainers[1], "dropdown-menu-button")?.length,
|
queryAllByTestId(excalContainers[1], "main-menu-trigger")?.length,
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1616,3 +1616,24 @@ export const eyeDropperIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
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,
|
||||||
|
);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const MainMenu = Object.assign(
|
|||||||
openMenu: appState.openMenu === "canvas" ? null : "canvas",
|
openMenu: appState.openMenu === "canvas" ? null : "canvas",
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
data-testid="main-menu-trigger"
|
||||||
>
|
>
|
||||||
{HamburgerMenuIcon}
|
{HamburgerMenuIcon}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|||||||
@@ -94,6 +94,17 @@ export const THEME = {
|
|||||||
DARK: "dark",
|
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 WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
||||||
|
|
||||||
export const DEFAULT_FONT_SIZE = 20;
|
export const DEFAULT_FONT_SIZE = 20;
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
|||||||
freedraw: true,
|
freedraw: true,
|
||||||
eraser: false,
|
eraser: false,
|
||||||
custom: true,
|
custom: true,
|
||||||
|
frame: true,
|
||||||
hand: true,
|
hand: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,6 +126,7 @@ const restoreElementWithProperties = <
|
|||||||
height: element.height || 0,
|
height: element.height || 0,
|
||||||
seed: element.seed ?? 1,
|
seed: element.seed ?? 1,
|
||||||
groupIds: element.groupIds ?? [],
|
groupIds: element.groupIds ?? [],
|
||||||
|
frameId: element.frameId ?? null,
|
||||||
roundness: element.roundness
|
roundness: element.roundness
|
||||||
? element.roundness
|
? element.roundness
|
||||||
: element.strokeSharpness === "round"
|
: element.strokeSharpness === "round"
|
||||||
@@ -272,6 +274,10 @@ const restoreElement = (
|
|||||||
return restoreElementWithProperties(element, {});
|
return restoreElementWithProperties(element, {});
|
||||||
case "diamond":
|
case "diamond":
|
||||||
return restoreElementWithProperties(element, {});
|
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.
|
// 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
|
// We also don't want to throw, but instead return void so we filter
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ export const isPointHittingLinkIcon = (
|
|||||||
if (
|
if (
|
||||||
!isMobile &&
|
!isMobile &&
|
||||||
appState.viewModeEnabled &&
|
appState.viewModeEnabled &&
|
||||||
isPointHittingElementBoundingBox(element, [x, y], threshold)
|
isPointHittingElementBoundingBox(element, [x, y], threshold, null)
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -440,7 +440,9 @@ export const shouldHideLinkPopup = (
|
|||||||
|
|
||||||
const threshold = 15 / appState.zoom.value;
|
const threshold = 15 / appState.zoom.value;
|
||||||
// hitbox to prevent hiding when hovered in element bounding box
|
// hitbox to prevent hiding when hovered in element bounding box
|
||||||
if (isPointHittingElementBoundingBox(element, [sceneX, sceneY], threshold)) {
|
if (
|
||||||
|
isPointHittingElementBoundingBox(element, [sceneX, sceneY], threshold, null)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const [x1, y1, x2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2] = getElementAbsoluteCoords(element);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export type SuggestedPointBinding = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const shouldEnableBindingForPointerEvent = (
|
export const shouldEnableBindingForPointerEvent = (
|
||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLElement>,
|
||||||
) => {
|
) => {
|
||||||
return !event[KEYS.CTRL_OR_CMD];
|
return !event[KEYS.CTRL_OR_CMD];
|
||||||
};
|
};
|
||||||
|
|||||||
+205
-61
@@ -6,7 +6,7 @@ import {
|
|||||||
NonDeleted,
|
NonDeleted,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { distance2d, rotate } from "../math";
|
import { distance2d, rotate, rotatePoint } from "../math";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { Drawable, Op } from "roughjs/bin/core";
|
import { Drawable, Op } from "roughjs/bin/core";
|
||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
@@ -25,10 +25,101 @@ import { getBoundTextElement, getContainerElement } from "./textElement";
|
|||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { Mutable } from "../utility-types";
|
import { Mutable } from "../utility-types";
|
||||||
|
|
||||||
// x and y position of top left corner, x and y position of bottom right corner
|
export type RectangleBox = {
|
||||||
export type Bounds = readonly [number, number, number, number];
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
angle: number;
|
||||||
|
};
|
||||||
|
|
||||||
type MaybeQuadraticSolution = [number | null, number | null] | false;
|
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
|
// 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.
|
// This set of functions retrieves the absolute position of the 4 points.
|
||||||
export const getElementAbsoluteCoords = (
|
export const getElementAbsoluteCoords = (
|
||||||
@@ -69,6 +160,111 @@ 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 = (
|
export const pointRelativeTo = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
absoluteCoords: Point,
|
absoluteCoords: Point,
|
||||||
@@ -454,64 +650,12 @@ const getLinearElementRotatedBounds = (
|
|||||||
return coords;
|
return coords;
|
||||||
};
|
};
|
||||||
|
|
||||||
// We could cache this stuff
|
export const getElementBounds = (element: ExcalidrawElement): Bounds => {
|
||||||
export const getElementBounds = (
|
return ElementBounds.getBounds(element);
|
||||||
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 = (
|
export const getCommonBounds = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
): [number, number, number, number] => {
|
): Bounds => {
|
||||||
if (!elements.length) {
|
if (!elements.length) {
|
||||||
return [0, 0, 0, 0];
|
return [0, 0, 0, 0];
|
||||||
}
|
}
|
||||||
@@ -608,7 +752,7 @@ export const getElementPointsCoords = (
|
|||||||
export const getClosestElementBounds = (
|
export const getClosestElementBounds = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
from: { x: number; y: number },
|
from: { x: number; y: number },
|
||||||
): [number, number, number, number] => {
|
): Bounds => {
|
||||||
if (!elements.length) {
|
if (!elements.length) {
|
||||||
return [0, 0, 0, 0];
|
return [0, 0, 0, 0];
|
||||||
}
|
}
|
||||||
@@ -629,7 +773,7 @@ export const getClosestElementBounds = (
|
|||||||
return getElementBounds(closestElement);
|
return getElementBounds(closestElement);
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Box {
|
export interface BoundingBox {
|
||||||
minX: number;
|
minX: number;
|
||||||
minY: number;
|
minY: number;
|
||||||
maxX: number;
|
maxX: number;
|
||||||
@@ -642,7 +786,7 @@ export interface Box {
|
|||||||
|
|
||||||
export const getCommonBoundingBox = (
|
export const getCommonBoundingBox = (
|
||||||
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
|
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
|
||||||
): Box => {
|
): BoundingBox => {
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
return {
|
return {
|
||||||
minX,
|
minX,
|
||||||
|
|||||||
+146
-22
@@ -26,10 +26,16 @@ import {
|
|||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
StrokeRoundness,
|
StrokeRoundness,
|
||||||
|
ExcalidrawFrameElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
|
import {
|
||||||
import { Point } from "../types";
|
getElementAbsoluteCoords,
|
||||||
|
getCurvePathOps,
|
||||||
|
getRectangleBoxAbsoluteCoords,
|
||||||
|
RectangleBox,
|
||||||
|
} from "./bounds";
|
||||||
|
import { FrameNameBoundsCache, Point } from "../types";
|
||||||
import { Drawable } from "roughjs/bin/core";
|
import { Drawable } from "roughjs/bin/core";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { getShapeForElement } from "../renderer/renderElement";
|
import { getShapeForElement } from "../renderer/renderElement";
|
||||||
@@ -61,6 +67,7 @@ const isElementDraggableFromInside = (
|
|||||||
export const hitTest = (
|
export const hitTest = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
frameNameBoundsCache: FrameNameBoundsCache,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
@@ -72,22 +79,39 @@ export const hitTest = (
|
|||||||
isElementSelected(appState, element) &&
|
isElementSelected(appState, element) &&
|
||||||
shouldShowBoundingBox([element], appState)
|
shouldShowBoundingBox([element], appState)
|
||||||
) {
|
) {
|
||||||
return isPointHittingElementBoundingBox(element, point, threshold);
|
return isPointHittingElementBoundingBox(
|
||||||
|
element,
|
||||||
|
point,
|
||||||
|
threshold,
|
||||||
|
frameNameBoundsCache,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const isHittingBoundTextElement = hitTest(boundTextElement, appState, x, y);
|
const isHittingBoundTextElement = hitTest(
|
||||||
|
boundTextElement,
|
||||||
|
appState,
|
||||||
|
frameNameBoundsCache,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
);
|
||||||
if (isHittingBoundTextElement) {
|
if (isHittingBoundTextElement) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return isHittingElementNotConsideringBoundingBox(element, appState, point);
|
return isHittingElementNotConsideringBoundingBox(
|
||||||
|
element,
|
||||||
|
appState,
|
||||||
|
frameNameBoundsCache,
|
||||||
|
point,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isHittingElementBoundingBoxWithoutHittingElement = (
|
export const isHittingElementBoundingBoxWithoutHittingElement = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
frameNameBoundsCache: FrameNameBoundsCache,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
@@ -96,19 +120,33 @@ 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
|
// 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
|
// eg for linear elements text can be outside the element bounding box
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement && hitTest(boundTextElement, appState, x, y)) {
|
if (
|
||||||
|
boundTextElement &&
|
||||||
|
hitTest(boundTextElement, appState, frameNameBoundsCache, x, y)
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!isHittingElementNotConsideringBoundingBox(element, appState, [x, y]) &&
|
!isHittingElementNotConsideringBoundingBox(
|
||||||
isPointHittingElementBoundingBox(element, [x, y], threshold)
|
element,
|
||||||
|
appState,
|
||||||
|
frameNameBoundsCache,
|
||||||
|
[x, y],
|
||||||
|
) &&
|
||||||
|
isPointHittingElementBoundingBox(
|
||||||
|
element,
|
||||||
|
[x, y],
|
||||||
|
threshold,
|
||||||
|
frameNameBoundsCache,
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isHittingElementNotConsideringBoundingBox = (
|
export const isHittingElementNotConsideringBoundingBox = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
frameNameBoundsCache: FrameNameBoundsCache | null,
|
||||||
point: Point,
|
point: Point,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const threshold = 10 / appState.zoom.value;
|
const threshold = 10 / appState.zoom.value;
|
||||||
@@ -117,7 +155,13 @@ export const isHittingElementNotConsideringBoundingBox = (
|
|||||||
: isElementDraggableFromInside(element)
|
: isElementDraggableFromInside(element)
|
||||||
? isInsideCheck
|
? isInsideCheck
|
||||||
: isNearCheck;
|
: isNearCheck;
|
||||||
return hitTestPointAgainstElement({ element, point, threshold, check });
|
return hitTestPointAgainstElement({
|
||||||
|
element,
|
||||||
|
point,
|
||||||
|
threshold,
|
||||||
|
check,
|
||||||
|
frameNameBoundsCache,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isElementSelected = (
|
const isElementSelected = (
|
||||||
@@ -129,7 +173,22 @@ export const isPointHittingElementBoundingBox = (
|
|||||||
element: NonDeleted<ExcalidrawElement>,
|
element: NonDeleted<ExcalidrawElement>,
|
||||||
[x, y]: Point,
|
[x, y]: Point,
|
||||||
threshold: number,
|
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 [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const elementCenterX = (x1 + x2) / 2;
|
const elementCenterX = (x1 + x2) / 2;
|
||||||
const elementCenterY = (y1 + y2) / 2;
|
const elementCenterY = (y1 + y2) / 2;
|
||||||
@@ -157,7 +216,13 @@ export const bindingBorderTest = (
|
|||||||
const threshold = maxBindingGap(element, element.width, element.height);
|
const threshold = maxBindingGap(element, element.width, element.height);
|
||||||
const check = isOutsideCheck;
|
const check = isOutsideCheck;
|
||||||
const point: Point = [x, y];
|
const point: Point = [x, y];
|
||||||
return hitTestPointAgainstElement({ element, point, threshold, check });
|
return hitTestPointAgainstElement({
|
||||||
|
element,
|
||||||
|
point,
|
||||||
|
threshold,
|
||||||
|
check,
|
||||||
|
frameNameBoundsCache: null,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const maxBindingGap = (
|
export const maxBindingGap = (
|
||||||
@@ -177,6 +242,7 @@ type HitTestArgs = {
|
|||||||
point: Point;
|
point: Point;
|
||||||
threshold: number;
|
threshold: number;
|
||||||
check: (distance: number, threshold: number) => boolean;
|
check: (distance: number, threshold: number) => boolean;
|
||||||
|
frameNameBoundsCache: FrameNameBoundsCache | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
|
const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
|
||||||
@@ -208,6 +274,27 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
|
|||||||
"This should not happen, we need to investigate why it does.",
|
"This should not happen, we need to investigate why it does.",
|
||||||
);
|
);
|
||||||
return false;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -219,6 +306,7 @@ export const distanceToBindableElement = (
|
|||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
|
case "frame":
|
||||||
return distanceToRectangle(element, point);
|
return distanceToRectangle(element, point);
|
||||||
case "diamond":
|
case "diamond":
|
||||||
return distanceToDiamond(element, point);
|
return distanceToDiamond(element, point);
|
||||||
@@ -248,7 +336,8 @@ const distanceToRectangle = (
|
|||||||
| ExcalidrawRectangleElement
|
| ExcalidrawRectangleElement
|
||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
| ExcalidrawFreeDrawElement
|
| ExcalidrawFreeDrawElement
|
||||||
| ExcalidrawImageElement,
|
| ExcalidrawImageElement
|
||||||
|
| ExcalidrawFrameElement,
|
||||||
point: Point,
|
point: Point,
|
||||||
): number => {
|
): number => {
|
||||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
|
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
|
||||||
@@ -258,6 +347,14 @@ 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 = (
|
const distanceToDiamond = (
|
||||||
element: ExcalidrawDiamondElement,
|
element: ExcalidrawDiamondElement,
|
||||||
point: Point,
|
point: Point,
|
||||||
@@ -457,8 +554,7 @@ const pointRelativeToElement = (
|
|||||||
): [GA.Point, GA.Point, number, number] => {
|
): [GA.Point, GA.Point, number, number] => {
|
||||||
const point = GAPoint.from(pointTuple);
|
const point = GAPoint.from(pointTuple);
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const elementCoords = getElementAbsoluteCoords(element);
|
const center = coordsCenter(x1, y1, x2, y2);
|
||||||
const center = coordsCenter([x1, y1, x2, y2]);
|
|
||||||
// GA has angle orientation opposite to `rotate`
|
// GA has angle orientation opposite to `rotate`
|
||||||
const rotate = GATransform.rotation(center, element.angle);
|
const rotate = GATransform.rotation(center, element.angle);
|
||||||
const pointRotated = GATransform.apply(rotate, point);
|
const pointRotated = GATransform.apply(rotate, point);
|
||||||
@@ -466,9 +562,26 @@ const pointRelativeToElement = (
|
|||||||
const pointRelToCenterAbs = GAPoint.abs(pointRelToCenter);
|
const pointRelToCenterAbs = GAPoint.abs(pointRelToCenter);
|
||||||
const elementPos = GA.offset(element.x, element.y);
|
const elementPos = GA.offset(element.x, element.y);
|
||||||
const pointRelToPos = GA.sub(pointRotated, elementPos);
|
const pointRelToPos = GA.sub(pointRotated, elementPos);
|
||||||
const [ax, ay, bx, by] = elementCoords;
|
const halfWidth = (x2 - x1) / 2;
|
||||||
const halfWidth = (bx - ax) / 2;
|
const halfHeight = (y2 - y1) / 2;
|
||||||
const halfHeight = (by - ay) / 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;
|
||||||
return [pointRelToPos, pointRelToCenterAbs, halfWidth, halfHeight];
|
return [pointRelToPos, pointRelToCenterAbs, halfWidth, halfHeight];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -490,7 +603,7 @@ const relativizationToElementCenter = (
|
|||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
): GA.Transform => {
|
): GA.Transform => {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
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`
|
// GA has angle orientation opposite to `rotate`
|
||||||
const rotate = GATransform.rotation(center, element.angle);
|
const rotate = GATransform.rotation(center, element.angle);
|
||||||
const translate = GA.reverse(
|
const translate = GA.reverse(
|
||||||
@@ -499,8 +612,13 @@ const relativizationToElementCenter = (
|
|||||||
return GATransform.compose(rotate, translate);
|
return GATransform.compose(rotate, translate);
|
||||||
};
|
};
|
||||||
|
|
||||||
const coordsCenter = ([ax, ay, bx, by]: Bounds): GA.Point => {
|
const coordsCenter = (
|
||||||
return GA.point((ax + bx) / 2, (ay + by) / 2);
|
x1: number,
|
||||||
|
y1: number,
|
||||||
|
x2: number,
|
||||||
|
y2: number,
|
||||||
|
): GA.Point => {
|
||||||
|
return GA.point((x1 + x2) / 2, (y1 + y2) / 2);
|
||||||
};
|
};
|
||||||
|
|
||||||
// The focus distance is the oriented ratio between the size of
|
// The focus distance is the oriented ratio between the size of
|
||||||
@@ -531,6 +649,7 @@ export const determineFocusDistance = (
|
|||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
|
case "frame":
|
||||||
return c / (hwidth * (nabs + q * mabs));
|
return c / (hwidth * (nabs + q * mabs));
|
||||||
case "diamond":
|
case "diamond":
|
||||||
return mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight);
|
return mabs < nabs ? c / (nabs * hwidth) : c / (mabs * hheight);
|
||||||
@@ -548,7 +667,7 @@ export const determineFocusPoint = (
|
|||||||
): Point => {
|
): Point => {
|
||||||
if (focus === 0) {
|
if (focus === 0) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
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);
|
return GAPoint.toTuple(center);
|
||||||
}
|
}
|
||||||
const relateToCenter = relativizationToElementCenter(element);
|
const relateToCenter = relativizationToElementCenter(element);
|
||||||
@@ -563,6 +682,7 @@ export const determineFocusPoint = (
|
|||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
|
case "frame":
|
||||||
point = findFocusPointForRectangulars(element, focus, adjecentPointRel);
|
point = findFocusPointForRectangulars(element, focus, adjecentPointRel);
|
||||||
break;
|
break;
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
@@ -613,6 +733,7 @@ const getSortedElementLineIntersections = (
|
|||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
|
case "frame":
|
||||||
const corners = getCorners(element);
|
const corners = getCorners(element);
|
||||||
intersections = corners
|
intersections = corners
|
||||||
.flatMap((point, i) => {
|
.flatMap((point, i) => {
|
||||||
@@ -646,7 +767,8 @@ const getCorners = (
|
|||||||
| ExcalidrawRectangleElement
|
| ExcalidrawRectangleElement
|
||||||
| ExcalidrawImageElement
|
| ExcalidrawImageElement
|
||||||
| ExcalidrawDiamondElement
|
| ExcalidrawDiamondElement
|
||||||
| ExcalidrawTextElement,
|
| ExcalidrawTextElement
|
||||||
|
| ExcalidrawFrameElement,
|
||||||
scale: number = 1,
|
scale: number = 1,
|
||||||
): GA.Point[] => {
|
): GA.Point[] => {
|
||||||
const hx = (scale * element.width) / 2;
|
const hx = (scale * element.width) / 2;
|
||||||
@@ -655,6 +777,7 @@ const getCorners = (
|
|||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
|
case "frame":
|
||||||
return [
|
return [
|
||||||
GA.point(hx, hy),
|
GA.point(hx, hy),
|
||||||
GA.point(hx, -hy),
|
GA.point(hx, -hy),
|
||||||
@@ -802,7 +925,8 @@ export const findFocusPointForRectangulars = (
|
|||||||
| ExcalidrawRectangleElement
|
| ExcalidrawRectangleElement
|
||||||
| ExcalidrawImageElement
|
| ExcalidrawImageElement
|
||||||
| ExcalidrawDiamondElement
|
| ExcalidrawDiamondElement
|
||||||
| ExcalidrawTextElement,
|
| ExcalidrawTextElement
|
||||||
|
| ExcalidrawFrameElement,
|
||||||
// Between -1 and 1 for how far away should the focus point be relative
|
// Between -1 and 1 for how far away should the focus point be relative
|
||||||
// to the size of the element. Sign determines orientation.
|
// to the size of the element. Sign determines orientation.
|
||||||
relativeDistance: number,
|
relativeDistance: number,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { NonDeletedExcalidrawElement } from "./types";
|
|||||||
import { AppState, PointerDownState } from "../types";
|
import { AppState, PointerDownState } from "../types";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { isSelectedViaGroup } from "../groups";
|
import { isSelectedViaGroup } from "../groups";
|
||||||
|
import Scene from "../scene/Scene";
|
||||||
|
import { isFrameElement } from "./typeChecks";
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
@@ -16,10 +18,31 @@ export const dragSelectedElements = (
|
|||||||
distanceX: number = 0,
|
distanceX: number = 0,
|
||||||
distanceY: number = 0,
|
distanceY: number = 0,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1] = getCommonBounds(selectedElements);
|
const [x1, y1] = getCommonBounds(selectedElements);
|
||||||
const offset = { x: pointerX - x1, y: pointerY - y1 };
|
const offset = { x: pointerX - x1, y: pointerY - y1 };
|
||||||
selectedElements.forEach((element) => {
|
|
||||||
|
// 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) => {
|
||||||
updateElementCoords(
|
updateElementCoords(
|
||||||
lockDirection,
|
lockDirection,
|
||||||
distanceX,
|
distanceX,
|
||||||
@@ -38,7 +61,13 @@ export const dragSelectedElements = (
|
|||||||
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
|
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
|
||||||
) {
|
) {
|
||||||
const textElement = getBoundTextElement(element);
|
const textElement = getBoundTextElement(element);
|
||||||
if (textElement) {
|
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))
|
||||||
|
) {
|
||||||
updateElementCoords(
|
updateElementCoords(
|
||||||
lockDirection,
|
lockDirection,
|
||||||
distanceX,
|
distanceX,
|
||||||
@@ -50,7 +79,7 @@ export const dragSelectedElements = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateBoundElements(element, {
|
updateBoundElements(element, {
|
||||||
simultaneouslyUpdated: selectedElements,
|
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+13
-1
@@ -2,6 +2,7 @@ import {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
ExcalidrawFrameElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { isInvisiblySmallElement } from "./sizeHelpers";
|
import { isInvisiblySmallElement } from "./sizeHelpers";
|
||||||
import { isLinearElementType } from "./typeChecks";
|
import { isLinearElementType } from "./typeChecks";
|
||||||
@@ -49,7 +50,11 @@ export {
|
|||||||
getDragOffsetXY,
|
getDragOffsetXY,
|
||||||
dragNewElement,
|
dragNewElement,
|
||||||
} from "./dragElements";
|
} from "./dragElements";
|
||||||
export { isTextElement, isExcalidrawElement } from "./typeChecks";
|
export {
|
||||||
|
isTextElement,
|
||||||
|
isExcalidrawElement,
|
||||||
|
isFrameElement,
|
||||||
|
} from "./typeChecks";
|
||||||
export { textWysiwyg } from "./textWysiwyg";
|
export { textWysiwyg } from "./textWysiwyg";
|
||||||
export { redrawTextBoundingBox } from "./textElement";
|
export { redrawTextBoundingBox } from "./textElement";
|
||||||
export {
|
export {
|
||||||
@@ -74,6 +79,13 @@ export const getNonDeletedElements = (elements: readonly ExcalidrawElement[]) =>
|
|||||||
(element) => !element.isDeleted,
|
(element) => !element.isDeleted,
|
||||||
) as readonly NonDeletedExcalidrawElement[];
|
) as readonly NonDeletedExcalidrawElement[];
|
||||||
|
|
||||||
|
export const getNonDeletedFrames = (
|
||||||
|
frames: readonly ExcalidrawFrameElement[],
|
||||||
|
) =>
|
||||||
|
frames.filter(
|
||||||
|
(frame) => !frame.isDeleted,
|
||||||
|
) as readonly NonDeleted<ExcalidrawFrameElement>[];
|
||||||
|
|
||||||
export const isNonDeletedElement = <T extends ExcalidrawElement>(
|
export const isNonDeletedElement = <T extends ExcalidrawElement>(
|
||||||
element: T,
|
element: T,
|
||||||
): element is NonDeleted<T> => !element.isDeleted;
|
): element is NonDeleted<T> => !element.isDeleted;
|
||||||
|
|||||||
@@ -594,7 +594,7 @@ export class LinearElementEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static handlePointerDown(
|
static handlePointerDown(
|
||||||
event: React.PointerEvent<HTMLCanvasElement>,
|
event: React.PointerEvent<HTMLElement>,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
history: History,
|
history: History,
|
||||||
scenePointer: { x: number; y: number },
|
scenePointer: { x: number; y: number },
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ExcalidrawFreeDrawElement,
|
ExcalidrawFreeDrawElement,
|
||||||
FontFamilyValues,
|
FontFamilyValues,
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
|
ExcalidrawFrameElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import {
|
import {
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
@@ -50,6 +51,7 @@ type ElementConstructorOpts = MarkOptional<
|
|||||||
| "height"
|
| "height"
|
||||||
| "angle"
|
| "angle"
|
||||||
| "groupIds"
|
| "groupIds"
|
||||||
|
| "frameId"
|
||||||
| "boundElements"
|
| "boundElements"
|
||||||
| "seed"
|
| "seed"
|
||||||
| "version"
|
| "version"
|
||||||
@@ -82,6 +84,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
|||||||
height = 0,
|
height = 0,
|
||||||
angle = 0,
|
angle = 0,
|
||||||
groupIds = [],
|
groupIds = [],
|
||||||
|
frameId = null,
|
||||||
roundness = null,
|
roundness = null,
|
||||||
boundElements = null,
|
boundElements = null,
|
||||||
link = null,
|
link = null,
|
||||||
@@ -106,6 +109,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
|||||||
roughness,
|
roughness,
|
||||||
opacity,
|
opacity,
|
||||||
groupIds,
|
groupIds,
|
||||||
|
frameId,
|
||||||
roundness,
|
roundness,
|
||||||
seed: rest.seed ?? randomInteger(),
|
seed: rest.seed ?? randomInteger(),
|
||||||
version: rest.version || 1,
|
version: rest.version || 1,
|
||||||
@@ -126,6 +130,21 @@ export const newElement = (
|
|||||||
): NonDeleted<ExcalidrawGenericElement> =>
|
): NonDeleted<ExcalidrawGenericElement> =>
|
||||||
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
_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 */
|
/** computes element x/y offset based on textAlign/verticalAlign */
|
||||||
const getTextElementPositionOffsets = (
|
const getTextElementPositionOffsets = (
|
||||||
opts: {
|
opts: {
|
||||||
@@ -158,6 +177,7 @@ export const newTextElement = (
|
|||||||
containerId?: ExcalidrawTextContainer["id"];
|
containerId?: ExcalidrawTextContainer["id"];
|
||||||
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
lineHeight?: ExcalidrawTextElement["lineHeight"];
|
||||||
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
|
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
|
||||||
|
isFrameName?: boolean;
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawTextElement> => {
|
): NonDeleted<ExcalidrawTextElement> => {
|
||||||
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
|
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
|
||||||
@@ -192,6 +212,7 @@ export const newTextElement = (
|
|||||||
containerId: opts.containerId || null,
|
containerId: opts.containerId || null,
|
||||||
originalText: text,
|
originalText: text,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
|
isFrameName: opts.isFrameName || false,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
@@ -612,6 +633,10 @@ export const duplicateElements = (
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (clonedElement.frameId) {
|
||||||
|
clonedElement.frameId = maybeGetNewId(clonedElement.frameId);
|
||||||
|
}
|
||||||
|
|
||||||
clonedElements.push(clonedElement);
|
clonedElements.push(clonedElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
|
isFrameElement,
|
||||||
isFreeDrawElement,
|
isFreeDrawElement,
|
||||||
isImageElement,
|
isImageElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
@@ -160,12 +161,17 @@ const rotateSingleElement = (
|
|||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
let angle = (5 * Math.PI) / 2 + Math.atan2(pointerY - cy, pointerX - cx);
|
let angle: number;
|
||||||
if (shouldRotateWithDiscreteAngle) {
|
if (isFrameElement(element)) {
|
||||||
angle += SHIFT_LOCKING_ANGLE / 2;
|
angle = 0;
|
||||||
angle -= angle % SHIFT_LOCKING_ANGLE;
|
} 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);
|
||||||
}
|
}
|
||||||
angle = normalizeAngle(angle);
|
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
const boundTextElementId = getBoundTextElementId(element);
|
||||||
|
|
||||||
mutateElement(element, { angle });
|
mutateElement(element, { angle });
|
||||||
@@ -877,39 +883,49 @@ const rotateMultipleElements = (
|
|||||||
centerAngle += SHIFT_LOCKING_ANGLE / 2;
|
centerAngle += SHIFT_LOCKING_ANGLE / 2;
|
||||||
centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
|
centerAngle -= centerAngle % SHIFT_LOCKING_ANGLE;
|
||||||
}
|
}
|
||||||
elements.forEach((element) => {
|
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
elements
|
||||||
const cx = (x1 + x2) / 2;
|
.filter((element) => element.type !== "frame")
|
||||||
const cy = (y1 + y2) / 2;
|
.forEach((element) => {
|
||||||
const origAngle =
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
pointerDownState.originalElements.get(element.id)?.angle ?? element.angle;
|
const cx = (x1 + x2) / 2;
|
||||||
const [rotatedCX, rotatedCY] = rotate(
|
const cy = (y1 + y2) / 2;
|
||||||
cx,
|
const origAngle =
|
||||||
cy,
|
pointerDownState.originalElements.get(element.id)?.angle ??
|
||||||
centerX,
|
element.angle;
|
||||||
centerY,
|
const [rotatedCX, rotatedCY] = rotate(
|
||||||
centerAngle + origAngle - element.angle,
|
cx,
|
||||||
);
|
cy,
|
||||||
mutateElement(element, {
|
centerX,
|
||||||
x: element.x + (rotatedCX - cx),
|
centerY,
|
||||||
y: element.y + (rotatedCY - cy),
|
centerAngle + origAngle - element.angle,
|
||||||
angle: normalizeAngle(centerAngle + origAngle),
|
);
|
||||||
});
|
mutateElement(
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
element,
|
||||||
if (boundTextElementId) {
|
{
|
||||||
const textElement =
|
x: element.x + (rotatedCX - cx),
|
||||||
Scene.getScene(element)?.getElement<ExcalidrawTextElementWithContainer>(
|
y: element.y + (rotatedCY - cy),
|
||||||
boundTextElementId,
|
|
||||||
);
|
|
||||||
if (textElement && !isArrowElement(element)) {
|
|
||||||
mutateElement(textElement, {
|
|
||||||
x: textElement.x + (rotatedCX - cx),
|
|
||||||
y: textElement.y + (rotatedCY - cy),
|
|
||||||
angle: normalizeAngle(centerAngle + origAngle),
|
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();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getResizeOffsetXY = (
|
export const getResizeOffsetXY = (
|
||||||
|
|||||||
@@ -840,10 +840,12 @@ export const getTextBindableContainerAtPosition = (
|
|||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]);
|
||||||
if (
|
if (
|
||||||
isArrowElement(elements[index]) &&
|
isArrowElement(elements[index]) &&
|
||||||
isHittingElementNotConsideringBoundingBox(elements[index], appState, [
|
isHittingElementNotConsideringBoundingBox(
|
||||||
x,
|
elements[index],
|
||||||
y,
|
appState,
|
||||||
])
|
null,
|
||||||
|
[x, y],
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
hitElement = elements[index];
|
hitElement = elements[index];
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -26,6 +26,17 @@ ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
|||||||
const tab = " ";
|
const tab = " ";
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
|
const getTextEditor = () => {
|
||||||
|
return document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTextEditor = (editor: HTMLTextAreaElement, value: string) => {
|
||||||
|
fireEvent.change(editor, { target: { value } });
|
||||||
|
editor.dispatchEvent(new Event("input"));
|
||||||
|
};
|
||||||
|
|
||||||
describe("textWysiwyg", () => {
|
describe("textWysiwyg", () => {
|
||||||
describe("start text editing", () => {
|
describe("start text editing", () => {
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
@@ -190,9 +201,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.clickAt(text.x + 50, text.y + 50);
|
mouse.clickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
expect(editor).not.toBe(null);
|
expect(editor).not.toBe(null);
|
||||||
expect(h.state.editingElement?.id).toBe(text.id);
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
@@ -214,9 +223,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
expect(editor).not.toBe(null);
|
expect(editor).not.toBe(null);
|
||||||
expect(h.state.editingElement?.id).toBe(text.id);
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
@@ -243,9 +250,7 @@ describe("textWysiwyg", () => {
|
|||||||
textElement = UI.createElement("text");
|
textElement = UI.createElement("text");
|
||||||
|
|
||||||
mouse.clickOn(textElement);
|
mouse.clickOn(textElement);
|
||||||
textarea = document.querySelector(
|
textarea = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
)!;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -455,17 +460,11 @@ describe("textWysiwyg", () => {
|
|||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.clickAt(750, 300);
|
mouse.clickAt(750, 300);
|
||||||
|
|
||||||
textarea = document.querySelector(
|
textarea = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
updateTextEditor(
|
||||||
)!;
|
textarea,
|
||||||
fireEvent.change(textarea, {
|
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
||||||
target: {
|
);
|
||||||
value:
|
|
||||||
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
textarea.dispatchEvent(new Event("input"));
|
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
textarea.blur();
|
textarea.blur();
|
||||||
expect(textarea.style.width).toBe("792px");
|
expect(textarea.style.width).toBe("792px");
|
||||||
@@ -513,11 +512,9 @@ describe("textWysiwyg", () => {
|
|||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -543,11 +540,9 @@ describe("textWysiwyg", () => {
|
|||||||
]);
|
]);
|
||||||
expect(text.angle).toBe(rectangle.angle);
|
expect(text.angle).toBe(rectangle.angle);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -572,9 +567,7 @@ describe("textWysiwyg", () => {
|
|||||||
API.setSelectedElements([diamond]);
|
API.setSelectedElements([diamond]);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
const value = new Array(1000).fill("1").join("\n");
|
const value = new Array(1000).fill("1").join("\n");
|
||||||
@@ -587,9 +580,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(diamond.height).toBe(50020);
|
expect(diamond.height).toBe(50020);
|
||||||
|
|
||||||
// Clearing text to simulate height decrease
|
// Clearing text to simulate height decrease
|
||||||
expect(() =>
|
expect(() => updateTextEditor(editor, "")).not.toThrow();
|
||||||
fireEvent.input(editor, { target: { value: "" } }),
|
|
||||||
).not.toThrow();
|
|
||||||
|
|
||||||
expect(diamond.height).toBe(70);
|
expect(diamond.height).toBe(70);
|
||||||
});
|
});
|
||||||
@@ -611,9 +602,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(null);
|
expect(text.containerId).toBe(null);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
let editor = document.querySelector(
|
let editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
@@ -628,11 +617,9 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
|
||||||
mouse.down();
|
mouse.down();
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
@@ -652,13 +639,11 @@ describe("textWysiwyg", () => {
|
|||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
@@ -689,11 +674,8 @@ describe("textWysiwyg", () => {
|
|||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
updateTextEditor(editor, "Hello World!");
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -717,17 +699,9 @@ describe("textWysiwyg", () => {
|
|||||||
freedraw.y + freedraw.height / 2,
|
freedraw.y + freedraw.height / 2,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
updateTextEditor(editor, "Hello World!");
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
|
||||||
target: {
|
|
||||||
value: "Hello World!",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
|
|
||||||
expect(freedraw.boundElements).toBe(null);
|
expect(freedraw.boundElements).toBe(null);
|
||||||
expect(h.elements[1].type).toBe("text");
|
expect(h.elements[1].type).toBe("text");
|
||||||
@@ -759,11 +733,9 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(null);
|
expect(text.containerId).toBe(null);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -776,17 +748,12 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.clickAt(20, 30);
|
mouse.clickAt(20, 30);
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
updateTextEditor(
|
||||||
target: {
|
editor,
|
||||||
value: "Excalidraw is an opensource virtual collaborative whiteboard",
|
"Excalidraw is an opensource virtual collaborative whiteboard",
|
||||||
},
|
);
|
||||||
});
|
|
||||||
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
expect(h.elements.length).toBe(2);
|
expect(h.elements.length).toBe(2);
|
||||||
expect(h.elements[1].type).toBe("text");
|
expect(h.elements[1].type).toBe("text");
|
||||||
@@ -826,12 +793,10 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.down();
|
mouse.down();
|
||||||
|
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
let editor = document.querySelector(
|
let editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
|
expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
@@ -841,9 +806,7 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
);
|
);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
fireEvent.click(screen.getByTitle(/code/i));
|
fireEvent.click(screen.getByTitle(/code/i));
|
||||||
@@ -876,17 +839,9 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
Keyboard.keyDown(KEYS.ENTER);
|
Keyboard.keyDown(KEYS.ENTER);
|
||||||
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
let editor = document.querySelector(
|
let editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
updateTextEditor(editor, "Hello World!");
|
||||||
target: {
|
|
||||||
value: "Hello World!",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
|
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -905,17 +860,8 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
updateTextEditor(editor, "Hello");
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
|
||||||
target: {
|
|
||||||
value: "Hello",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
@@ -943,13 +889,11 @@ describe("textWysiwyg", () => {
|
|||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
@@ -982,11 +926,9 @@ describe("textWysiwyg", () => {
|
|||||||
// Bind first text
|
// Bind first text
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
@@ -1005,11 +947,9 @@ describe("textWysiwyg", () => {
|
|||||||
it("should respect text alignment when resizing", async () => {
|
it("should respect text alignment when resizing", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
let editor = document.querySelector(
|
let editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello" } });
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
// should center align horizontally and vertically by default
|
// should center align horizontally and vertically by default
|
||||||
@@ -1024,9 +964,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
|
|
||||||
@@ -1049,9 +987,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
|
|
||||||
@@ -1089,11 +1025,9 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1106,11 +1040,9 @@ describe("textWysiwyg", () => {
|
|||||||
it("should scale font size correctly when resizing using shift", async () => {
|
it("should scale font size correctly when resizing using shift", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello" } });
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
const textElement = h.elements[1] as ExcalidrawTextElement;
|
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||||
expect(rectangle.width).toBe(90);
|
expect(rectangle.width).toBe(90);
|
||||||
@@ -1128,11 +1060,9 @@ describe("textWysiwyg", () => {
|
|||||||
it("should bind text correctly when container duplicated with alt-drag", async () => {
|
it("should bind text correctly when container duplicated with alt-drag", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello" } });
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(h.elements.length).toBe(2);
|
expect(h.elements.length).toBe(2);
|
||||||
|
|
||||||
@@ -1162,11 +1092,9 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
it("undo should work", async () => {
|
it("undo should work", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello" } });
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: h.elements[1].id, type: "text" },
|
{ id: h.elements[1].id, type: "text" },
|
||||||
@@ -1201,12 +1129,10 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
it("should not allow bound text with only whitespaces", async () => {
|
it("should not allow bound text with only whitespaces", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: " " } });
|
updateTextEditor(editor, " ");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.boundElements).toStrictEqual([]);
|
expect(rectangle.boundElements).toStrictEqual([]);
|
||||||
expect(h.elements[1].isDeleted).toBe(true);
|
expect(h.elements[1].isDeleted).toBe(true);
|
||||||
@@ -1225,9 +1151,9 @@ describe("textWysiwyg", () => {
|
|||||||
type: "text",
|
type: "text",
|
||||||
text: "Online whiteboard collaboration made easy",
|
text: "Online whiteboard collaboration made easy",
|
||||||
});
|
});
|
||||||
|
|
||||||
h.elements = [container, text];
|
h.elements = [container, text];
|
||||||
API.setSelectedElements([container, text]);
|
API.setSelectedElements([container, text]);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
@@ -1258,11 +1184,9 @@ describe("textWysiwyg", () => {
|
|||||||
it("should reset the container height cache when resizing", async () => {
|
it("should reset the container height cache when resizing", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
let editor = document.querySelector(
|
let editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello" } });
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
@@ -1272,9 +1196,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1287,12 +1209,8 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
updateTextEditor(editor, "Hello World!");
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
@@ -1316,12 +1234,8 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
updateTextEditor(editor, "Hello World!");
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
||||||
@@ -1352,17 +1266,12 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
updateTextEditor(editor, "Hello");
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello" } });
|
|
||||||
editor.blur();
|
editor.blur();
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = document.querySelector(
|
editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
editor.select();
|
editor.select();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1473,17 +1382,12 @@ describe("textWysiwyg", () => {
|
|||||||
it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
|
it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.clickAt(20, 30);
|
mouse.clickAt(20, 30);
|
||||||
const editor = document.querySelector(
|
const editor = getTextEditor();
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
updateTextEditor(
|
||||||
target: {
|
editor,
|
||||||
value: "Excalidraw is an opensource virtual collaborative whiteboard",
|
"Excalidraw is an opensource virtual collaborative whiteboard",
|
||||||
},
|
);
|
||||||
});
|
|
||||||
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { getElementAbsoluteCoords } from "./bounds";
|
|||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
import { AppState, Zoom } from "../types";
|
import { AppState, Zoom } from "../types";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
import { isLinearElement } from "./typeChecks";
|
import { isFrameElement, isLinearElement } from "./typeChecks";
|
||||||
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
||||||
|
|
||||||
export type TransformHandleDirection =
|
export type TransformHandleDirection =
|
||||||
@@ -44,6 +44,14 @@ export const OMIT_SIDES_FOR_MULTIPLE_ELEMENTS = {
|
|||||||
w: true,
|
w: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const OMIT_SIDES_FOR_FRAME = {
|
||||||
|
e: true,
|
||||||
|
s: true,
|
||||||
|
n: true,
|
||||||
|
w: true,
|
||||||
|
rotation: true,
|
||||||
|
};
|
||||||
|
|
||||||
const OMIT_SIDES_FOR_TEXT_ELEMENT = {
|
const OMIT_SIDES_FOR_TEXT_ELEMENT = {
|
||||||
e: true,
|
e: true,
|
||||||
s: true,
|
s: true,
|
||||||
@@ -249,6 +257,10 @@ export const getTransformHandles = (
|
|||||||
}
|
}
|
||||||
} else if (isTextElement(element)) {
|
} else if (isTextElement(element)) {
|
||||||
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
|
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
|
||||||
|
} else if (isFrameElement(element)) {
|
||||||
|
omitSides = {
|
||||||
|
rotation: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const dashedLineMargin = isLinearElement(element)
|
const dashedLineMargin = isLinearElement(element)
|
||||||
? DEFAULT_SPACING + 8
|
? DEFAULT_SPACING + 8
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
|
ExcalidrawFrameElement,
|
||||||
RoundnessType,
|
RoundnessType,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -45,6 +46,12 @@ export const isTextElement = (
|
|||||||
return element != null && element.type === "text";
|
return element != null && element.type === "text";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isFrameElement = (
|
||||||
|
element: ExcalidrawElement | null,
|
||||||
|
): element is ExcalidrawFrameElement => {
|
||||||
|
return element != null && element.type === "frame";
|
||||||
|
};
|
||||||
|
|
||||||
export const isFreeDrawElement = (
|
export const isFreeDrawElement = (
|
||||||
element?: ExcalidrawElement | null,
|
element?: ExcalidrawElement | null,
|
||||||
): element is ExcalidrawFreeDrawElement => {
|
): element is ExcalidrawFreeDrawElement => {
|
||||||
|
|||||||
+10
-2
@@ -53,6 +53,7 @@ type _ExcalidrawElementBase = Readonly<{
|
|||||||
/** List of groups the element belongs to.
|
/** List of groups the element belongs to.
|
||||||
Ordered from deepest to shallowest. */
|
Ordered from deepest to shallowest. */
|
||||||
groupIds: readonly GroupId[];
|
groupIds: readonly GroupId[];
|
||||||
|
frameId: string | null;
|
||||||
/** other elements that are bound to this element */
|
/** other elements that are bound to this element */
|
||||||
boundElements:
|
boundElements:
|
||||||
| readonly Readonly<{
|
| readonly Readonly<{
|
||||||
@@ -98,6 +99,11 @@ export type InitializedExcalidrawImageElement = MarkNonNullable<
|
|||||||
"fileId"
|
"fileId"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type ExcalidrawFrameElement = _ExcalidrawElementBase & {
|
||||||
|
type: "frame";
|
||||||
|
name: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These are elements that don't have any additional properties.
|
* These are elements that don't have any additional properties.
|
||||||
*/
|
*/
|
||||||
@@ -117,7 +123,8 @@ export type ExcalidrawElement =
|
|||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
| ExcalidrawLinearElement
|
| ExcalidrawLinearElement
|
||||||
| ExcalidrawFreeDrawElement
|
| ExcalidrawFreeDrawElement
|
||||||
| ExcalidrawImageElement;
|
| ExcalidrawImageElement
|
||||||
|
| ExcalidrawFrameElement;
|
||||||
|
|
||||||
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
@@ -148,7 +155,8 @@ export type ExcalidrawBindableElement =
|
|||||||
| ExcalidrawDiamondElement
|
| ExcalidrawDiamondElement
|
||||||
| ExcalidrawEllipseElement
|
| ExcalidrawEllipseElement
|
||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
| ExcalidrawImageElement;
|
| ExcalidrawImageElement
|
||||||
|
| ExcalidrawFrameElement;
|
||||||
|
|
||||||
export type ExcalidrawTextContainer =
|
export type ExcalidrawTextContainer =
|
||||||
| ExcalidrawRectangleElement
|
| ExcalidrawRectangleElement
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
window.addEventListener("offline", this.onOfflineStatusToggle);
|
window.addEventListener("offline", this.onOfflineStatusToggle);
|
||||||
window.addEventListener(EVENT.UNLOAD, this.onUnload);
|
window.addEventListener(EVENT.UNLOAD, this.onUnload);
|
||||||
|
|
||||||
|
this.onOfflineStatusToggle();
|
||||||
|
|
||||||
const collabAPI: CollabAPI = {
|
const collabAPI: CollabAPI = {
|
||||||
isCollaborating: this.isCollaborating,
|
isCollaborating: this.isCollaborating,
|
||||||
onPointerUpdate: this.onPointerUpdate,
|
onPointerUpdate: this.onPointerUpdate,
|
||||||
@@ -168,7 +170,6 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
appJotaiStore.set(collabAPIAtom, collabAPI);
|
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||||
this.onOfflineStatusToggle();
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
process.env.NODE_ENV === ENV.TEST ||
|
process.env.NODE_ENV === ENV.TEST ||
|
||||||
@@ -380,6 +381,13 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
startCollaboration = async (
|
startCollaboration = async (
|
||||||
existingRoomLinkData: null | { roomId: string; roomKey: string },
|
existingRoomLinkData: null | { roomId: string; roomKey: string },
|
||||||
): Promise<ImportedDataState | null> => {
|
): Promise<ImportedDataState | null> => {
|
||||||
|
if (!this.state.username) {
|
||||||
|
import("@excalidraw/random-username").then(({ getRandomUsername }) => {
|
||||||
|
const username = getRandomUsername();
|
||||||
|
this.onUsernameChange(username);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.portal.socket) {
|
if (this.portal.socket) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { LanguageList } from "./LanguageList";
|
|||||||
export const AppMainMenu: React.FC<{
|
export const AppMainMenu: React.FC<{
|
||||||
setCollabDialogShown: (toggle: boolean) => any;
|
setCollabDialogShown: (toggle: boolean) => any;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
|
isCollabEnabled: boolean;
|
||||||
}> = React.memo((props) => {
|
}> = React.memo((props) => {
|
||||||
return (
|
return (
|
||||||
<MainMenu>
|
<MainMenu>
|
||||||
@@ -13,10 +14,12 @@ export const AppMainMenu: React.FC<{
|
|||||||
<MainMenu.DefaultItems.SaveToActiveFile />
|
<MainMenu.DefaultItems.SaveToActiveFile />
|
||||||
<MainMenu.DefaultItems.Export />
|
<MainMenu.DefaultItems.Export />
|
||||||
<MainMenu.DefaultItems.SaveAsImage />
|
<MainMenu.DefaultItems.SaveAsImage />
|
||||||
<MainMenu.DefaultItems.LiveCollaborationTrigger
|
{props.isCollabEnabled && (
|
||||||
isCollaborating={props.isCollaborating}
|
<MainMenu.DefaultItems.LiveCollaborationTrigger
|
||||||
onSelect={() => props.setCollabDialogShown(true)}
|
isCollaborating={props.isCollaborating}
|
||||||
/>
|
onSelect={() => props.setCollabDialogShown(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<MainMenu.DefaultItems.Help />
|
<MainMenu.DefaultItems.Help />
|
||||||
<MainMenu.DefaultItems.ClearCanvas />
|
<MainMenu.DefaultItems.ClearCanvas />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { isExcalidrawPlusSignedUser } from "../app_constants";
|
|||||||
|
|
||||||
export const AppWelcomeScreen: React.FC<{
|
export const AppWelcomeScreen: React.FC<{
|
||||||
setCollabDialogShown: (toggle: boolean) => any;
|
setCollabDialogShown: (toggle: boolean) => any;
|
||||||
|
isCollabEnabled: boolean;
|
||||||
}> = React.memo((props) => {
|
}> = React.memo((props) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
let headingContent;
|
let headingContent;
|
||||||
@@ -46,9 +47,11 @@ export const AppWelcomeScreen: React.FC<{
|
|||||||
<WelcomeScreen.Center.Menu>
|
<WelcomeScreen.Center.Menu>
|
||||||
<WelcomeScreen.Center.MenuItemLoadScene />
|
<WelcomeScreen.Center.MenuItemLoadScene />
|
||||||
<WelcomeScreen.Center.MenuItemHelp />
|
<WelcomeScreen.Center.MenuItemHelp />
|
||||||
<WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
|
{props.isCollabEnabled && (
|
||||||
onSelect={() => props.setCollabDialogShown(true)}
|
<WelcomeScreen.Center.MenuItemLiveCollaborationTrigger
|
||||||
/>
|
onSelect={() => props.setCollabDialogShown(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!isExcalidrawPlusSignedUser && (
|
{!isExcalidrawPlusSignedUser && (
|
||||||
<WelcomeScreen.Center.MenuItemLink
|
<WelcomeScreen.Center.MenuItemLink
|
||||||
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
preventUnload,
|
preventUnload,
|
||||||
ResolvablePromise,
|
ResolvablePromise,
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
|
isRunningInIframe,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import {
|
import {
|
||||||
FIREBASE_STORAGE_PREFIXES,
|
FIREBASE_STORAGE_PREFIXES,
|
||||||
@@ -98,7 +99,7 @@ languageDetector.init({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const initializeScene = async (opts: {
|
const initializeScene = async (opts: {
|
||||||
collabAPI: CollabAPI;
|
collabAPI: CollabAPI | null;
|
||||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||||
}): Promise<
|
}): Promise<
|
||||||
{ scene: ExcalidrawInitialDataState | null } & (
|
{ scene: ExcalidrawInitialDataState | null } & (
|
||||||
@@ -183,7 +184,7 @@ const initializeScene = async (opts: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (roomLinkData) {
|
if (roomLinkData && opts.collabAPI) {
|
||||||
const { excalidrawAPI } = opts;
|
const { excalidrawAPI } = opts;
|
||||||
|
|
||||||
const scene = await opts.collabAPI.startCollaboration(roomLinkData);
|
const scene = await opts.collabAPI.startCollaboration(roomLinkData);
|
||||||
@@ -237,6 +238,7 @@ export const appLangCodeAtom = atom(
|
|||||||
const ExcalidrawWrapper = () => {
|
const ExcalidrawWrapper = () => {
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
||||||
|
const isCollabDisabled = isRunningInIframe();
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -272,7 +274,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!collabAPI || !excalidrawAPI) {
|
if (!excalidrawAPI || (!isCollabDisabled && !collabAPI)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +285,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
if (!data.scene) {
|
if (!data.scene) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (collabAPI.isCollaborating()) {
|
if (collabAPI?.isCollaborating()) {
|
||||||
if (data.scene.elements) {
|
if (data.scene.elements) {
|
||||||
collabAPI
|
collabAPI
|
||||||
.fetchImageFilesFromFirebase({
|
.fetchImageFilesFromFirebase({
|
||||||
@@ -353,7 +355,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
const libraryUrlTokens = parseLibraryTokensFromUrl();
|
const libraryUrlTokens = parseLibraryTokensFromUrl();
|
||||||
if (!libraryUrlTokens) {
|
if (!libraryUrlTokens) {
|
||||||
if (
|
if (
|
||||||
collabAPI.isCollaborating() &&
|
collabAPI?.isCollaborating() &&
|
||||||
!isCollaborationLink(window.location.href)
|
!isCollaborationLink(window.location.href)
|
||||||
) {
|
) {
|
||||||
collabAPI.stopCollaboration(false);
|
collabAPI.stopCollaboration(false);
|
||||||
@@ -382,7 +384,10 @@ const ExcalidrawWrapper = () => {
|
|||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!document.hidden && !collabAPI.isCollaborating()) {
|
if (
|
||||||
|
!document.hidden &&
|
||||||
|
((collabAPI && !collabAPI.isCollaborating()) || isCollabDisabled)
|
||||||
|
) {
|
||||||
// don't sync if local state is newer or identical to browser state
|
// don't sync if local state is newer or identical to browser state
|
||||||
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
|
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
|
||||||
const localDataState = importFromLocalStorage();
|
const localDataState = importFromLocalStorage();
|
||||||
@@ -398,7 +403,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
excalidrawAPI.updateLibrary({
|
excalidrawAPI.updateLibrary({
|
||||||
libraryItems: getLibraryItemsFromStorage(),
|
libraryItems: getLibraryItemsFromStorage(),
|
||||||
});
|
});
|
||||||
collabAPI.setUsername(username || "");
|
collabAPI?.setUsername(username || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) {
|
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) {
|
||||||
@@ -466,7 +471,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
);
|
);
|
||||||
clearTimeout(titleTimeout);
|
clearTimeout(titleTimeout);
|
||||||
};
|
};
|
||||||
}, [collabAPI, excalidrawAPI, setLangCode]);
|
}, [isCollabDisabled, collabAPI, excalidrawAPI, setLangCode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unloadHandler = (event: BeforeUnloadEvent) => {
|
const unloadHandler = (event: BeforeUnloadEvent) => {
|
||||||
@@ -649,7 +654,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
renderTopRightUI={(isMobile) => {
|
renderTopRightUI={(isMobile) => {
|
||||||
if (isMobile) {
|
if (isMobile || !collabAPI || isCollabDisabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -663,15 +668,21 @@ const ExcalidrawWrapper = () => {
|
|||||||
<AppMainMenu
|
<AppMainMenu
|
||||||
setCollabDialogShown={setCollabDialogShown}
|
setCollabDialogShown={setCollabDialogShown}
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
|
isCollabEnabled={!isCollabDisabled}
|
||||||
|
/>
|
||||||
|
<AppWelcomeScreen
|
||||||
|
setCollabDialogShown={setCollabDialogShown}
|
||||||
|
isCollabEnabled={!isCollabDisabled}
|
||||||
/>
|
/>
|
||||||
<AppWelcomeScreen setCollabDialogShown={setCollabDialogShown} />
|
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
{isCollaborating && isOffline && (
|
{isCollaborating && isOffline && (
|
||||||
<div className="collab-offline-warning">
|
<div className="collab-offline-warning">
|
||||||
{t("alerts.collabOfflineWarning")}
|
{t("alerts.collabOfflineWarning")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
{excalidrawAPI && !isCollabDisabled && (
|
||||||
|
<Collab excalidrawAPI={excalidrawAPI} />
|
||||||
|
)}
|
||||||
</Excalidraw>
|
</Excalidraw>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<ErrorDialog onClose={() => setErrorMessage("")}>
|
<ErrorDialog onClose={() => setErrorMessage("")}>
|
||||||
|
|||||||
+705
@@ -0,0 +1,705 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
@@ -94,6 +94,31 @@ export const selectGroupsForSelectedElements = (
|
|||||||
return nextAppState;
|
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 = (
|
export const editGroupForSelectedElement = (
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
element: NonDeleted<ExcalidrawElement>,
|
element: NonDeleted<ExcalidrawElement>,
|
||||||
@@ -186,3 +211,18 @@ export const getMaximumGroups = (
|
|||||||
|
|
||||||
return Array.from(groups.values());
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const KEYS = {
|
|||||||
CHEVRON_RIGHT: ">",
|
CHEVRON_RIGHT: ">",
|
||||||
PERIOD: ".",
|
PERIOD: ".",
|
||||||
COMMA: ",",
|
COMMA: ",",
|
||||||
|
SUBTRACT: "-",
|
||||||
|
|
||||||
A: "a",
|
A: "a",
|
||||||
C: "c",
|
C: "c",
|
||||||
|
|||||||
@@ -0,0 +1,449 @@
|
|||||||
|
{
|
||||||
|
"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
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"paste": "Vložit",
|
"paste": "Vložit",
|
||||||
"pasteAsPlaintext": "",
|
"pasteAsPlaintext": "Vložit jako prostý text",
|
||||||
"pasteCharts": "Vložit grafy",
|
"pasteCharts": "Vložit grafy",
|
||||||
"selectAll": "Vybrat vše",
|
"selectAll": "Vybrat vše",
|
||||||
"multiSelect": "Přidat prvek do výběru",
|
"multiSelect": "Přidat prvek do výběru",
|
||||||
@@ -49,9 +49,9 @@
|
|||||||
"large": "Velké",
|
"large": "Velké",
|
||||||
"veryLarge": "Velmi velké",
|
"veryLarge": "Velmi velké",
|
||||||
"solid": "Plný",
|
"solid": "Plný",
|
||||||
"hachure": "",
|
"hachure": "Hachure",
|
||||||
"zigzag": "",
|
"zigzag": "Klikatě",
|
||||||
"crossHatch": "",
|
"crossHatch": "Křížový šrafování",
|
||||||
"thin": "Tenký",
|
"thin": "Tenký",
|
||||||
"bold": "Tlustý",
|
"bold": "Tlustý",
|
||||||
"left": "Vlevo",
|
"left": "Vlevo",
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"extraBold": "Extra tlustý",
|
"extraBold": "Extra tlustý",
|
||||||
"architect": "Architekt",
|
"architect": "Architekt",
|
||||||
"artist": "Umělec",
|
"artist": "Umělec",
|
||||||
"cartoonist": "",
|
"cartoonist": "Kartoonista",
|
||||||
"fileTitle": "Název souboru",
|
"fileTitle": "Název souboru",
|
||||||
"colorPicker": "Výběr barvy",
|
"colorPicker": "Výběr barvy",
|
||||||
"canvasColors": "Použito na plátně",
|
"canvasColors": "Použito na plátně",
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
"increaseFontSize": "Zvětšit písmo",
|
"increaseFontSize": "Zvětšit písmo",
|
||||||
"unbindText": "Zrušit vazbu textu",
|
"unbindText": "Zrušit vazbu textu",
|
||||||
"bindText": "Vázat text s kontejnerem",
|
"bindText": "Vázat text s kontejnerem",
|
||||||
"createContainerFromText": "",
|
"createContainerFromText": "Zabalit text do kontejneru",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Upravit odkaz",
|
"edit": "Upravit odkaz",
|
||||||
"create": "Vytvořit odkaz",
|
"create": "Vytvořit odkaz",
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Zveřejněno",
|
"statusPublished": "Zveřejněno",
|
||||||
"sidebarLock": "Ponechat postranní panel otevřený",
|
"sidebarLock": "Ponechat postranní panel otevřený",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "Vyberte barvu z plátna"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "Dosud neexistují žádné položky...",
|
"noItems": "Dosud neexistují žádné položky...",
|
||||||
@@ -177,38 +177,38 @@
|
|||||||
"decryptFailed": "Nelze dešifrovat data.",
|
"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.",
|
"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?",
|
"loadSceneOverridePrompt": "Načítání externího výkresu nahradí váš existující obsah. Přejete si pokračovat?",
|
||||||
"collabStopOverridePrompt": "",
|
"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)",
|
||||||
"errorAddingToLibrary": "Položku nelze přidat do knihovny",
|
"errorAddingToLibrary": "Položku nelze přidat do knihovny",
|
||||||
"errorRemovingFromLibrary": "Položku nelze odstranit z knihovny",
|
"errorRemovingFromLibrary": "Položku nelze odstranit z knihovny",
|
||||||
"confirmAddLibrary": "Tímto přidáte {{numShapes}} tvarů do tvé knihovny. Jste si jisti?",
|
"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?",
|
"imageDoesNotContainScene": "Zdá se, že tento obrázek neobsahuje žádná data o scéně. Zapnuli jste při exportu vkládání scény?",
|
||||||
"cannotRestoreFromImage": "",
|
"cannotRestoreFromImage": "Scénu nelze obnovit z tohoto souboru obrázku",
|
||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "Nelze importovat scénu z zadané URL. Je buď poškozená, nebo neobsahuje platná JSON data Excalidraw.",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "Tímto vymažete vaši knihovnu. Jste si jisti?",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "Smazat {{count}} položek z knihovny?",
|
||||||
"invalidEncryptionKey": "",
|
"invalidEncryptionKey": "Šifrovací klíč musí mít 22 znaků. Live spolupráce je zakázána.",
|
||||||
"collabOfflineWarning": ""
|
"collabOfflineWarning": "Není k dispozici žádné internetové připojení.\nVaše změny nebudou uloženy!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "",
|
"unsupportedFileType": "Nepodporovaný typ souboru.",
|
||||||
"imageInsertError": "",
|
"imageInsertError": "Nelze vložit obrázek. Zkuste to později...",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "Soubor je příliš velký. Maximální povolená velikost je {{maxSize}}.",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "Nelze vložit SVG obrázek. Značení SVG je neplatné.",
|
||||||
"invalidSVGString": "",
|
"invalidSVGString": "Neplatný SVG.",
|
||||||
"cannotResolveCollabServer": "",
|
"cannotResolveCollabServer": "Nelze se připojit ke sdílenému serveru. Prosím obnovte stránku a zkuste to znovu.",
|
||||||
"importLibraryError": "",
|
"importLibraryError": "Nelze načíst knihovnu",
|
||||||
"collabSaveFailed": "",
|
"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": "",
|
"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.",
|
||||||
"brave_measure_text_error": {
|
"brave_measure_text_error": {
|
||||||
"line1": "",
|
"line1": "Vypadá to, že používáte Brave prohlížeč s povoleným nastavením <bold>Aggressively Block Fingerprinting</bold>.",
|
||||||
"line2": "",
|
"line2": "To by mohlo vést k narušení <bold>Textových elementů</bold> ve vašich výkresech.",
|
||||||
"line3": "",
|
"line3": "Důrazně doporučujeme zakázat toto nastavení. Můžete sledovat <link>tyto kroky</link> jak to udělat.",
|
||||||
"line4": ""
|
"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>"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Výběr",
|
"selection": "Výběr",
|
||||||
"image": "",
|
"image": "Vložit obrázek",
|
||||||
"rectangle": "Obdélník",
|
"rectangle": "Obdélník",
|
||||||
"diamond": "Diamant",
|
"diamond": "Diamant",
|
||||||
"ellipse": "Elipsa",
|
"ellipse": "Elipsa",
|
||||||
@@ -216,69 +216,69 @@
|
|||||||
"line": "Čára",
|
"line": "Čára",
|
||||||
"freedraw": "Kreslení",
|
"freedraw": "Kreslení",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"library": "",
|
"library": "Knihovna",
|
||||||
"lock": "",
|
"lock": "Po kreslení ponechat vybraný nástroj aktivní",
|
||||||
"penMode": "",
|
"penMode": "Režim Pera - zabránit dotyku",
|
||||||
"link": "",
|
"link": "Přidat/aktualizovat odkaz pro vybraný tvar",
|
||||||
"eraser": "Guma",
|
"eraser": "Guma",
|
||||||
"hand": ""
|
"hand": "Ruka (nástroj pro posouvání)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "",
|
"canvasActions": "Akce plátna",
|
||||||
"selectedShapeActions": "",
|
"selectedShapeActions": "Akce vybraného tvaru",
|
||||||
"shapes": "Tvary"
|
"shapes": "Tvary"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "",
|
"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": "",
|
"linearElement": "Kliknutím pro více bodů, táhnutím pro jednu čáru",
|
||||||
"freeDraw": "",
|
"freeDraw": "Klikněte a táhněte, pro ukončení pusťte",
|
||||||
"text": "",
|
"text": "Tip: Text můžete také přidat dvojitým kliknutím kdekoli pomocí nástroje pro výběr",
|
||||||
"text_selected": "",
|
"text_selected": "Dvojklikem nebo stisknutím klávesy ENTER upravíte text",
|
||||||
"text_editing": "",
|
"text_editing": "Stiskněte Escape nebo Ctrl/Cmd+ENTER pro dokončení úprav",
|
||||||
"linearElementMulti": "",
|
"linearElementMulti": "Klikněte na poslední bod nebo stiskněte Escape anebo Enter pro dokončení",
|
||||||
"lockAngle": "",
|
"lockAngle": "Úhel můžete omezit podržením SHIFT",
|
||||||
"resize": "",
|
"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": "",
|
"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": "",
|
"rotate": "Úhly můžete omezit podržením SHIFT při otáčení",
|
||||||
"lineEditor_info": "",
|
"lineEditor_info": "Podržte Ctrl/Cmd a dvakrát klikněte nebo stiskněte Ctrl/Cmd + Enter pro úpravu bodů",
|
||||||
"lineEditor_pointSelected": "",
|
"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": "",
|
"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": "",
|
"placeImage": "Kliknutím umístěte obrázek, nebo klepnutím a přetažením ručně nastavíte jeho velikost",
|
||||||
"publishLibrary": "",
|
"publishLibrary": "Publikovat vlastní knihovnu",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "Stiskněte Enter pro přidání textu",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "Podržte Ctrl/Cmd pro hluboký výběr a pro zabránění táhnutí",
|
||||||
"eraserRevert": "",
|
"eraserRevert": "Podržením klávesy Alt vrátíte prvky označené pro smazání",
|
||||||
"firefox_clipboard_write": ""
|
"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\"."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "",
|
"cannotShowPreview": "Náhled nelze zobrazit",
|
||||||
"canvasTooBig": "",
|
"canvasTooBig": "Plátno je možná příliš velké.",
|
||||||
"canvasTooBigTip": ""
|
"canvasTooBigTip": "Tip: zkus posunout nejvzdálenější prvky trochu blíže k sobě."
|
||||||
},
|
},
|
||||||
"errorSplash": {
|
"errorSplash": {
|
||||||
"headingMain": "",
|
"headingMain": "Chyba. Zkuste <button>znovu načíst stránku</button>.",
|
||||||
"clearCanvasMessage": "",
|
"clearCanvasMessage": "Pokud opětovné načtení nefunguje, zkuste <button>vymazat plátno</button>.",
|
||||||
"clearCanvasCaveat": "",
|
"clearCanvasCaveat": " To povede ke ztrátě dat ",
|
||||||
"trackedToSentry": "Chyba identifikátoru {{eventId}} byl zaznamenán v našem systému.",
|
"trackedToSentry": "Chyba identifikátoru {{eventId}} byl zaznamenán v našem systému.",
|
||||||
"openIssueMessage": "",
|
"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": ""
|
"sceneContent": "Obsah scény:"
|
||||||
},
|
},
|
||||||
"roomDialog": {
|
"roomDialog": {
|
||||||
"desc_intro": "",
|
"desc_intro": "Můžete pozvat lidi na vaši aktuální scénu ke spolupráci s vámi.",
|
||||||
"desc_privacy": "",
|
"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": "",
|
"button_startSession": "Zahájit relaci",
|
||||||
"button_stopSession": "",
|
"button_stopSession": "Ukončit relaci",
|
||||||
"desc_inProgressIntro": "",
|
"desc_inProgressIntro": "Živá spolupráce právě probíhá.",
|
||||||
"desc_shareLink": "",
|
"desc_shareLink": "Sdílejte tento odkaz s každým, s kým chcete spolupracovat:",
|
||||||
"desc_exitSession": "",
|
"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": ""
|
"shareTitle": "Připojte se k aktivní spolupráci na Excalidraw"
|
||||||
},
|
},
|
||||||
"errorDialog": {
|
"errorDialog": {
|
||||||
"title": ""
|
"title": "Chyba"
|
||||||
},
|
},
|
||||||
"exportDialog": {
|
"exportDialog": {
|
||||||
"disk_title": "",
|
"disk_title": "Uložit na disk",
|
||||||
"disk_details": "",
|
"disk_details": "Exportovat data scény do souboru, ze kterého můžete importovat později.",
|
||||||
"disk_button": "Uložit do souboru",
|
"disk_button": "Uložit do souboru",
|
||||||
"link_title": "Odkaz pro sdílení",
|
"link_title": "Odkaz pro sdílení",
|
||||||
"link_details": "Exportovat jako odkaz pouze pro čtení.",
|
"link_details": "Exportovat jako odkaz pouze pro čtení.",
|
||||||
@@ -290,18 +290,18 @@
|
|||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "Přečtěte si náš blog",
|
"blog": "Přečtěte si náš blog",
|
||||||
"click": "kliknutí",
|
"click": "kliknutí",
|
||||||
"deepSelect": "",
|
"deepSelect": "Hluboký výběr",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "Hluboký výběr uvnitř boxu a zabránění táhnnutí",
|
||||||
"curvedArrow": "Zakřivená šipka",
|
"curvedArrow": "Zakřivená šipka",
|
||||||
"curvedLine": "Zakřivená čára",
|
"curvedLine": "Zakřivená čára",
|
||||||
"documentation": "Dokumentace",
|
"documentation": "Dokumentace",
|
||||||
"doubleClick": "dvojklik",
|
"doubleClick": "dvojklik",
|
||||||
"drag": "tažení",
|
"drag": "tažení",
|
||||||
"editor": "Editor",
|
"editor": "Editor",
|
||||||
"editLineArrowPoints": "",
|
"editLineArrowPoints": "Upravit body linií/šipek",
|
||||||
"editText": "",
|
"editText": "Upravit text / přidat popis",
|
||||||
"github": "",
|
"github": "Našel jsi problém? Nahlaš ho",
|
||||||
"howto": "",
|
"howto": "Sledujte naše návody",
|
||||||
"or": "nebo",
|
"or": "nebo",
|
||||||
"preventBinding": "Zabránit vázání šipky",
|
"preventBinding": "Zabránit vázání šipky",
|
||||||
"tools": "Nástroje",
|
"tools": "Nástroje",
|
||||||
@@ -313,8 +313,8 @@
|
|||||||
"zoomToFit": "Přiblížit na zobrazení všech prvků",
|
"zoomToFit": "Přiblížit na zobrazení všech prvků",
|
||||||
"zoomToSelection": "Přiblížit na výběr",
|
"zoomToSelection": "Přiblížit na výběr",
|
||||||
"toggleElementLock": "Zamknout/odemknout výběr",
|
"toggleElementLock": "Zamknout/odemknout výběr",
|
||||||
"movePageUpDown": "",
|
"movePageUpDown": "Posunout stránku nahoru/dolů",
|
||||||
"movePageLeftRight": ""
|
"movePageLeftRight": "Přesunout stránku doleva/doprava"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Vymazat plátno"
|
"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é.",
|
"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>",
|
"noteGuidelines": "Knihovna musí být nejdříve ručně schválena. Přečtěte si prosím <link>pokyny</link>",
|
||||||
"noteLicense": "",
|
"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": "",
|
"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": "",
|
"atleastOneLibItem": "Vyberte alespoň jednu položku knihovny, kterou chcete začít",
|
||||||
"republishWarning": ""
|
"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í."
|
||||||
},
|
},
|
||||||
"publishSuccessDialog": {
|
"publishSuccessDialog": {
|
||||||
"title": "Knihovna byla odeslána",
|
"title": "Knihovna byla odeslána",
|
||||||
"content": ""
|
"content": "Děkujeme vám {{authorName}}. Vaše knihovna byla odeslána k posouzení. Stav můžete sledovat <link>zde</link>"
|
||||||
},
|
},
|
||||||
"confirmDialog": {
|
"confirmDialog": {
|
||||||
"resetLibrary": "",
|
"resetLibrary": "Resetovat knihovnu",
|
||||||
"removeItemsFromLib": ""
|
"removeItemsFromLib": "Odstranit vybrané položky z knihovny"
|
||||||
},
|
},
|
||||||
"imageExportDialog": {
|
"imageExportDialog": {
|
||||||
"header": "",
|
"header": "Exportovat obrázek",
|
||||||
"label": {
|
"label": {
|
||||||
"withBackground": "",
|
"withBackground": "Pozadí",
|
||||||
"onlySelected": "",
|
"onlySelected": "Pouze vybrané",
|
||||||
"darkMode": "",
|
"darkMode": "Tmavý režim",
|
||||||
"embedScene": "",
|
"embedScene": "Vložit scénu",
|
||||||
"scale": "",
|
"scale": "Měřítko",
|
||||||
"padding": ""
|
"padding": "Odsazení"
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"embedScene": ""
|
"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."
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"exportToPng": "",
|
"exportToPng": "Exportovat do PNG",
|
||||||
"exportToSvg": "",
|
"exportToSvg": "Exportovat do SVG",
|
||||||
"copyPngToClipboard": ""
|
"copyPngToClipboard": "Kopírovat PNG do schránky"
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"exportToPng": "",
|
"exportToPng": "PNG",
|
||||||
"exportToSvg": "",
|
"exportToSvg": "SVG",
|
||||||
"copyPngToClipboard": ""
|
"copyPngToClipboard": "Kopírovat do schránky"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
"tooltip": "",
|
"tooltip": "Vaše kresby jsou end-to-end šifrované, takže servery Excalidraw je nikdy neuvidí.",
|
||||||
"link": ""
|
"link": "Blog příspěvek na end-to-end šifrování v Excalidraw"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"angle": "Úhel",
|
"angle": "Úhel",
|
||||||
@@ -402,48 +402,48 @@
|
|||||||
"addedToLibrary": "Přidáno do knihovny",
|
"addedToLibrary": "Přidáno do knihovny",
|
||||||
"copyStyles": "Styly byly zkopírovány.",
|
"copyStyles": "Styly byly zkopírovány.",
|
||||||
"copyToClipboard": "Zkopírováno do schránky.",
|
"copyToClipboard": "Zkopírováno do schránky.",
|
||||||
"copyToClipboardAsPng": "",
|
"copyToClipboardAsPng": "{{exportSelection}} zkopírován do schránky jako PNG\n({{exportColorScheme}})",
|
||||||
"fileSaved": "Soubor byl uložen.",
|
"fileSaved": "Soubor byl uložen.",
|
||||||
"fileSavedToFilename": "Uloženo do {filename}",
|
"fileSavedToFilename": "Uloženo do {filename}",
|
||||||
"canvas": "plátno",
|
"canvas": "plátno",
|
||||||
"selection": "výběr",
|
"selection": "výběr",
|
||||||
"pasteAsSingleElement": ""
|
"pasteAsSingleElement": "Pomocí {{shortcut}} vložte jako jeden prvek,\nnebo vložte do existujícího textového editoru"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"transparent": "Průhledná",
|
"transparent": "Průhledná",
|
||||||
"black": "",
|
"black": "Černá",
|
||||||
"white": "",
|
"white": "Bílá",
|
||||||
"red": "",
|
"red": "Červená",
|
||||||
"pink": "",
|
"pink": "Růžová",
|
||||||
"grape": "",
|
"grape": "Vínová",
|
||||||
"violet": "",
|
"violet": "Fialová",
|
||||||
"gray": "",
|
"gray": "Šedá",
|
||||||
"blue": "",
|
"blue": "Modrá",
|
||||||
"cyan": "",
|
"cyan": "Azurová",
|
||||||
"teal": "",
|
"teal": "Modrozelená",
|
||||||
"green": "",
|
"green": "Zelená",
|
||||||
"yellow": "",
|
"yellow": "Žlutá",
|
||||||
"orange": "",
|
"orange": "Oranžová",
|
||||||
"bronze": ""
|
"bronze": "Bronzová"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"app": {
|
"app": {
|
||||||
"center_heading": "",
|
"center_heading": "Všechna vaše data jsou uložena lokálně ve vašem prohlížeči.",
|
||||||
"center_heading_plus": "",
|
"center_heading_plus": "Chcete místo toho přejít na Excalidraw+?",
|
||||||
"menuHint": ""
|
"menuHint": "Export, nastavení, jazyky, ..."
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"menuHint": "",
|
"menuHint": "Export, nastavení a další...",
|
||||||
"center_heading": "",
|
"center_heading": "Diagramy. Vytvořeny. Jednoduše.",
|
||||||
"toolbarHint": "",
|
"toolbarHint": "Vyberte nástroj a začněte kreslit!",
|
||||||
"helpHint": ""
|
"helpHint": "Zkratky a pomoc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"colorPicker": {
|
"colorPicker": {
|
||||||
"mostUsedCustomColors": "",
|
"mostUsedCustomColors": "Nejpoužívanější vlastní barvy",
|
||||||
"colors": "",
|
"colors": "Barvy",
|
||||||
"shades": "",
|
"shades": "Stíny",
|
||||||
"hexCode": "",
|
"hexCode": "Hex kód",
|
||||||
"noShades": ""
|
"noShades": "Pro tuto barvu nejsou k dispozici žádné odstíny"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Veröffentlicht",
|
"statusPublished": "Veröffentlicht",
|
||||||
"sidebarLock": "Seitenleiste offen lassen",
|
"sidebarLock": "Seitenleiste offen lassen",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "Farbe von der Zeichenfläche auswählen"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "Noch keine Elemente hinzugefügt...",
|
"noItems": "Noch keine Elemente hinzugefügt...",
|
||||||
|
|||||||
+5
-1
@@ -124,6 +124,8 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Published",
|
"statusPublished": "Published",
|
||||||
"sidebarLock": "Keep sidebar open",
|
"sidebarLock": "Keep sidebar open",
|
||||||
|
"selectAllElementsInFrame": "Select all elements in frame",
|
||||||
|
"removeAllElementsFromFrame": "Remove all elements from frame",
|
||||||
"eyeDropper": "Pick color from canvas"
|
"eyeDropper": "Pick color from canvas"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
@@ -221,7 +223,9 @@
|
|||||||
"penMode": "Pen mode - prevent touch",
|
"penMode": "Pen mode - prevent touch",
|
||||||
"link": "Add/ Update link for a selected shape",
|
"link": "Add/ Update link for a selected shape",
|
||||||
"eraser": "Eraser",
|
"eraser": "Eraser",
|
||||||
"hand": "Hand (panning tool)"
|
"frame": "Frame tool",
|
||||||
|
"hand": "Hand (panning tool)",
|
||||||
|
"extraTools": "More tools"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Canvas actions",
|
"canvasActions": "Canvas actions",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "प्रकाशित",
|
"statusPublished": "प्रकाशित",
|
||||||
"sidebarLock": "साइडबार खुला रखे.",
|
"sidebarLock": "साइडबार खुला रखे.",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "चित्रफलक से रंग चुने"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "अभी तक कोई आइटम जोडा नहीं गया.",
|
"noItems": "अभी तक कोई आइटम जोडा नहीं गया.",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "게시됨",
|
"statusPublished": "게시됨",
|
||||||
"sidebarLock": "사이드바 유지",
|
"sidebarLock": "사이드바 유지",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "캔버스에서 색상 고르기"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "추가된 아이템 없음",
|
"noItems": "추가된 아이템 없음",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "प्रकाशित करा",
|
"statusPublished": "प्रकाशित करा",
|
||||||
"sidebarLock": "साइडबार उघडं ठेवा",
|
"sidebarLock": "साइडबार उघडं ठेवा",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "चित्रफलकातून रंग निवडा"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "अजून कोणतेही आइटम जोडलेले नाही...",
|
"noItems": "अजून कोणतेही आइटम जोडलेले नाही...",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Publisert",
|
"statusPublished": "Publisert",
|
||||||
"sidebarLock": "Holde sidemenyen åpen",
|
"sidebarLock": "Holde sidemenyen åpen",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "Velg farge fra lerretet"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "Ingen elementer lagt til ennå...",
|
"noItems": "Ingen elementer lagt til ennå...",
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"ar-SA": 80,
|
"ar-SA": 80,
|
||||||
|
"az-AZ": 20,
|
||||||
"bg-BG": 54,
|
"bg-BG": 54,
|
||||||
"bn-BD": 60,
|
"bn-BD": 60,
|
||||||
"ca-ES": 88,
|
"ca-ES": 88,
|
||||||
"cs-CZ": 64,
|
"cs-CZ": 100,
|
||||||
"da-DK": 33,
|
"da-DK": 33,
|
||||||
"de-DE": 99,
|
"de-DE": 100,
|
||||||
"el-GR": 93,
|
"el-GR": 93,
|
||||||
"en": 100,
|
"en": 100,
|
||||||
"es-ES": 89,
|
"es-ES": 89,
|
||||||
@@ -24,32 +25,32 @@
|
|||||||
"kab-KAB": 88,
|
"kab-KAB": 88,
|
||||||
"kk-KZ": 21,
|
"kk-KZ": 21,
|
||||||
"km-KH": 95,
|
"km-KH": 95,
|
||||||
"ko-KR": 99,
|
"ko-KR": 100,
|
||||||
"ku-TR": 90,
|
"ku-TR": 90,
|
||||||
"lt-LT": 56,
|
"lt-LT": 56,
|
||||||
"lv-LV": 89,
|
"lv-LV": 89,
|
||||||
"mr-IN": 95,
|
"mr-IN": 96,
|
||||||
"my-MM": 41,
|
"my-MM": 41,
|
||||||
"nb-NO": 99,
|
"nb-NO": 100,
|
||||||
"nl-NL": 83,
|
"nl-NL": 83,
|
||||||
"nn-NO": 77,
|
"nn-NO": 77,
|
||||||
"oc-FR": 87,
|
"oc-FR": 87,
|
||||||
"pa-IN": 90,
|
"pa-IN": 90,
|
||||||
"pl-PL": 99,
|
"pl-PL": 100,
|
||||||
"pt-BR": 99,
|
"pt-BR": 99,
|
||||||
"pt-PT": 95,
|
"pt-PT": 95,
|
||||||
"ro-RO": 99,
|
"ro-RO": 100,
|
||||||
"ru-RU": 99,
|
"ru-RU": 100,
|
||||||
"si-LK": 9,
|
"si-LK": 9,
|
||||||
"sk-SK": 99,
|
"sk-SK": 100,
|
||||||
"sl-SI": 99,
|
"sl-SI": 100,
|
||||||
"sv-SE": 99,
|
"sv-SE": 100,
|
||||||
"ta-IN": 83,
|
"ta-IN": 85,
|
||||||
"th-TH": 39,
|
"th-TH": 39,
|
||||||
"tr-TR": 87,
|
"tr-TR": 87,
|
||||||
"uk-UA": 97,
|
"uk-UA": 97,
|
||||||
"vi-VN": 56,
|
"vi-VN": 56,
|
||||||
"zh-CN": 95,
|
"zh-CN": 100,
|
||||||
"zh-HK": 26,
|
"zh-HK": 26,
|
||||||
"zh-TW": 99
|
"zh-TW": 100
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Opublikowano",
|
"statusPublished": "Opublikowano",
|
||||||
"sidebarLock": "Panel boczny zawsze otwarty",
|
"sidebarLock": "Panel boczny zawsze otwarty",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "Wybierz kolor z płótna"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "Nie dodano jeszcze żadnych elementów...",
|
"noItems": "Nie dodano jeszcze żadnych elementów...",
|
||||||
|
|||||||
@@ -363,7 +363,7 @@
|
|||||||
"darkMode": "",
|
"darkMode": "",
|
||||||
"embedScene": "Cena embutida",
|
"embedScene": "Cena embutida",
|
||||||
"scale": "",
|
"scale": "",
|
||||||
"padding": ""
|
"padding": "Espaçamento"
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"embedScene": ""
|
"embedScene": ""
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Publicat",
|
"statusPublished": "Publicat",
|
||||||
"sidebarLock": "Păstrează deschisă bara laterală",
|
"sidebarLock": "Păstrează deschisă bara laterală",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "Alegere culoare din pânză"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "Niciun element adăugat încă...",
|
"noItems": "Niciun element adăugat încă...",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Опубликовано",
|
"statusPublished": "Опубликовано",
|
||||||
"sidebarLock": "Держать боковую панель открытой",
|
"sidebarLock": "Держать боковую панель открытой",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "Взять образец цвета с холста"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "Пока ничего не добавлено...",
|
"noItems": "Пока ничего не добавлено...",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Zverejnené",
|
"statusPublished": "Zverejnené",
|
||||||
"sidebarLock": "Nechať bočný panel otvorený",
|
"sidebarLock": "Nechať bočný panel otvorený",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "Vybrať farbu z plátna"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "Zatiaľ neboli pridané žiadne položky...",
|
"noItems": "Zatiaľ neboli pridané žiadne položky...",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Objavljeno",
|
"statusPublished": "Objavljeno",
|
||||||
"sidebarLock": "Obdrži stransko vrstico odprto",
|
"sidebarLock": "Obdrži stransko vrstico odprto",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "Izberi barvo s platna"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "Dodan še ni noben element...",
|
"noItems": "Dodan še ni noben element...",
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Publicerad",
|
"statusPublished": "Publicerad",
|
||||||
"sidebarLock": "Håll sidofältet öppet",
|
"sidebarLock": "Håll sidofältet öppet",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "Välj färg från canvas"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "Inga objekt tillagda ännu...",
|
"noItems": "Inga objekt tillagda ännu...",
|
||||||
|
|||||||
+10
-10
@@ -50,7 +50,7 @@
|
|||||||
"veryLarge": "மிகப் பெரிய",
|
"veryLarge": "மிகப் பெரிய",
|
||||||
"solid": "திடமான",
|
"solid": "திடமான",
|
||||||
"hachure": "மலைக்குறிக்கோடு",
|
"hachure": "மலைக்குறிக்கோடு",
|
||||||
"zigzag": "",
|
"zigzag": "கோணல்மாணல்",
|
||||||
"crossHatch": "குறுக்குகதவு",
|
"crossHatch": "குறுக்குகதவு",
|
||||||
"thin": "மெல்லிய",
|
"thin": "மெல்லிய",
|
||||||
"bold": "பட்டை",
|
"bold": "பட்டை",
|
||||||
@@ -105,8 +105,8 @@
|
|||||||
"decreaseFontSize": "எழுத்துரு அளவைக் குறை",
|
"decreaseFontSize": "எழுத்துரு அளவைக் குறை",
|
||||||
"increaseFontSize": "எழுத்துரு அளவை அதிகரி",
|
"increaseFontSize": "எழுத்துரு அளவை அதிகரி",
|
||||||
"unbindText": "உரையைப் பிணைவவிழ்",
|
"unbindText": "உரையைப் பிணைவவிழ்",
|
||||||
"bindText": "",
|
"bindText": "உரையைக் கொள்கலனுக்குப் பிணை",
|
||||||
"createContainerFromText": "",
|
"createContainerFromText": "உரையைக் கொள்கலனுள் சுருட்டு",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "தொடுப்பைத் திருத்து",
|
"edit": "தொடுப்பைத் திருத்து",
|
||||||
"create": "தொடுப்பைப் படை",
|
"create": "தொடுப்பைப் படை",
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "தொடுப்பைத் திருத்து",
|
"edit": "தொடுப்பைத் திருத்து",
|
||||||
"exit": ""
|
"exit": "வரி திருத்தியிலிருந்து வெளியேறு"
|
||||||
},
|
},
|
||||||
"elementLock": {
|
"elementLock": {
|
||||||
"lock": "பூட்டு",
|
"lock": "பூட்டு",
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "வெளியிடப்பட்டது",
|
"statusPublished": "வெளியிடப்பட்டது",
|
||||||
"sidebarLock": "பக்கப்பட்டையைத் திறந்தே வை",
|
"sidebarLock": "பக்கப்பட்டையைத் திறந்தே வை",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "கித்தானிலிருந்து நிறம் தேர்ந்தெடு"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "இதுவரை உருப்படிகள் சேரக்கப்படவில்லை...",
|
"noItems": "இதுவரை உருப்படிகள் சேரக்கப்படவில்லை...",
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "கித்தானை அகரமாக்கு",
|
"clearReset": "கித்தானை அகரமாக்கு",
|
||||||
"exportJSON": "கோப்புக்கு ஏற்றுமதிசெய்",
|
"exportJSON": "கோப்புக்கு ஏற்றுமதிசெய்",
|
||||||
"exportImage": "",
|
"exportImage": "படத்தை ஏற்றுமதிசெய்...",
|
||||||
"export": "இதில் சேமி...",
|
"export": "இதில் சேமி...",
|
||||||
"copyToClipboard": "நகலகத்திற்கு நகலெடு",
|
"copyToClipboard": "நகலகத்திற்கு நகலெடு",
|
||||||
"save": "தற்போதைய கோப்புக்குச் சேமி",
|
"save": "தற்போதைய கோப்புக்குச் சேமி",
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
"resetLibrary": "இது உங்கள் நுலகத்தைத் துடைக்கும். நீங்கள் உறுதியா?",
|
"resetLibrary": "இது உங்கள் நுலகத்தைத் துடைக்கும். நீங்கள் உறுதியா?",
|
||||||
"removeItemsFromsLibrary": "{{count}} உருப்படி(கள்)-ஐ உம் நூலகத்திலிருந்து அழிக்கவா?",
|
"removeItemsFromsLibrary": "{{count}} உருப்படி(கள்)-ஐ உம் நூலகத்திலிருந்து அழிக்கவா?",
|
||||||
"invalidEncryptionKey": "மறையாக்க விசை 22 வரியுருக்கள் கொண்டிருக்கவேண்டும். நேரடி கூட்டுப்பணி முடக்கப்பட்டது.",
|
"invalidEncryptionKey": "மறையாக்க விசை 22 வரியுருக்கள் கொண்டிருக்கவேண்டும். நேரடி கூட்டுப்பணி முடக்கப்பட்டது.",
|
||||||
"collabOfflineWarning": ""
|
"collabOfflineWarning": "இணைய இணைப்பு இல்லை.\nஉமது மாற்றங்கள் சேமிக்கப்படா!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "ஆதரிக்கப்படா கோப்பு வகை.",
|
"unsupportedFileType": "ஆதரிக்கப்படா கோப்பு வகை.",
|
||||||
@@ -195,10 +195,10 @@
|
|||||||
"fileTooBig": "கோப்பு மிகப்பெரிது. அனுமதிக்கப்பட்ட அதிகபட்ச அளவு {{maxSize}}.",
|
"fileTooBig": "கோப்பு மிகப்பெரிது. அனுமதிக்கப்பட்ட அதிகபட்ச அளவு {{maxSize}}.",
|
||||||
"svgImageInsertError": "எஸ்விஜி படத்தைப் புகுத்தவியலா. எஸ்விஜியின் மார்க்அப் செல்லாததாக தெரிகிறது.",
|
"svgImageInsertError": "எஸ்விஜி படத்தைப் புகுத்தவியலா. எஸ்விஜியின் மார்க்அப் செல்லாததாக தெரிகிறது.",
|
||||||
"invalidSVGString": "செல்லாத SVG.",
|
"invalidSVGString": "செல்லாத SVG.",
|
||||||
"cannotResolveCollabServer": "",
|
"cannotResolveCollabServer": "கூட்டுப்பணிச் சேவையகத்துடன் இணைக்க முடியவில்லை. பக்கத்தை மீளேற்றி மீண்டும் முயலவும்.",
|
||||||
"importLibraryError": "நூலகத்தை ஏற்ற முடியவில்லை",
|
"importLibraryError": "நூலகத்தை ஏற்ற முடியவில்லை",
|
||||||
"collabSaveFailed": "",
|
"collabSaveFailed": "பின்முனை தரவுத்தளத்தில் சேமிக்க முடியவில்லை. சிக்கல்கள் நீடித்தால், உமது வேலைகளை இழக்காமலிருப்பதை உறுதிசெய்ய உமது கோப்பை உள்ளகத்தில் சேமிக்க வேண்டும்.",
|
||||||
"collabSaveFailed_sizeExceeded": "",
|
"collabSaveFailed_sizeExceeded": "பின்முனை தரவுத்தளத்தில் சேமிக்க முடியவில்லை, கித்தான் மிகப்பெரிதாகத் தெரிகிறது. உமது வேலைகளை இழக்காமலிருப்பதை உறுதிசெய்ய உமது கோப்பை உள்ளகத்தில் சேமிக்க வேண்டும்.",
|
||||||
"brave_measure_text_error": {
|
"brave_measure_text_error": {
|
||||||
"line1": "",
|
"line1": "",
|
||||||
"line2": "",
|
"line2": "",
|
||||||
|
|||||||
+20
-20
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "已发布",
|
"statusPublished": "已发布",
|
||||||
"sidebarLock": "侧边栏常驻",
|
"sidebarLock": "侧边栏常驻",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "从画布上取色"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "尚未添加任何项目……",
|
"noItems": "尚未添加任何项目……",
|
||||||
@@ -266,12 +266,12 @@
|
|||||||
"roomDialog": {
|
"roomDialog": {
|
||||||
"desc_intro": "你可以邀请其他人到目前的画面中与你协作。",
|
"desc_intro": "你可以邀请其他人到目前的画面中与你协作。",
|
||||||
"desc_privacy": "别担心,该会话使用端到端加密,无论绘制什么都将保持私密,甚至连我们的服务器也无法查看。",
|
"desc_privacy": "别担心,该会话使用端到端加密,无论绘制什么都将保持私密,甚至连我们的服务器也无法查看。",
|
||||||
"button_startSession": "启动会议",
|
"button_startSession": "开始会话",
|
||||||
"button_stopSession": "结束会议",
|
"button_stopSession": "结束会话",
|
||||||
"desc_inProgressIntro": "实时协作会议正在进行。",
|
"desc_inProgressIntro": "实时协作会话进行中。",
|
||||||
"desc_shareLink": "分享此链接给你要协作的用户",
|
"desc_shareLink": "分享此链接给你要协作的用户",
|
||||||
"desc_exitSession": "停止会话将中断您在与房间的连接,但您依然可以在本地继续使用画布. 请注意,这不会影响到其他用户, 他们仍可以在他们的版本上继续协作。",
|
"desc_exitSession": "停止会话将中断您与房间的连接,但您依然可以在本地继续使用画布。请注意,这不会影响到其他用户,他们仍可以在他们的版本上继续协作。",
|
||||||
"shareTitle": "加入 Excalidraw 实时协作会议"
|
"shareTitle": "加入 Excalidraw 实时协作会话"
|
||||||
},
|
},
|
||||||
"errorDialog": {
|
"errorDialog": {
|
||||||
"title": "错误"
|
"title": "错误"
|
||||||
@@ -356,27 +356,27 @@
|
|||||||
"removeItemsFromLib": "从素材库中删除选中的项目"
|
"removeItemsFromLib": "从素材库中删除选中的项目"
|
||||||
},
|
},
|
||||||
"imageExportDialog": {
|
"imageExportDialog": {
|
||||||
"header": "",
|
"header": "导出图片",
|
||||||
"label": {
|
"label": {
|
||||||
"withBackground": "",
|
"withBackground": "背景",
|
||||||
"onlySelected": "",
|
"onlySelected": "仅选中",
|
||||||
"darkMode": "",
|
"darkMode": "深色模式",
|
||||||
"embedScene": "",
|
"embedScene": "包含画布数据",
|
||||||
"scale": "",
|
"scale": "缩放比例",
|
||||||
"padding": ""
|
"padding": "内边距"
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"embedScene": ""
|
"embedScene": "画布数据将被保存到导出的 PNG/SVG 文件,以便恢复。\n将会增加导出文件的大小。"
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"exportToPng": "",
|
"exportToPng": "导出为 PNG",
|
||||||
"exportToSvg": "",
|
"exportToSvg": "导出为 SVG",
|
||||||
"copyPngToClipboard": ""
|
"copyPngToClipboard": "复制 PNG 到剪切板"
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"exportToPng": "",
|
"exportToPng": "PNG",
|
||||||
"exportToSvg": "",
|
"exportToSvg": "SVG",
|
||||||
"copyPngToClipboard": ""
|
"copyPngToClipboard": "复制到剪贴板"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "已發布",
|
"statusPublished": "已發布",
|
||||||
"sidebarLock": "側欄維持開啟",
|
"sidebarLock": "側欄維持開啟",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "從畫布中選取顏色"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "尚未加入任何物件...",
|
"noItems": "尚未加入任何物件...",
|
||||||
|
|||||||
+1
-1
@@ -206,7 +206,7 @@ export const isPointInPolygon = (
|
|||||||
|
|
||||||
// Returns whether `q` lies inside the segment/rectangle defined by `p` and `r`.
|
// 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.
|
// This is an approximation to "does `q` lie on a segment `pr`" check.
|
||||||
const isPointWithinBounds = (p: Point, q: Point, r: Point) => {
|
export const isPointWithinBounds = (p: Point, q: Point, r: Point) => {
|
||||||
return (
|
return (
|
||||||
q[0] <= Math.max(p[0], r[0]) &&
|
q[0] <= Math.max(p[0], r[0]) &&
|
||||||
q[0] >= Math.min(p[0], r[0]) &&
|
q[0] >= Math.min(p[0], r[0]) &&
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
|||||||
height: 141.9765625,
|
height: 141.9765625,
|
||||||
seed: 1968410350,
|
seed: 1968410350,
|
||||||
groupIds: [],
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
boundElements: null,
|
boundElements: null,
|
||||||
locked: false,
|
locked: false,
|
||||||
link: null,
|
link: null,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default {
|
|||||||
height: 141.9765625,
|
height: 141.9765625,
|
||||||
seed: 1968410350,
|
seed: 1968410350,
|
||||||
groupIds: [],
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "-xMIs_0jIFqvpx-R9UnaG",
|
id: "-xMIs_0jIFqvpx-R9UnaG",
|
||||||
@@ -37,6 +38,7 @@ export default {
|
|||||||
roughness: 1,
|
roughness: 1,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
groupIds: [],
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
seed: 957947807,
|
seed: 957947807,
|
||||||
version: 47,
|
version: 47,
|
||||||
versionNonce: 1128618623,
|
versionNonce: 1128618623,
|
||||||
@@ -58,6 +60,7 @@ export default {
|
|||||||
roughness: 1,
|
roughness: 1,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
groupIds: [],
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
seed: 707269846,
|
seed: 707269846,
|
||||||
version: 143,
|
version: 143,
|
||||||
@@ -94,6 +97,7 @@ export default {
|
|||||||
height: 103.65107323746608,
|
height: 103.65107323746608,
|
||||||
seed: 1445523839,
|
seed: 1445523839,
|
||||||
groupIds: [],
|
groupIds: [],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
@@ -133,6 +137,7 @@ export default {
|
|||||||
height: 113.8575037534261,
|
height: 113.8575037534261,
|
||||||
seed: 1513238033,
|
seed: 1513238033,
|
||||||
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
@@ -182,6 +187,7 @@ export default {
|
|||||||
height: 9.797916664247975,
|
height: 9.797916664247975,
|
||||||
seed: 683951089,
|
seed: 683951089,
|
||||||
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
@@ -220,6 +226,7 @@ export default {
|
|||||||
height: 9.797916664247975,
|
height: 9.797916664247975,
|
||||||
seed: 1817746897,
|
seed: 1817746897,
|
||||||
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
@@ -258,6 +265,7 @@ export default {
|
|||||||
height: 17.72670397681366,
|
height: 17.72670397681366,
|
||||||
seed: 1409727409,
|
seed: 1409727409,
|
||||||
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: ["bxuMGTzXLn7H-uBCptINx"],
|
boundElementIds: ["bxuMGTzXLn7H-uBCptINx"],
|
||||||
},
|
},
|
||||||
@@ -281,6 +289,7 @@ export default {
|
|||||||
height: 13.941904362416096,
|
height: 13.941904362416096,
|
||||||
seed: 1073094033,
|
seed: 1073094033,
|
||||||
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -304,6 +313,7 @@ export default {
|
|||||||
height: 13.941904362416096,
|
height: 13.941904362416096,
|
||||||
seed: 526271345,
|
seed: 526271345,
|
||||||
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -327,6 +337,7 @@ export default {
|
|||||||
height: 13.941904362416096,
|
height: 13.941904362416096,
|
||||||
seed: 243707217,
|
seed: 243707217,
|
||||||
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -352,6 +363,7 @@ export default {
|
|||||||
height: 36.77344700318558,
|
height: 36.77344700318558,
|
||||||
seed: 511870335,
|
seed: 511870335,
|
||||||
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
|
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -375,6 +387,7 @@ export default {
|
|||||||
height: 36.77344700318558,
|
height: 36.77344700318558,
|
||||||
seed: 1283079231,
|
seed: 1283079231,
|
||||||
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
|
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -398,6 +411,7 @@ export default {
|
|||||||
height: 36.77344700318558,
|
height: 36.77344700318558,
|
||||||
seed: 996251633,
|
seed: 996251633,
|
||||||
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
|
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -421,6 +435,7 @@ export default {
|
|||||||
height: 36.77344700318558,
|
height: 36.77344700318558,
|
||||||
seed: 1764842481,
|
seed: 1764842481,
|
||||||
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
|
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -446,6 +461,7 @@ export default {
|
|||||||
height: 154.56722543646003,
|
height: 154.56722543646003,
|
||||||
seed: 1424381745,
|
seed: 1424381745,
|
||||||
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
|
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
@@ -495,6 +511,7 @@ export default {
|
|||||||
height: 12.698053371678215,
|
height: 12.698053371678215,
|
||||||
seed: 726657713,
|
seed: 726657713,
|
||||||
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
|
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
@@ -533,6 +550,7 @@ export default {
|
|||||||
height: 10.178760037658167,
|
height: 10.178760037658167,
|
||||||
seed: 1977326481,
|
seed: 1977326481,
|
||||||
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
|
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
@@ -571,6 +589,7 @@ export default {
|
|||||||
height: 22.797152568995934,
|
height: 22.797152568995934,
|
||||||
seed: 1774660383,
|
seed: 1774660383,
|
||||||
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
|
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: ["bxuMGTzXLn7H-uBCptINx"],
|
boundElementIds: ["bxuMGTzXLn7H-uBCptINx"],
|
||||||
},
|
},
|
||||||
@@ -596,6 +615,7 @@ export default {
|
|||||||
height: 107.25081879410921,
|
height: 107.25081879410921,
|
||||||
seed: 371096063,
|
seed: 371096063,
|
||||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [
|
boundElementIds: [
|
||||||
"CFu0B4Mw_1wC1Hbgx8Fs0",
|
"CFu0B4Mw_1wC1Hbgx8Fs0",
|
||||||
@@ -623,6 +643,7 @@ export default {
|
|||||||
height: 107.25081879410921,
|
height: 107.25081879410921,
|
||||||
seed: 685932433,
|
seed: 685932433,
|
||||||
groupIds: ["0RJwA-yKP5dqk5oMiSeot", "9ppmKFUbA4iKjt8FaDFox"],
|
groupIds: ["0RJwA-yKP5dqk5oMiSeot", "9ppmKFUbA4iKjt8FaDFox"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [
|
boundElementIds: [
|
||||||
"CFu0B4Mw_1wC1Hbgx8Fs0",
|
"CFu0B4Mw_1wC1Hbgx8Fs0",
|
||||||
@@ -650,6 +671,7 @@ export default {
|
|||||||
height: 107.25081879410921,
|
height: 107.25081879410921,
|
||||||
seed: 58634943,
|
seed: 58634943,
|
||||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [
|
boundElementIds: [
|
||||||
"CFu0B4Mw_1wC1Hbgx8Fs0",
|
"CFu0B4Mw_1wC1Hbgx8Fs0",
|
||||||
@@ -677,6 +699,7 @@ export default {
|
|||||||
height: 3.249953844290203,
|
height: 3.249953844290203,
|
||||||
seed: 1673003743,
|
seed: 1673003743,
|
||||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
points: [
|
points: [
|
||||||
@@ -708,6 +731,7 @@ export default {
|
|||||||
height: 2.8032978840147194,
|
height: 2.8032978840147194,
|
||||||
seed: 1821527807,
|
seed: 1821527807,
|
||||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
points: [
|
points: [
|
||||||
@@ -739,6 +763,7 @@ export default {
|
|||||||
height: 4.280657518731036,
|
height: 4.280657518731036,
|
||||||
seed: 1485707039,
|
seed: 1485707039,
|
||||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
points: [
|
points: [
|
||||||
@@ -771,6 +796,7 @@ export default {
|
|||||||
height: 2.9096445412231735,
|
height: 2.9096445412231735,
|
||||||
seed: 1042012991,
|
seed: 1042012991,
|
||||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
points: [
|
points: [
|
||||||
@@ -804,6 +830,7 @@ export default {
|
|||||||
height: 2.4757501798128,
|
height: 2.4757501798128,
|
||||||
seed: 295443295,
|
seed: 295443295,
|
||||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
points: [
|
points: [
|
||||||
@@ -835,6 +862,7 @@ export default {
|
|||||||
height: 2.4757501798128,
|
height: 2.4757501798128,
|
||||||
seed: 1734301567,
|
seed: 1734301567,
|
||||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
points: [
|
points: [
|
||||||
@@ -869,6 +897,7 @@ export default {
|
|||||||
height: 76.53703389977764,
|
height: 76.53703389977764,
|
||||||
seed: 106569279,
|
seed: 106569279,
|
||||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -892,6 +921,7 @@ export default {
|
|||||||
height: 0,
|
height: 0,
|
||||||
seed: 73916127,
|
seed: 73916127,
|
||||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
@@ -924,6 +954,7 @@ export default {
|
|||||||
height: 5.001953125,
|
height: 5.001953125,
|
||||||
seed: 387857791,
|
seed: 387857791,
|
||||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -947,6 +978,7 @@ export default {
|
|||||||
height: 5.001953125,
|
height: 5.001953125,
|
||||||
seed: 1486370207,
|
seed: 1486370207,
|
||||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -970,6 +1002,7 @@ export default {
|
|||||||
height: 5.001953125,
|
height: 5.001953125,
|
||||||
seed: 610150847,
|
seed: 610150847,
|
||||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -993,6 +1026,7 @@ export default {
|
|||||||
height: 42.72020253937572,
|
height: 42.72020253937572,
|
||||||
seed: 144280593,
|
seed: 144280593,
|
||||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -1016,6 +1050,7 @@ export default {
|
|||||||
height: 24.44112284281997,
|
height: 24.44112284281997,
|
||||||
seed: 29167967,
|
seed: 29167967,
|
||||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
@@ -1068,6 +1103,7 @@ export default {
|
|||||||
height: 0,
|
height: 0,
|
||||||
seed: 1443027377,
|
seed: 1443027377,
|
||||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
@@ -1100,6 +1136,7 @@ export default {
|
|||||||
height: 5.711199931375845,
|
height: 5.711199931375845,
|
||||||
seed: 244310513,
|
seed: 244310513,
|
||||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
@@ -1138,6 +1175,7 @@ export default {
|
|||||||
height: 44.82230388130942,
|
height: 44.82230388130942,
|
||||||
seed: 683572113,
|
seed: 683572113,
|
||||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -1161,6 +1199,7 @@ export default {
|
|||||||
height: 5.896061363392446,
|
height: 5.896061363392446,
|
||||||
seed: 318798801,
|
seed: 318798801,
|
||||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "round",
|
strokeSharpness: "round",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
@@ -1200,6 +1239,7 @@ export default {
|
|||||||
height: 108.30428902193904,
|
height: 108.30428902193904,
|
||||||
seed: 1914896753,
|
seed: 1914896753,
|
||||||
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -1223,6 +1263,7 @@ export default {
|
|||||||
height: 82.83278895375764,
|
height: 82.83278895375764,
|
||||||
seed: 1306468145,
|
seed: 1306468145,
|
||||||
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -1246,6 +1287,7 @@ export default {
|
|||||||
height: 11.427824006438863,
|
height: 11.427824006438863,
|
||||||
seed: 93422161,
|
seed: 93422161,
|
||||||
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -1269,6 +1311,7 @@ export default {
|
|||||||
height: 19.889460471185775,
|
height: 19.889460471185775,
|
||||||
seed: 11646495,
|
seed: 11646495,
|
||||||
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
@@ -1292,6 +1335,7 @@ export default {
|
|||||||
height: 19.889460471185775,
|
height: 19.889460471185775,
|
||||||
seed: 291717649,
|
seed: 291717649,
|
||||||
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
||||||
|
frameId: null,
|
||||||
strokeSharpness: "sharp",
|
strokeSharpness: "sharp",
|
||||||
boundElementIds: [],
|
boundElementIds: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
||||||
exclude: /node_modules\/(?!browser-fs-access)/,
|
exclude:
|
||||||
|
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
||||||
use: [
|
use: [
|
||||||
{
|
{
|
||||||
loader: "ts-loader",
|
loader: "ts-loader",
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
test: /\.(ts|tsx|js|jsx|mjs)$/,
|
||||||
exclude: /node_modules\/(?!browser-fs-access)/,
|
exclude:
|
||||||
|
/node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/,
|
||||||
|
|
||||||
use: [
|
use: [
|
||||||
{
|
{
|
||||||
loader: "ts-loader",
|
loader: "ts-loader",
|
||||||
|
|||||||
+134
-29
@@ -34,6 +34,7 @@ import { AppState, BinaryFiles, Zoom } from "../types";
|
|||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import {
|
import {
|
||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
|
FRAME_STYLE,
|
||||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
SVG_NS,
|
SVG_NS,
|
||||||
@@ -48,6 +49,7 @@ import {
|
|||||||
getBoundTextMaxWidth,
|
getBoundTextMaxWidth,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import { getContainingFrame } from "../frame";
|
||||||
|
|
||||||
// using a stronger invert (100% vs our regular 93%) and saturate
|
// using a stronger invert (100% vs our regular 93%) and saturate
|
||||||
// as a temp hack to make images in dark theme look closer to original
|
// as a temp hack to make images in dark theme look closer to original
|
||||||
@@ -92,6 +94,7 @@ export interface ExcalidrawElementWithCanvas {
|
|||||||
canvasOffsetX: number;
|
canvasOffsetX: number;
|
||||||
canvasOffsetY: number;
|
canvasOffsetY: number;
|
||||||
boundTextElementVersion: number | null;
|
boundTextElementVersion: number | null;
|
||||||
|
containingFrameOpacity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cappedElementCanvasSize = (
|
const cappedElementCanvasSize = (
|
||||||
@@ -207,6 +210,7 @@ const generateElementCanvas = (
|
|||||||
canvasOffsetX,
|
canvasOffsetX,
|
||||||
canvasOffsetY,
|
canvasOffsetY,
|
||||||
boundTextElementVersion: getBoundTextElement(element)?.version || null,
|
boundTextElementVersion: getBoundTextElement(element)?.version || null,
|
||||||
|
containingFrameOpacity: getContainingFrame(element)?.opacity || 100,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -253,7 +257,8 @@ const drawElementOnCanvas = (
|
|||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: RenderConfig,
|
||||||
) => {
|
) => {
|
||||||
context.globalAlpha = element.opacity / 100;
|
context.globalAlpha =
|
||||||
|
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
@@ -469,7 +474,7 @@ const generateElementShape = (
|
|||||||
elementWithCanvasCache.delete(element);
|
elementWithCanvasCache.delete(element);
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle": {
|
||||||
if (element.roundness) {
|
if (element.roundness) {
|
||||||
const w = element.width;
|
const w = element.width;
|
||||||
const h = element.height;
|
const h = element.height;
|
||||||
@@ -494,6 +499,7 @@ const generateElementShape = (
|
|||||||
setShapeForElement(element, shape);
|
setShapeForElement(element, shape);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "diamond": {
|
case "diamond": {
|
||||||
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] =
|
||||||
getDiamondPoints(element);
|
getDiamondPoints(element);
|
||||||
@@ -717,12 +723,14 @@ const generateElementWithCanvas = (
|
|||||||
prevElementWithCanvas.zoomValue !== zoom.value &&
|
prevElementWithCanvas.zoomValue !== zoom.value &&
|
||||||
!renderConfig?.shouldCacheIgnoreZoom;
|
!renderConfig?.shouldCacheIgnoreZoom;
|
||||||
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
|
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
|
||||||
|
const containingFrameOpacity = getContainingFrame(element)?.opacity || 100;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!prevElementWithCanvas ||
|
!prevElementWithCanvas ||
|
||||||
shouldRegenerateBecauseZoom ||
|
shouldRegenerateBecauseZoom ||
|
||||||
prevElementWithCanvas.theme !== renderConfig.theme ||
|
prevElementWithCanvas.theme !== renderConfig.theme ||
|
||||||
prevElementWithCanvas.boundTextElementVersion !== boundTextElementVersion
|
prevElementWithCanvas.boundTextElementVersion !== boundTextElementVersion ||
|
||||||
|
prevElementWithCanvas.containingFrameOpacity !== containingFrameOpacity
|
||||||
) {
|
) {
|
||||||
const elementWithCanvas = generateElementCanvas(
|
const elementWithCanvas = generateElementCanvas(
|
||||||
element,
|
element,
|
||||||
@@ -897,25 +905,59 @@ export const renderElement = (
|
|||||||
const generator = rc.generator;
|
const generator = rc.generator;
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "selection": {
|
case "selection": {
|
||||||
context.save();
|
// do not render selection when exporting
|
||||||
context.translate(
|
if (!renderConfig.isExporting) {
|
||||||
element.x + renderConfig.scrollX,
|
context.save();
|
||||||
element.y + renderConfig.scrollY,
|
context.translate(
|
||||||
);
|
element.x + renderConfig.scrollX,
|
||||||
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
element.y + renderConfig.scrollY,
|
||||||
|
);
|
||||||
|
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
||||||
|
|
||||||
// render from 0.5px offset to get 1px wide line
|
// render from 0.5px offset to get 1px wide line
|
||||||
// https://stackoverflow.com/questions/7530593/html5-canvas-and-line-width/7531540#7531540
|
// 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
|
// TODO can be be improved by offseting to the negative when user selects
|
||||||
// from right to left
|
// from right to left
|
||||||
const offset = 0.5 / renderConfig.zoom.value;
|
const offset = 0.5 / renderConfig.zoom.value;
|
||||||
|
|
||||||
context.fillRect(offset, offset, element.width, element.height);
|
context.fillRect(offset, offset, element.width, element.height);
|
||||||
context.lineWidth = 1 / renderConfig.zoom.value;
|
context.lineWidth = 1 / renderConfig.zoom.value;
|
||||||
context.strokeStyle = "rgb(105, 101, 219)";
|
context.strokeStyle = " rgb(105, 101, 219)";
|
||||||
context.strokeRect(offset, offset, element.width, element.height);
|
context.strokeRect(offset, offset, element.width, element.height);
|
||||||
|
|
||||||
context.restore();
|
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();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "freedraw": {
|
case "freedraw": {
|
||||||
@@ -1107,6 +1149,23 @@ const roughSVGDrawWithPrecision = (
|
|||||||
return rsvg.draw(pshape);
|
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 = (
|
export const renderElementToSvg = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
rsvg: RoughSVG,
|
rsvg: RoughSVG,
|
||||||
@@ -1115,6 +1174,7 @@ export const renderElementToSvg = (
|
|||||||
offsetX: number,
|
offsetX: number,
|
||||||
offsetY: number,
|
offsetY: number,
|
||||||
exportWithDarkMode?: boolean,
|
exportWithDarkMode?: boolean,
|
||||||
|
exportingFrameId?: string | null,
|
||||||
) => {
|
) => {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
let cx = (x2 - x1) / 2 - (element.x - x1);
|
let cx = (x2 - x1) / 2 - (element.x - x1);
|
||||||
@@ -1148,6 +1208,9 @@ export const renderElementToSvg = (
|
|||||||
root = anchorTag;
|
root = anchorTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const opacity =
|
||||||
|
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "selection": {
|
case "selection": {
|
||||||
// Since this is used only during editing experience, which is canvas based,
|
// Since this is used only during editing experience, which is canvas based,
|
||||||
@@ -1163,7 +1226,6 @@ export const renderElementToSvg = (
|
|||||||
getShapeForElement(element)!,
|
getShapeForElement(element)!,
|
||||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||||
);
|
);
|
||||||
const opacity = element.opacity / 100;
|
|
||||||
if (opacity !== 1) {
|
if (opacity !== 1) {
|
||||||
node.setAttribute("stroke-opacity", `${opacity}`);
|
node.setAttribute("stroke-opacity", `${opacity}`);
|
||||||
node.setAttribute("fill-opacity", `${opacity}`);
|
node.setAttribute("fill-opacity", `${opacity}`);
|
||||||
@@ -1175,7 +1237,15 @@ export const renderElementToSvg = (
|
|||||||
offsetY || 0
|
offsetY || 0
|
||||||
}) rotate(${degree} ${cx} ${cy})`,
|
}) rotate(${degree} ${cx} ${cy})`,
|
||||||
);
|
);
|
||||||
root.appendChild(node);
|
|
||||||
|
const g = maybeWrapNodesInFrameClipPath(
|
||||||
|
element,
|
||||||
|
root,
|
||||||
|
[node],
|
||||||
|
exportingFrameId,
|
||||||
|
);
|
||||||
|
|
||||||
|
g ? root.appendChild(g) : root.appendChild(node);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "line":
|
case "line":
|
||||||
@@ -1228,7 +1298,6 @@ export const renderElementToSvg = (
|
|||||||
if (boundText) {
|
if (boundText) {
|
||||||
group.setAttribute("mask", `url(#mask-${element.id})`);
|
group.setAttribute("mask", `url(#mask-${element.id})`);
|
||||||
}
|
}
|
||||||
const opacity = element.opacity / 100;
|
|
||||||
group.setAttribute("stroke-linecap", "round");
|
group.setAttribute("stroke-linecap", "round");
|
||||||
|
|
||||||
getShapeForElement(element)!.forEach((shape) => {
|
getShapeForElement(element)!.forEach((shape) => {
|
||||||
@@ -1256,14 +1325,24 @@ export const renderElementToSvg = (
|
|||||||
}
|
}
|
||||||
group.appendChild(node);
|
group.appendChild(node);
|
||||||
});
|
});
|
||||||
root.appendChild(group);
|
|
||||||
root.append(maskPath);
|
const g = maybeWrapNodesInFrameClipPath(
|
||||||
|
element,
|
||||||
|
root,
|
||||||
|
[group, maskPath],
|
||||||
|
exportingFrameId,
|
||||||
|
);
|
||||||
|
if (g) {
|
||||||
|
root.appendChild(g);
|
||||||
|
} else {
|
||||||
|
root.appendChild(group);
|
||||||
|
root.append(maskPath);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "freedraw": {
|
case "freedraw": {
|
||||||
generateElementShape(element, generator);
|
generateElementShape(element, generator);
|
||||||
generateFreeDrawShape(element);
|
generateFreeDrawShape(element);
|
||||||
const opacity = element.opacity / 100;
|
|
||||||
const shape = getShapeForElement(element);
|
const shape = getShapeForElement(element);
|
||||||
const node = shape
|
const node = shape
|
||||||
? roughSVGDrawWithPrecision(rsvg, shape, MAX_DECIMALS_FOR_SVG_EXPORT)
|
? roughSVGDrawWithPrecision(rsvg, shape, MAX_DECIMALS_FOR_SVG_EXPORT)
|
||||||
@@ -1283,7 +1362,15 @@ export const renderElementToSvg = (
|
|||||||
path.setAttribute("fill", element.strokeColor);
|
path.setAttribute("fill", element.strokeColor);
|
||||||
path.setAttribute("d", getFreeDrawSvgPath(element));
|
path.setAttribute("d", getFreeDrawSvgPath(element));
|
||||||
node.appendChild(path);
|
node.appendChild(path);
|
||||||
root.appendChild(node);
|
|
||||||
|
const g = maybeWrapNodesInFrameClipPath(
|
||||||
|
element,
|
||||||
|
root,
|
||||||
|
[node],
|
||||||
|
exportingFrameId,
|
||||||
|
);
|
||||||
|
|
||||||
|
g ? root.appendChild(g) : root.appendChild(node);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "image": {
|
case "image": {
|
||||||
@@ -1319,6 +1406,7 @@ export const renderElementToSvg = (
|
|||||||
|
|
||||||
use.setAttribute("width", `${width}`);
|
use.setAttribute("width", `${width}`);
|
||||||
use.setAttribute("height", `${height}`);
|
use.setAttribute("height", `${height}`);
|
||||||
|
use.setAttribute("opacity", `${opacity}`);
|
||||||
|
|
||||||
// We first apply `scale` transforms (horizontal/vertical mirroring)
|
// We first apply `scale` transforms (horizontal/vertical mirroring)
|
||||||
// on the <use> element, then apply translation and rotation
|
// on the <use> element, then apply translation and rotation
|
||||||
@@ -1344,13 +1432,22 @@ export const renderElementToSvg = (
|
|||||||
}) rotate(${degree} ${cx} ${cy})`,
|
}) rotate(${degree} ${cx} ${cy})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
root.appendChild(g);
|
const clipG = maybeWrapNodesInFrameClipPath(
|
||||||
|
element,
|
||||||
|
root,
|
||||||
|
[g],
|
||||||
|
exportingFrameId,
|
||||||
|
);
|
||||||
|
clipG ? root.appendChild(clipG) : root.appendChild(g);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// frames are not rendered and only acts as a container
|
||||||
|
case "frame": {
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
const opacity = element.opacity / 100;
|
|
||||||
const node = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
|
const node = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
|
||||||
if (opacity !== 1) {
|
if (opacity !== 1) {
|
||||||
node.setAttribute("stroke-opacity", `${opacity}`);
|
node.setAttribute("stroke-opacity", `${opacity}`);
|
||||||
@@ -1395,7 +1492,15 @@ export const renderElementToSvg = (
|
|||||||
text.setAttribute("dominant-baseline", "text-before-edge");
|
text.setAttribute("dominant-baseline", "text-before-edge");
|
||||||
node.appendChild(text);
|
node.appendChild(text);
|
||||||
}
|
}
|
||||||
root.appendChild(node);
|
|
||||||
|
const g = maybeWrapNodesInFrameClipPath(
|
||||||
|
element,
|
||||||
|
root,
|
||||||
|
[node],
|
||||||
|
exportingFrameId,
|
||||||
|
);
|
||||||
|
|
||||||
|
g ? root.appendChild(g) : root.appendChild(node);
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
throw new Error(`Unimplemented type ${element.type}`);
|
throw new Error(`Unimplemented type ${element.type}`);
|
||||||
|
|||||||
+261
-69
@@ -10,6 +10,7 @@ import {
|
|||||||
NonDeleted,
|
NonDeleted,
|
||||||
GroupId,
|
GroupId,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
|
ExcalidrawFrameElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import {
|
import {
|
||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
@@ -30,12 +31,13 @@ import {
|
|||||||
import { getSelectedElements } from "../scene/selection";
|
import { getSelectedElements } from "../scene/selection";
|
||||||
|
|
||||||
import { renderElement, renderElementToSvg } from "./renderElement";
|
import { renderElement, renderElementToSvg } from "./renderElement";
|
||||||
import { getClientColors } from "../clients";
|
import { getClientColor } from "../clients";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import {
|
import {
|
||||||
isSelectedViaGroup,
|
isSelectedViaGroup,
|
||||||
getSelectedGroupIds,
|
getSelectedGroupIds,
|
||||||
getElementsInGroup,
|
getElementsInGroup,
|
||||||
|
selectGroupsFromGivenElements,
|
||||||
} from "../groups";
|
} from "../groups";
|
||||||
import { maxBindingGap } from "../element/collision";
|
import { maxBindingGap } from "../element/collision";
|
||||||
import {
|
import {
|
||||||
@@ -44,24 +46,30 @@ import {
|
|||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
} from "../element/binding";
|
} from "../element/binding";
|
||||||
import {
|
import {
|
||||||
|
OMIT_SIDES_FOR_FRAME,
|
||||||
shouldShowBoundingBox,
|
shouldShowBoundingBox,
|
||||||
TransformHandles,
|
TransformHandles,
|
||||||
TransformHandleType,
|
TransformHandleType,
|
||||||
} from "../element/transformHandles";
|
} from "../element/transformHandles";
|
||||||
import {
|
import {
|
||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
supportsEmoji,
|
|
||||||
throttleRAF,
|
throttleRAF,
|
||||||
|
isOnlyExportingSingleFrame,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { UserIdleState } from "../types";
|
import { UserIdleState } from "../types";
|
||||||
import { THEME_FILTER } from "../constants";
|
import { FRAME_STYLE, THEME_FILTER } from "../constants";
|
||||||
import {
|
import {
|
||||||
EXTERNAL_LINK_IMG,
|
EXTERNAL_LINK_IMG,
|
||||||
getLinkHandleFromCoords,
|
getLinkHandleFromCoords,
|
||||||
} from "../element/Hyperlink";
|
} from "../element/Hyperlink";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isFrameElement, isLinearElement } from "../element/typeChecks";
|
||||||
|
import {
|
||||||
|
elementOverlapsWithFrame,
|
||||||
|
getTargetFrame,
|
||||||
|
isElementInFrame,
|
||||||
|
} from "../frame";
|
||||||
|
import "canvas-roundrect-polyfill";
|
||||||
|
|
||||||
const hasEmojiSupport = supportsEmoji();
|
|
||||||
export const DEFAULT_SPACING = 2;
|
export const DEFAULT_SPACING = 2;
|
||||||
|
|
||||||
const strokeRectWithRotation = (
|
const strokeRectWithRotation = (
|
||||||
@@ -74,6 +82,8 @@ const strokeRectWithRotation = (
|
|||||||
cy: number,
|
cy: number,
|
||||||
angle: number,
|
angle: number,
|
||||||
fill: boolean = false,
|
fill: boolean = false,
|
||||||
|
/** should account for zoom */
|
||||||
|
radius: number = 0,
|
||||||
) => {
|
) => {
|
||||||
context.save();
|
context.save();
|
||||||
context.translate(cx, cy);
|
context.translate(cx, cy);
|
||||||
@@ -81,7 +91,14 @@ const strokeRectWithRotation = (
|
|||||||
if (fill) {
|
if (fill) {
|
||||||
context.fillRect(x - cx, y - cy, width, height);
|
context.fillRect(x - cx, y - cy, width, height);
|
||||||
}
|
}
|
||||||
context.strokeRect(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.restore();
|
context.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -159,7 +176,6 @@ const strokeGrid = (
|
|||||||
|
|
||||||
const renderSingleLinearPoint = (
|
const renderSingleLinearPoint = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
appState: AppState,
|
|
||||||
renderConfig: RenderConfig,
|
renderConfig: RenderConfig,
|
||||||
point: Point,
|
point: Point,
|
||||||
radius: number,
|
radius: number,
|
||||||
@@ -206,14 +222,7 @@ const renderLinearPointHandles = (
|
|||||||
const isSelected =
|
const isSelected =
|
||||||
!!appState.editingLinearElement?.selectedPointsIndices?.includes(idx);
|
!!appState.editingLinearElement?.selectedPointsIndices?.includes(idx);
|
||||||
|
|
||||||
renderSingleLinearPoint(
|
renderSingleLinearPoint(context, renderConfig, point, radius, isSelected);
|
||||||
context,
|
|
||||||
appState,
|
|
||||||
renderConfig,
|
|
||||||
point,
|
|
||||||
radius,
|
|
||||||
isSelected,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//Rendering segment mid points
|
//Rendering segment mid points
|
||||||
@@ -237,7 +246,6 @@ const renderLinearPointHandles = (
|
|||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
renderSingleLinearPoint(
|
renderSingleLinearPoint(
|
||||||
context,
|
context,
|
||||||
appState,
|
|
||||||
renderConfig,
|
renderConfig,
|
||||||
segmentMidPoint,
|
segmentMidPoint,
|
||||||
radius,
|
radius,
|
||||||
@@ -248,7 +256,6 @@ const renderLinearPointHandles = (
|
|||||||
highlightPoint(segmentMidPoint, context, renderConfig);
|
highlightPoint(segmentMidPoint, context, renderConfig);
|
||||||
renderSingleLinearPoint(
|
renderSingleLinearPoint(
|
||||||
context,
|
context,
|
||||||
appState,
|
|
||||||
renderConfig,
|
renderConfig,
|
||||||
segmentMidPoint,
|
segmentMidPoint,
|
||||||
radius,
|
radius,
|
||||||
@@ -258,7 +265,6 @@ const renderLinearPointHandles = (
|
|||||||
} else if (appState.editingLinearElement || points.length === 2) {
|
} else if (appState.editingLinearElement || points.length === 2) {
|
||||||
renderSingleLinearPoint(
|
renderSingleLinearPoint(
|
||||||
context,
|
context,
|
||||||
appState,
|
|
||||||
renderConfig,
|
renderConfig,
|
||||||
segmentMidPoint,
|
segmentMidPoint,
|
||||||
POINT_HANDLE_SIZE / 2,
|
POINT_HANDLE_SIZE / 2,
|
||||||
@@ -314,6 +320,34 @@ const renderLinearElementPointHighlight = (
|
|||||||
context.restore();
|
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 = ({
|
export const _renderScene = ({
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
@@ -405,11 +439,51 @@ 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 =
|
let editingLinearElement: NonDeleted<ExcalidrawLinearElement> | undefined =
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
visibleElements.forEach((element) => {
|
visibleElements.forEach((element) => {
|
||||||
try {
|
try {
|
||||||
renderElement(element, rc, context, renderConfig, appState);
|
// - 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);
|
||||||
|
}
|
||||||
// Getting the element using LinearElementEditor during collab mismatches version - being one head of visible elements due to
|
// 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
|
// ShapeCache returns empty hence making sure that we get the
|
||||||
// correct element from visible elements
|
// correct element from visible elements
|
||||||
@@ -458,7 +532,24 @@ export const _renderScene = ({
|
|||||||
renderBindingHighlight(context, renderConfig, suggestedBinding!);
|
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 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
|
// 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
|
// ShapeCache returns empty hence making sure that we get the
|
||||||
@@ -527,7 +618,7 @@ export const _renderScene = ({
|
|||||||
selectionColors.push(
|
selectionColors.push(
|
||||||
...renderConfig.remoteSelectedElementIds[element.id].map(
|
...renderConfig.remoteSelectedElementIds[element.id].map(
|
||||||
(socketId) => {
|
(socketId) => {
|
||||||
const { background } = getClientColors(socketId, appState);
|
const background = getClientColor(socketId);
|
||||||
return background;
|
return background;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -628,7 +719,9 @@ export const _renderScene = ({
|
|||||||
0,
|
0,
|
||||||
renderConfig.zoom,
|
renderConfig.zoom,
|
||||||
"mouse",
|
"mouse",
|
||||||
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
isFrameSelected
|
||||||
|
? OMIT_SIDES_FOR_FRAME
|
||||||
|
: OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
|
||||||
);
|
);
|
||||||
if (locallySelectedElements.some((element) => !element.locked)) {
|
if (locallySelectedElements.some((element) => !element.locked)) {
|
||||||
renderTransformHandles(context, renderConfig, transformHandles, 0);
|
renderTransformHandles(context, renderConfig, transformHandles, 0);
|
||||||
@@ -647,7 +740,7 @@ export const _renderScene = ({
|
|||||||
x -= appState.offsetLeft;
|
x -= appState.offsetLeft;
|
||||||
y -= appState.offsetTop;
|
y -= appState.offsetTop;
|
||||||
|
|
||||||
const width = 9;
|
const width = 11;
|
||||||
const height = 14;
|
const height = 14;
|
||||||
|
|
||||||
const isOutOfBounds =
|
const isOutOfBounds =
|
||||||
@@ -661,15 +754,20 @@ export const _renderScene = ({
|
|||||||
y = Math.max(y, 0);
|
y = Math.max(y, 0);
|
||||||
y = Math.min(y, normalizedCanvasHeight - height);
|
y = Math.min(y, normalizedCanvasHeight - height);
|
||||||
|
|
||||||
const { background, stroke } = getClientColors(clientId, appState);
|
const background = getClientColor(clientId);
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
context.strokeStyle = stroke;
|
context.strokeStyle = background;
|
||||||
context.fillStyle = background;
|
context.fillStyle = background;
|
||||||
|
|
||||||
const userState = renderConfig.remotePointerUserStates[clientId];
|
const userState = renderConfig.remotePointerUserStates[clientId];
|
||||||
if (isOutOfBounds || userState === UserIdleState.AWAY) {
|
const isInactive =
|
||||||
context.globalAlpha = 0.48;
|
isOutOfBounds ||
|
||||||
|
userState === UserIdleState.IDLE ||
|
||||||
|
userState === UserIdleState.AWAY;
|
||||||
|
|
||||||
|
if (isInactive) {
|
||||||
|
context.globalAlpha = 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -686,73 +784,86 @@ export const _renderScene = ({
|
|||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.arc(x, y, 15, 0, 2 * Math.PI, false);
|
context.arc(x, y, 15, 0, 2 * Math.PI, false);
|
||||||
context.lineWidth = 1;
|
context.lineWidth = 1;
|
||||||
context.strokeStyle = stroke;
|
context.strokeStyle = background;
|
||||||
context.stroke();
|
context.stroke();
|
||||||
context.closePath();
|
context.closePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Background (white outline) for arrow
|
||||||
|
context.fillStyle = oc.white;
|
||||||
|
context.strokeStyle = oc.white;
|
||||||
|
context.lineWidth = 6;
|
||||||
|
context.lineJoin = "round";
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.moveTo(x, y);
|
context.moveTo(x, y);
|
||||||
context.lineTo(x + 1, y + 14);
|
context.lineTo(x + 0, y + 14);
|
||||||
context.lineTo(x + 4, y + 9);
|
context.lineTo(x + 4, y + 9);
|
||||||
context.lineTo(x + 9, y + 10);
|
context.lineTo(x + 11, y + 8);
|
||||||
context.lineTo(x, y);
|
context.closePath();
|
||||||
context.fill();
|
|
||||||
context.stroke();
|
context.stroke();
|
||||||
|
context.fill();
|
||||||
|
|
||||||
const username = renderConfig.remotePointerUsernames[clientId];
|
// Arrow
|
||||||
|
context.fillStyle = background;
|
||||||
let idleState = "";
|
context.strokeStyle = background;
|
||||||
if (userState === UserIdleState.AWAY) {
|
context.lineWidth = 2;
|
||||||
idleState = hasEmojiSupport ? "⚫️" : ` (${UserIdleState.AWAY})`;
|
context.lineJoin = "round";
|
||||||
} else if (userState === UserIdleState.IDLE) {
|
context.beginPath();
|
||||||
idleState = hasEmojiSupport ? "💤" : ` (${UserIdleState.IDLE})`;
|
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 usernameAndIdleState = `${username || ""}${
|
const username = renderConfig.remotePointerUsernames[clientId] || "";
|
||||||
idleState ? ` ${idleState}` : ""
|
|
||||||
}`;
|
|
||||||
|
|
||||||
if (!isOutOfBounds && usernameAndIdleState) {
|
if (!isOutOfBounds && username) {
|
||||||
const offsetX = x + width;
|
context.font = "600 12px sans-serif"; // font has to be set before context.measureText()
|
||||||
const offsetY = y + height;
|
|
||||||
const paddingHorizontal = 4;
|
const offsetX = x + width / 2;
|
||||||
const paddingVertical = 4;
|
const offsetY = y + height + 2;
|
||||||
const measure = context.measureText(usernameAndIdleState);
|
const paddingHorizontal = 5;
|
||||||
|
const paddingVertical = 3;
|
||||||
|
const measure = context.measureText(username);
|
||||||
const measureHeight =
|
const measureHeight =
|
||||||
measure.actualBoundingBoxDescent + measure.actualBoundingBoxAscent;
|
measure.actualBoundingBoxDescent + measure.actualBoundingBoxAscent;
|
||||||
|
const finalHeight = Math.max(measureHeight, 12);
|
||||||
|
|
||||||
const boxX = offsetX - 1;
|
const boxX = offsetX - 1;
|
||||||
const boxY = offsetY - 1;
|
const boxY = offsetY - 1;
|
||||||
const boxWidth = measure.width + 2 * paddingHorizontal + 2;
|
const boxWidth = measure.width + 2 + paddingHorizontal * 2 + 2;
|
||||||
const boxHeight = measureHeight + 2 * paddingVertical + 2;
|
const boxHeight = finalHeight + 2 + paddingVertical * 2 + 2;
|
||||||
if (context.roundRect) {
|
if (context.roundRect) {
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.roundRect(
|
context.roundRect(boxX, boxY, boxWidth, boxHeight, 8);
|
||||||
boxX,
|
|
||||||
boxY,
|
|
||||||
boxWidth,
|
|
||||||
boxHeight,
|
|
||||||
4 / renderConfig.zoom.value,
|
|
||||||
);
|
|
||||||
context.fillStyle = background;
|
context.fillStyle = background;
|
||||||
context.fill();
|
context.fill();
|
||||||
context.fillStyle = stroke;
|
context.strokeStyle = oc.white;
|
||||||
context.stroke();
|
context.stroke();
|
||||||
} else {
|
} else {
|
||||||
// Border
|
roundRect(context, boxX, boxY, boxWidth, boxHeight, 8, oc.white);
|
||||||
context.fillStyle = stroke;
|
|
||||||
context.fillRect(boxX, boxY, boxWidth, boxHeight);
|
|
||||||
// Background
|
|
||||||
context.fillStyle = background;
|
|
||||||
context.fillRect(offsetX, offsetY, boxWidth - 2, boxHeight - 2);
|
|
||||||
}
|
}
|
||||||
context.fillStyle = oc.white;
|
context.fillStyle = oc.black;
|
||||||
|
|
||||||
context.fillText(
|
context.fillText(
|
||||||
usernameAndIdleState,
|
username,
|
||||||
offsetX + paddingHorizontal,
|
offsetX + paddingHorizontal + 1,
|
||||||
offsetY + paddingVertical + measure.actualBoundingBoxAscent,
|
offsetY +
|
||||||
|
paddingVertical +
|
||||||
|
measure.actualBoundingBoxAscent +
|
||||||
|
Math.floor((finalHeight - measureHeight) / 2) +
|
||||||
|
2,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -971,6 +1082,7 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "text":
|
case "text":
|
||||||
case "image":
|
case "image":
|
||||||
|
case "frame":
|
||||||
strokeRectWithRotation(
|
strokeRectWithRotation(
|
||||||
context,
|
context,
|
||||||
x1 - padding,
|
x1 - padding,
|
||||||
@@ -1008,6 +1120,82 @@ 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 = (
|
const renderBindingHighlightForSuggestedPointBinding = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
suggestedBinding: SuggestedPointBinding,
|
suggestedBinding: SuggestedPointBinding,
|
||||||
@@ -1089,7 +1277,7 @@ const renderLinkIcon = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isVisibleElement = (
|
export const isVisibleElement = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
canvasWidth: number,
|
canvasWidth: number,
|
||||||
canvasHeight: number,
|
canvasHeight: number,
|
||||||
@@ -1135,17 +1323,20 @@ export const renderSceneToSvg = (
|
|||||||
offsetX = 0,
|
offsetX = 0,
|
||||||
offsetY = 0,
|
offsetY = 0,
|
||||||
exportWithDarkMode = false,
|
exportWithDarkMode = false,
|
||||||
|
exportingFrameId = null,
|
||||||
}: {
|
}: {
|
||||||
offsetX?: number;
|
offsetX?: number;
|
||||||
offsetY?: number;
|
offsetY?: number;
|
||||||
exportWithDarkMode?: boolean;
|
exportWithDarkMode?: boolean;
|
||||||
|
exportingFrameId?: string | null;
|
||||||
} = {},
|
} = {},
|
||||||
) => {
|
) => {
|
||||||
if (!svgRoot) {
|
if (!svgRoot) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// render elements
|
// render elements
|
||||||
elements.forEach((element, index) => {
|
elements.forEach((element) => {
|
||||||
if (!element.isDeleted) {
|
if (!element.isDeleted) {
|
||||||
try {
|
try {
|
||||||
renderElementToSvg(
|
renderElementToSvg(
|
||||||
@@ -1156,6 +1347,7 @@ export const renderSceneToSvg = (
|
|||||||
element.x + offsetX,
|
element.x + offsetX,
|
||||||
element.y + offsetY,
|
element.y + offsetY,
|
||||||
exportWithDarkMode,
|
exportWithDarkMode,
|
||||||
|
exportingFrameId,
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const roundRect = (
|
|||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
radius: number,
|
radius: number,
|
||||||
|
strokeColor?: string,
|
||||||
) => {
|
) => {
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.moveTo(x + radius, y);
|
context.moveTo(x + radius, y);
|
||||||
@@ -33,5 +34,8 @@ export const roundRect = (
|
|||||||
context.quadraticCurveTo(x, y, x + radius, y);
|
context.quadraticCurveTo(x, y, x + radius, y);
|
||||||
context.closePath();
|
context.closePath();
|
||||||
context.fill();
|
context.fill();
|
||||||
|
if (strokeColor) {
|
||||||
|
context.strokeStyle = strokeColor;
|
||||||
|
}
|
||||||
context.stroke();
|
context.stroke();
|
||||||
};
|
};
|
||||||
|
|||||||
+51
-1
@@ -2,9 +2,15 @@ import {
|
|||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
|
ExcalidrawFrameElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getNonDeletedElements, isNonDeletedElement } from "../element";
|
import {
|
||||||
|
getNonDeletedElements,
|
||||||
|
getNonDeletedFrames,
|
||||||
|
isNonDeletedElement,
|
||||||
|
} from "../element";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
import { isFrameElement } from "../element/typeChecks";
|
||||||
|
|
||||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||||
@@ -12,6 +18,10 @@ type ElementKey = ExcalidrawElement | ElementIdKey;
|
|||||||
type SceneStateCallback = () => void;
|
type SceneStateCallback = () => void;
|
||||||
type SceneStateCallbackRemover = () => 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 => {
|
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
|
||||||
if (typeof elementKey === "string") {
|
if (typeof elementKey === "string") {
|
||||||
return true;
|
return true;
|
||||||
@@ -55,6 +65,8 @@ class Scene {
|
|||||||
|
|
||||||
private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
|
private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
|
||||||
private elements: readonly ExcalidrawElement[] = [];
|
private elements: readonly ExcalidrawElement[] = [];
|
||||||
|
private nonDeletedFrames: readonly NonDeleted<ExcalidrawFrameElement>[] = [];
|
||||||
|
private frames: readonly ExcalidrawFrameElement[] = [];
|
||||||
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
|
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
|
||||||
|
|
||||||
getElementsIncludingDeleted() {
|
getElementsIncludingDeleted() {
|
||||||
@@ -65,6 +77,14 @@ class Scene {
|
|||||||
return this.nonDeletedElements;
|
return this.nonDeletedElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFramesIncludingDeleted() {
|
||||||
|
return this.frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNonDeletedFrames(): readonly NonDeleted<ExcalidrawFrameElement>[] {
|
||||||
|
return this.nonDeletedFrames;
|
||||||
|
}
|
||||||
|
|
||||||
getElement<T extends ExcalidrawElement>(id: T["id"]): T | null {
|
getElement<T extends ExcalidrawElement>(id: T["id"]): T | null {
|
||||||
return (this.elementsMap.get(id) as T | undefined) || null;
|
return (this.elementsMap.get(id) as T | undefined) || null;
|
||||||
}
|
}
|
||||||
@@ -110,12 +130,19 @@ class Scene {
|
|||||||
|
|
||||||
replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
|
replaceAllElements(nextElements: readonly ExcalidrawElement[]) {
|
||||||
this.elements = nextElements;
|
this.elements = nextElements;
|
||||||
|
const nextFrames: ExcalidrawFrameElement[] = [];
|
||||||
this.elementsMap.clear();
|
this.elementsMap.clear();
|
||||||
nextElements.forEach((element) => {
|
nextElements.forEach((element) => {
|
||||||
|
if (isFrameElement(element)) {
|
||||||
|
nextFrames.push(element);
|
||||||
|
}
|
||||||
this.elementsMap.set(element.id, element);
|
this.elementsMap.set(element.id, element);
|
||||||
Scene.mapElementToScene(element, this);
|
Scene.mapElementToScene(element, this);
|
||||||
});
|
});
|
||||||
this.nonDeletedElements = getNonDeletedElements(this.elements);
|
this.nonDeletedElements = getNonDeletedElements(this.elements);
|
||||||
|
this.frames = nextFrames;
|
||||||
|
this.nonDeletedFrames = getNonDeletedFrames(this.frames);
|
||||||
|
|
||||||
this.informMutation();
|
this.informMutation();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +192,29 @@ class Scene {
|
|||||||
this.replaceAllElements(nextElements);
|
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) {
|
getElementIndex(elementId: string) {
|
||||||
return this.elements.findIndex((element) => element.id === elementId);
|
return this.elements.findIndex((element) => element.id === elementId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export const hasBackground = (type: string) =>
|
|||||||
type === "line" ||
|
type === "line" ||
|
||||||
type === "freedraw";
|
type === "freedraw";
|
||||||
|
|
||||||
export const hasStrokeColor = (type: string) => type !== "image";
|
export const hasStrokeColor = (type: string) =>
|
||||||
|
type !== "image" && type !== "frame";
|
||||||
|
|
||||||
export const hasStrokeWidth = (type: string) =>
|
export const hasStrokeWidth = (type: string) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
|
|||||||
+75
-8
@@ -1,8 +1,8 @@
|
|||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { getCommonBounds } from "../element/bounds";
|
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
|
||||||
import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
|
import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
|
||||||
import { distance } from "../utils";
|
import { distance, isOnlyExportingSingleFrame } from "../utils";
|
||||||
import { AppState, BinaryFiles } from "../types";
|
import { AppState, BinaryFiles } from "../types";
|
||||||
import { DEFAULT_EXPORT_PADDING, SVG_NS, THEME_FILTER } from "../constants";
|
import { DEFAULT_EXPORT_PADDING, SVG_NS, THEME_FILTER } from "../constants";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
getInitializedImageElements,
|
getInitializedImageElements,
|
||||||
updateImageCache,
|
updateImageCache,
|
||||||
} from "../element/image";
|
} from "../element/image";
|
||||||
|
import Scene from "./Scene";
|
||||||
|
|
||||||
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
export const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||||
|
|
||||||
@@ -51,6 +52,8 @@ export const exportToCanvas = async (
|
|||||||
files,
|
files,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
|
||||||
|
|
||||||
renderScene({
|
renderScene({
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
@@ -59,8 +62,8 @@ export const exportToCanvas = async (
|
|||||||
canvas,
|
canvas,
|
||||||
renderConfig: {
|
renderConfig: {
|
||||||
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
||||||
scrollX: -minX + exportPadding,
|
scrollX: -minX + (onlyExportingSingleFrame ? 0 : exportPadding),
|
||||||
scrollY: -minY + exportPadding,
|
scrollY: -minY + (onlyExportingSingleFrame ? 0 : exportPadding),
|
||||||
zoom: defaultAppState.zoom,
|
zoom: defaultAppState.zoom,
|
||||||
remotePointerViewportCoords: {},
|
remotePointerViewportCoords: {},
|
||||||
remoteSelectedElementIds: {},
|
remoteSelectedElementIds: {},
|
||||||
@@ -88,6 +91,7 @@ export const exportToSvg = async (
|
|||||||
viewBackgroundColor: string;
|
viewBackgroundColor: string;
|
||||||
exportWithDarkMode?: boolean;
|
exportWithDarkMode?: boolean;
|
||||||
exportEmbedScene?: boolean;
|
exportEmbedScene?: boolean;
|
||||||
|
renderFrame?: boolean;
|
||||||
},
|
},
|
||||||
files: BinaryFiles | null,
|
files: BinaryFiles | null,
|
||||||
opts?: {
|
opts?: {
|
||||||
@@ -140,6 +144,39 @@ export const exportToSvg = async (
|
|||||||
}
|
}
|
||||||
assetPath = `${assetPath}/dist/excalidraw-assets/`;
|
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 = `
|
svgRoot.innerHTML = `
|
||||||
${SVG_EXPORT_TAG}
|
${SVG_EXPORT_TAG}
|
||||||
${metadata}
|
${metadata}
|
||||||
@@ -154,8 +191,10 @@ export const exportToSvg = async (
|
|||||||
src: url("${assetPath}Cascadia.woff2");
|
src: url("${assetPath}Cascadia.woff2");
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
${exportingFrameClipPath}
|
||||||
</defs>
|
</defs>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// render background rect
|
// render background rect
|
||||||
if (appState.exportBackground && viewBackgroundColor) {
|
if (appState.exportBackground && viewBackgroundColor) {
|
||||||
const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
|
const rect = svgRoot.ownerDocument!.createElementNS(SVG_NS, "rect");
|
||||||
@@ -169,9 +208,10 @@ export const exportToSvg = async (
|
|||||||
|
|
||||||
const rsvg = rough.svg(svgRoot);
|
const rsvg = rough.svg(svgRoot);
|
||||||
renderSceneToSvg(elements, rsvg, svgRoot, files || {}, {
|
renderSceneToSvg(elements, rsvg, svgRoot, files || {}, {
|
||||||
offsetX: -minX + exportPadding,
|
offsetX,
|
||||||
offsetY: -minY + exportPadding,
|
offsetY,
|
||||||
exportWithDarkMode: appState.exportWithDarkMode,
|
exportWithDarkMode: appState.exportWithDarkMode,
|
||||||
|
exportingFrameId: exportingFrame?.id || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return svgRoot;
|
return svgRoot;
|
||||||
@@ -182,9 +222,36 @@ const getCanvasSize = (
|
|||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
exportPadding: number,
|
exportPadding: number,
|
||||||
): [number, number, number, 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 [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
const width = distance(minX, maxX) + exportPadding * 2;
|
const width =
|
||||||
const height = distance(minY, maxY) + exportPadding + exportPadding;
|
distance(minX, maxX) + (onlyExportingSingleFrame ? 0 : exportPadding * 2);
|
||||||
|
const height =
|
||||||
|
distance(minY, maxY) + (onlyExportingSingleFrame ? 0 : exportPadding * 2);
|
||||||
|
|
||||||
return [minX, minY, width, height];
|
return [minX, minY, width, height];
|
||||||
};
|
};
|
||||||
|
|||||||
+89
-7
@@ -5,17 +5,61 @@ import {
|
|||||||
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
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 = (
|
export const getElementsWithinSelection = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
selection: NonDeletedExcalidrawElement,
|
selection: NonDeletedExcalidrawElement,
|
||||||
|
excludeElementsInFrames: boolean = true,
|
||||||
) => {
|
) => {
|
||||||
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
||||||
getElementAbsoluteCoords(selection);
|
getElementAbsoluteCoords(selection);
|
||||||
return elements.filter((element) => {
|
|
||||||
const [elementX1, elementY1, elementX2, elementY2] =
|
let elementsInSelection = elements.filter((element) => {
|
||||||
|
let [elementX1, elementY1, elementX2, elementY2] =
|
||||||
getElementBounds(element);
|
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 (
|
return (
|
||||||
element.locked === false &&
|
element.locked === false &&
|
||||||
element.type !== "selection" &&
|
element.type !== "selection" &&
|
||||||
@@ -26,6 +70,22 @@ export const getElementsWithinSelection = (
|
|||||||
selectionY2 >= elementY2
|
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 = (
|
export const isSomeElementSelected = (
|
||||||
@@ -56,14 +116,17 @@ export const getCommonAttributeOfSelectedElements = <T>(
|
|||||||
export const getSelectedElements = (
|
export const getSelectedElements = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: Pick<AppState, "selectedElementIds">,
|
appState: Pick<AppState, "selectedElementIds">,
|
||||||
includeBoundTextElement: boolean = false,
|
opts?: {
|
||||||
) =>
|
includeBoundTextElement?: boolean;
|
||||||
elements.filter((element) => {
|
includeElementsInFrames?: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const selectedElements = elements.filter((element) => {
|
||||||
if (appState.selectedElementIds[element.id]) {
|
if (appState.selectedElementIds[element.id]) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
includeBoundTextElement &&
|
opts?.includeBoundTextElement &&
|
||||||
isBoundToContainer(element) &&
|
isBoundToContainer(element) &&
|
||||||
appState.selectedElementIds[element?.containerId]
|
appState.selectedElementIds[element?.containerId]
|
||||||
) {
|
) {
|
||||||
@@ -72,10 +135,29 @@ export const getSelectedElements = (
|
|||||||
return null;
|
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 = (
|
export const getTargetElements = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: Pick<AppState, "selectedElementIds" | "editingElement">,
|
appState: Pick<AppState, "selectedElementIds" | "editingElement">,
|
||||||
) =>
|
) =>
|
||||||
appState.editingElement
|
appState.editingElement
|
||||||
? [appState.editingElement]
|
? [appState.editingElement]
|
||||||
: getSelectedElements(elements, appState, true);
|
: getSelectedElements(elements, appState, {
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
});
|
||||||
|
|||||||
@@ -83,6 +83,14 @@ export const SHAPES = [
|
|||||||
numericKey: KEYS["0"],
|
numericKey: KEYS["0"],
|
||||||
fillable: false,
|
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;
|
] as const;
|
||||||
|
|
||||||
export const findShapeByKey = (key: string) => {
|
export const findShapeByKey = (key: string) => {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ Object {
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"frameId": null,
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
@@ -56,6 +57,7 @@ Object {
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"frameId": null,
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
@@ -89,6 +91,7 @@ Object {
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"frameId": null,
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
@@ -122,6 +125,7 @@ Object {
|
|||||||
"endArrowhead": null,
|
"endArrowhead": null,
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"frameId": null,
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
@@ -168,6 +172,7 @@ Object {
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"frameId": null,
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ exports[`export exporting svg containing transformed images: svg export output 1
|
|||||||
src: url(\\"https://excalidraw.com/Cascadia.woff2\\");
|
src: url(\\"https://excalidraw.com/Cascadia.woff2\\");
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</defs>
|
</defs>
|
||||||
<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>"
|
<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>"
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Object {
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"frameId": null,
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0_copy",
|
"id": "id0_copy",
|
||||||
@@ -37,6 +38,7 @@ Object {
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"frameId": null,
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
@@ -68,6 +70,7 @@ Object {
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"frameId": null,
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 50,
|
"height": 50,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
@@ -104,6 +107,7 @@ Object {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"frameId": null,
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 100,
|
"height": 100,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
@@ -140,6 +144,7 @@ Object {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"frameId": null,
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 300,
|
"height": 300,
|
||||||
"id": "id1",
|
"id": "id1",
|
||||||
@@ -177,6 +182,7 @@ Object {
|
|||||||
"gap": 10,
|
"gap": 10,
|
||||||
},
|
},
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"frameId": null,
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 81.48231043525051,
|
"height": 81.48231043525051,
|
||||||
"id": "id2",
|
"id": "id2",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Object {
|
|||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"frameId": null,
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 110,
|
"height": 110,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
@@ -61,6 +62,7 @@ Object {
|
|||||||
"endArrowhead": null,
|
"endArrowhead": null,
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
|
"frameId": null,
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 110,
|
"height": 110,
|
||||||
"id": "id0",
|
"id": "id0",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user