Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9fee5cf9c |
@@ -1,5 +1,3 @@
|
|||||||
MODE="development"
|
|
||||||
|
|
||||||
VITE_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
|
VITE_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
|
||||||
VITE_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
VITE_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
MODE="production"
|
|
||||||
|
|
||||||
VITE_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
VITE_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
|
||||||
VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
VITE_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,6 @@
|
|||||||
<a href="https://discord.gg/UexuTaE">
|
<a href="https://discord.gg/UexuTaE">
|
||||||
<img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
|
<img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://deepwiki.com/excalidraw/excalidraw">
|
|
||||||
<img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg" />
|
|
||||||
</a>
|
|
||||||
<a href="https://twitter.com/excalidraw">
|
<a href="https://twitter.com/excalidraw">
|
||||||
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
|
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1094,9 +1094,7 @@ export interface BoundingBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getCommonBoundingBox = (
|
export const getCommonBoundingBox = (
|
||||||
elements:
|
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
|
||||||
| readonly ExcalidrawElement[]
|
|
||||||
| readonly NonDeleted<ExcalidrawElement>[],
|
|
||||||
): BoundingBox => {
|
): BoundingBox => {
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -974,25 +974,6 @@ export const updateElbowArrowPoints = (
|
|||||||
),
|
),
|
||||||
"Elbow arrow segments must be either horizontal or vertical",
|
"Elbow arrow segments must be either horizontal or vertical",
|
||||||
);
|
);
|
||||||
|
|
||||||
invariant(
|
|
||||||
updates.fixedSegments?.find(
|
|
||||||
(segment) =>
|
|
||||||
segment.index === 1 &&
|
|
||||||
pointsEqual(segment.start, (updates.points ?? arrow.points)[0]),
|
|
||||||
) == null &&
|
|
||||||
updates.fixedSegments?.find(
|
|
||||||
(segment) =>
|
|
||||||
segment.index === (updates.points ?? arrow.points).length - 1 &&
|
|
||||||
pointsEqual(
|
|
||||||
segment.end,
|
|
||||||
(updates.points ?? arrow.points)[
|
|
||||||
(updates.points ?? arrow.points).length - 1
|
|
||||||
],
|
|
||||||
),
|
|
||||||
) == null,
|
|
||||||
"The first and last segments cannot be fixed",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
|
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
|
||||||
|
|||||||
@@ -1483,13 +1483,13 @@ const getArrowheadOptions = (flip: boolean) => {
|
|||||||
value: "crowfoot_one",
|
value: "crowfoot_one",
|
||||||
text: t("labels.arrowhead_crowfoot_one"),
|
text: t("labels.arrowhead_crowfoot_one"),
|
||||||
icon: <ArrowheadCrowfootOneIcon flip={flip} />,
|
icon: <ArrowheadCrowfootOneIcon flip={flip} />,
|
||||||
keyBinding: "x",
|
keyBinding: "c",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "crowfoot_many",
|
value: "crowfoot_many",
|
||||||
text: t("labels.arrowhead_crowfoot_many"),
|
text: t("labels.arrowhead_crowfoot_many"),
|
||||||
icon: <ArrowheadCrowfootIcon flip={flip} />,
|
icon: <ArrowheadCrowfootIcon flip={flip} />,
|
||||||
keyBinding: "c",
|
keyBinding: "x",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "crowfoot_one_or_many",
|
value: "crowfoot_one_or_many",
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ import {
|
|||||||
generateIdFromFile,
|
generateIdFromFile,
|
||||||
getDataURL,
|
getDataURL,
|
||||||
getDataURL_sync,
|
getDataURL_sync,
|
||||||
getFilesFromEvent,
|
getFileFromEvent,
|
||||||
ImageURLToFile,
|
ImageURLToFile,
|
||||||
isImageFileHandle,
|
isImageFileHandle,
|
||||||
isSupportedImageFile,
|
isSupportedImageFile,
|
||||||
@@ -3120,16 +3120,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const filesData = await getFilesFromEvent(event);
|
|
||||||
|
|
||||||
const imageFiles = filesData
|
|
||||||
.map((data) => data.file)
|
|
||||||
.filter((file): file is File => isSupportedImageFile(file));
|
|
||||||
|
|
||||||
if (imageFiles.length > 0 && this.isToolSupported("image")) {
|
|
||||||
return this.insertMultipleImages(imageFiles, sceneX, sceneY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// must be called in the same frame (thus before any awaits) as the paste
|
// must be called in the same frame (thus before any awaits) as the paste
|
||||||
// event else some browsers (FF...) will clear the clipboardData
|
// event else some browsers (FF...) will clear the clipboardData
|
||||||
// (something something security)
|
// (something something security)
|
||||||
@@ -4843,7 +4833,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ suggestedBindings: [] });
|
this.setState({ suggestedBindings: [] });
|
||||||
}
|
}
|
||||||
if (nextActiveTool.type === "image") {
|
if (nextActiveTool.type === "image") {
|
||||||
this.onImageToolbarButtonClick({
|
this.onImageAction({
|
||||||
insertOnCanvasDirectly:
|
insertOnCanvasDirectly:
|
||||||
(tool.type === "image" && tool.insertOnCanvasDirectly) ?? false,
|
(tool.type === "image" && tool.insertOnCanvasDirectly) ?? false,
|
||||||
});
|
});
|
||||||
@@ -10124,7 +10114,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// a future case, let's throw here
|
// a future case, let's throw here
|
||||||
if (!this.isToolSupported("image")) {
|
if (!this.isToolSupported("image")) {
|
||||||
this.setState({ errorMessage: t("errors.imageToolNotSupported") });
|
this.setState({ errorMessage: t("errors.imageToolNotSupported") });
|
||||||
return imageElement;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scene.insertElement(imageElement);
|
this.scene.insertElement(imageElement);
|
||||||
@@ -10143,7 +10133,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
errorMessage: error.message || t("errors.imageInsertError"),
|
errorMessage: error.message || t("errors.imageInsertError"),
|
||||||
});
|
});
|
||||||
return imageElement;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -10194,7 +10184,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onImageToolbarButtonClick = async ({
|
private onImageAction = async ({
|
||||||
insertOnCanvasDirectly,
|
insertOnCanvasDirectly,
|
||||||
}: {
|
}: {
|
||||||
insertOnCanvasDirectly: boolean;
|
insertOnCanvasDirectly: boolean;
|
||||||
@@ -10208,12 +10198,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const imageFiles = await fileOpen({
|
const imageFile = await fileOpen({
|
||||||
description: "Image",
|
description: "Image",
|
||||||
extensions: Object.keys(
|
extensions: Object.keys(
|
||||||
IMAGE_MIME_TYPES,
|
IMAGE_MIME_TYPES,
|
||||||
) as (keyof typeof IMAGE_MIME_TYPES)[],
|
) as (keyof typeof IMAGE_MIME_TYPES)[],
|
||||||
multiple: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const imageElement = this.createImageElement({
|
const imageElement = this.createImageElement({
|
||||||
@@ -10222,11 +10211,21 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
addToFrameUnderCursor: false,
|
addToFrameUnderCursor: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (insertOnCanvasDirectly || imageFiles.length > 1) {
|
if (insertOnCanvasDirectly) {
|
||||||
this.insertMultipleImages(imageFiles, x, y);
|
this.insertImageElement(imageElement, imageFile);
|
||||||
|
this.initializeImageDimensions(imageElement);
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
{ [imageElement.id]: true },
|
||||||
|
this.state,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.actionManager.executeAction(actionFinalize);
|
||||||
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const imageFile = imageFiles[0];
|
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
pendingImageElementId: imageElement.id,
|
pendingImageElementId: imageElement.id,
|
||||||
@@ -10504,205 +10503,69 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO rewrite (vibe-coded)
|
|
||||||
private positionElementsOnGrid = (
|
|
||||||
elements: ExcalidrawElement[] | ExcalidrawElement[][],
|
|
||||||
centerX: number,
|
|
||||||
centerY: number,
|
|
||||||
padding = 50,
|
|
||||||
) => {
|
|
||||||
// Ensure there are elements to position
|
|
||||||
if (!elements || elements.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize input to work with atomic units (groups of elements)
|
|
||||||
// If elements is a flat array, treat each element as its own atomic unit
|
|
||||||
const atomicUnits: ExcalidrawElement[][] = Array.isArray(elements[0])
|
|
||||||
? (elements as ExcalidrawElement[][])
|
|
||||||
: (elements as ExcalidrawElement[]).map((element) => [element]);
|
|
||||||
|
|
||||||
// Determine the number of columns for atomic units
|
|
||||||
// A common approach for a "grid-like" layout without specific column constraints
|
|
||||||
// is to aim for a roughly square arrangement.
|
|
||||||
const numUnits = atomicUnits.length;
|
|
||||||
const numColumns = Math.max(1, Math.ceil(Math.sqrt(numUnits)));
|
|
||||||
|
|
||||||
// Group atomic units into rows based on the calculated number of columns
|
|
||||||
const rows: ExcalidrawElement[][][] = [];
|
|
||||||
for (let i = 0; i < numUnits; i += numColumns) {
|
|
||||||
rows.push(atomicUnits.slice(i, i + numColumns));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate properties for each row (total width, max height)
|
|
||||||
// and the total actual height of all row content.
|
|
||||||
let totalGridActualHeight = 0; // Sum of max heights of rows, without inter-row padding
|
|
||||||
const rowProperties = rows.map((rowUnits) => {
|
|
||||||
let rowWidth = 0;
|
|
||||||
let maxUnitHeightInRow = 0;
|
|
||||||
|
|
||||||
const unitBounds = rowUnits.map((unit) => {
|
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(unit);
|
|
||||||
return {
|
|
||||||
elements: unit,
|
|
||||||
bounds: [minX, minY, maxX, maxY] as const,
|
|
||||||
width: maxX - minX,
|
|
||||||
height: maxY - minY,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
unitBounds.forEach((unitBound, index) => {
|
|
||||||
rowWidth += unitBound.width;
|
|
||||||
// Add padding between units in the same row, but not after the last one
|
|
||||||
if (index < unitBounds.length - 1) {
|
|
||||||
rowWidth += padding;
|
|
||||||
}
|
|
||||||
if (unitBound.height > maxUnitHeightInRow) {
|
|
||||||
maxUnitHeightInRow = unitBound.height;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
totalGridActualHeight += maxUnitHeightInRow;
|
|
||||||
return {
|
|
||||||
unitBounds,
|
|
||||||
width: rowWidth,
|
|
||||||
maxHeight: maxUnitHeightInRow,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate the total height of the grid including padding between rows
|
|
||||||
const totalGridHeightWithPadding =
|
|
||||||
totalGridActualHeight + Math.max(0, rows.length - 1) * padding;
|
|
||||||
|
|
||||||
// Calculate the starting Y position to center the entire grid vertically around centerY
|
|
||||||
let currentY = centerY - totalGridHeightWithPadding / 2;
|
|
||||||
|
|
||||||
// Position atomic units row by row
|
|
||||||
rowProperties.forEach((rowProp) => {
|
|
||||||
const { unitBounds, width: rowWidth, maxHeight: rowMaxHeight } = rowProp;
|
|
||||||
|
|
||||||
// Calculate the starting X for the current row to center it horizontally around centerX
|
|
||||||
let currentX = centerX - rowWidth / 2;
|
|
||||||
|
|
||||||
unitBounds.forEach((unitBound) => {
|
|
||||||
// Calculate the offset needed to position this atomic unit
|
|
||||||
const [originalMinX, originalMinY] = unitBound.bounds;
|
|
||||||
const offsetX = currentX - originalMinX;
|
|
||||||
const offsetY = currentY - originalMinY;
|
|
||||||
|
|
||||||
// Apply the offset to all elements in this atomic unit
|
|
||||||
unitBound.elements.forEach((element) => {
|
|
||||||
this.scene.mutateElement(element, {
|
|
||||||
x: element.x + offsetX,
|
|
||||||
y: element.y + offsetY,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Move X for the next unit in the row
|
|
||||||
currentX += unitBound.width + padding;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Move Y to the starting position for the next row
|
|
||||||
// This accounts for the tallest unit in the current row and the inter-row padding
|
|
||||||
currentY += rowMaxHeight + padding;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private insertMultipleImages = async (
|
|
||||||
imageFiles: File[],
|
|
||||||
sceneX: number,
|
|
||||||
sceneY: number,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const selectedElementIds: Record<ExcalidrawElement["id"], true> = {};
|
|
||||||
|
|
||||||
const imageElements: Promise<NonDeleted<ExcalidrawImageElement>>[] = [];
|
|
||||||
for (let i = 0; i < imageFiles.length; i++) {
|
|
||||||
const file = imageFiles[i];
|
|
||||||
|
|
||||||
const imageElement = this.createImageElement({
|
|
||||||
sceneX,
|
|
||||||
sceneY,
|
|
||||||
});
|
|
||||||
|
|
||||||
imageElements.push(this.insertImageElement(imageElement, file));
|
|
||||||
this.initializeImageDimensions(imageElement);
|
|
||||||
selectedElementIds[imageElement.id] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
selectedElementIds: makeNextSelectedElementIds(
|
|
||||||
selectedElementIds,
|
|
||||||
this.state,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.actionManager.executeAction(actionFinalize);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const initializedImageElements = await Promise.all(imageElements);
|
|
||||||
|
|
||||||
this.positionElementsOnGrid(initializedImageElements, sceneX, sceneY);
|
|
||||||
} catch (error: any) {
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : String(error);
|
|
||||||
this.setState({
|
|
||||||
isLoading: false,
|
|
||||||
errorMessage,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private handleAppOnDrop = async (event: React.DragEvent<HTMLDivElement>) => {
|
private handleAppOnDrop = async (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
// must be retrieved first, in the same frame
|
||||||
|
const { file, fileHandle } = await getFileFromEvent(event);
|
||||||
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||||
event,
|
event,
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
// must be retrieved first, in the same frame
|
try {
|
||||||
const filesData = await getFilesFromEvent(event);
|
// if image tool not supported, don't show an error here and let it fall
|
||||||
|
// through so we still support importing scene data from images. If no
|
||||||
|
// scene data encoded, we'll show an error then
|
||||||
|
if (isSupportedImageFile(file) && this.isToolSupported("image")) {
|
||||||
|
// first attempt to decode scene from the image if it's embedded
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
if (filesData.length === 1) {
|
if (file?.type === MIME_TYPES.png || file?.type === MIME_TYPES.svg) {
|
||||||
const { file, fileHandle } = filesData[0];
|
try {
|
||||||
|
const scene = await loadFromBlob(
|
||||||
if (
|
file,
|
||||||
file &&
|
this.state,
|
||||||
(file.type === MIME_TYPES.png || file.type === MIME_TYPES.svg)
|
this.scene.getElementsIncludingDeleted(),
|
||||||
) {
|
fileHandle,
|
||||||
try {
|
);
|
||||||
const scene = await loadFromBlob(
|
this.syncActionResult({
|
||||||
file,
|
...scene,
|
||||||
this.state,
|
appState: {
|
||||||
this.scene.getElementsIncludingDeleted(),
|
...(scene.appState || this.state),
|
||||||
fileHandle,
|
isLoading: false,
|
||||||
);
|
},
|
||||||
this.syncActionResult({
|
replaceFiles: true,
|
||||||
...scene,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
appState: {
|
});
|
||||||
...(scene.appState || this.state),
|
return;
|
||||||
isLoading: false,
|
} catch (error: any) {
|
||||||
},
|
// Don't throw for image scene daa
|
||||||
replaceFiles: true,
|
if (error.name !== "EncodingError") {
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||||
});
|
}
|
||||||
return;
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.name !== "EncodingError") {
|
|
||||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
|
||||||
}
|
}
|
||||||
// if EncodingError, fall through to insert as regular image
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if no scene is embedded or we fail for whatever reason, fall back
|
||||||
|
// to importing as regular image
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
const imageElement = this.createImageElement({ sceneX, sceneY });
|
||||||
|
this.insertImageElement(imageElement, file);
|
||||||
|
this.initializeImageDimensions(imageElement);
|
||||||
|
this.setState({
|
||||||
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
{ [imageElement.id]: true },
|
||||||
|
this.state,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
} catch (error: any) {
|
||||||
|
return this.setState({
|
||||||
const imageFiles = filesData
|
isLoading: false,
|
||||||
.map((data) => data.file)
|
errorMessage: error.message,
|
||||||
.filter((file): file is File => isSupportedImageFile(file));
|
});
|
||||||
|
|
||||||
if (imageFiles.length > 0 && this.isToolSupported("image")) {
|
|
||||||
return this.insertMultipleImages(imageFiles, sceneX, sceneY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const libraryJSON = event.dataTransfer.getData(MIME_TYPES.excalidrawlib);
|
const libraryJSON = event.dataTransfer.getData(MIME_TYPES.excalidrawlib);
|
||||||
@@ -10720,12 +10583,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filesData.length > 1) {
|
if (file) {
|
||||||
const { file, fileHandle } = filesData[0];
|
// Attempt to parse an excalidraw/excalidrawlib file
|
||||||
if (file) {
|
await this.loadFileToCanvas(file, fileHandle);
|
||||||
// Attempt to parse an excalidraw/excalidrawlib file
|
|
||||||
await this.loadFileToCanvas(file, fileHandle);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.dataTransfer?.types?.includes("text/plain")) {
|
if (event.dataTransfer?.types?.includes("text/plain")) {
|
||||||
|
|||||||
@@ -564,7 +564,7 @@ export const convertElementTypes = (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const fixedSegments: FixedSegment[] = [];
|
const fixedSegments: FixedSegment[] = [];
|
||||||
for (let i = 1; i < nextPoints.length - 2; i++) {
|
for (let i = 0; i < nextPoints.length - 1; i++) {
|
||||||
fixedSegments.push({
|
fixedSegments.push({
|
||||||
start: nextPoints[i],
|
start: nextPoints[i],
|
||||||
end: nextPoints[i + 1],
|
end: nextPoints[i + 1],
|
||||||
@@ -581,7 +581,6 @@ export const convertElementTypes = (
|
|||||||
);
|
);
|
||||||
mutateElement(element, app.scene.getNonDeletedElementsMap(), {
|
mutateElement(element, app.scene.getNonDeletedElementsMap(), {
|
||||||
...updates,
|
...updates,
|
||||||
endArrowhead: "arrow",
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// if we're converting to non-elbow linear element, check if
|
// if we're converting to non-elbow linear element, check if
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
shouldKeepAspectRatio,
|
shouldKeepAspectRatio,
|
||||||
shouldChangeByStepSize,
|
shouldChangeByStepSize,
|
||||||
nextValue,
|
nextValue,
|
||||||
|
ratio,
|
||||||
property,
|
property,
|
||||||
originalAppState,
|
originalAppState,
|
||||||
instantChange,
|
instantChange,
|
||||||
@@ -154,6 +155,12 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
|
if (ratio) {
|
||||||
|
ratio = property === "width" ? ratio : [ratio[1], ratio[0]];
|
||||||
|
nextValue = (origElement[property] / ratio[0]) * ratio[1];
|
||||||
|
property = property === "width" ? "height" : "width";
|
||||||
|
}
|
||||||
|
|
||||||
const nextWidth = Math.max(
|
const nextWidth = Math.max(
|
||||||
property === "width"
|
property === "width"
|
||||||
? nextValue
|
? nextValue
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export type DragInputCallbackType<
|
|||||||
shouldChangeByStepSize: boolean;
|
shouldChangeByStepSize: boolean;
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
nextValue?: number;
|
nextValue?: number;
|
||||||
|
ratio?: [number, number] | null;
|
||||||
property: P;
|
property: P;
|
||||||
originalAppState: AppState;
|
originalAppState: AppState;
|
||||||
setInputValue: (value: number) => void;
|
setInputValue: (value: number) => void;
|
||||||
@@ -109,10 +110,21 @@ const StatsDragInput = <
|
|||||||
}
|
}
|
||||||
stateRef.current.updatePending = false;
|
stateRef.current.updatePending = false;
|
||||||
|
|
||||||
|
const ratioMatch = updatedValue
|
||||||
|
.trim()
|
||||||
|
.match(/^(\d+(?:\.\d+)?):(\d+(?:\.\d+)?)$/);
|
||||||
|
|
||||||
|
let ratio: [number, number] | null = null;
|
||||||
|
if (ratioMatch) {
|
||||||
|
ratio = [Number(ratioMatch[1]), Number(ratioMatch[2])];
|
||||||
|
}
|
||||||
|
|
||||||
const parsed = Number(updatedValue);
|
const parsed = Number(updatedValue);
|
||||||
if (isNaN(parsed)) {
|
if (isNaN(parsed)) {
|
||||||
setInputValue(value.toString());
|
setInputValue(value.toString());
|
||||||
return;
|
if (!ratio) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rounded = Number(parsed.toFixed(2));
|
const rounded = Number(parsed.toFixed(2));
|
||||||
@@ -123,7 +135,11 @@ const StatsDragInput = <
|
|||||||
// 2. original was not "Mixed" and the difference between a new value and previous value is greater
|
// 2. original was not "Mixed" and the difference between a new value and previous value is greater
|
||||||
// than the smallest delta allowed, which is 0.01
|
// than the smallest delta allowed, which is 0.01
|
||||||
// reason: idempotent to avoid unnecessary
|
// reason: idempotent to avoid unnecessary
|
||||||
if (isNaN(original) || Math.abs(rounded - original) >= SMALLEST_DELTA) {
|
if (
|
||||||
|
isNaN(original) ||
|
||||||
|
ratio ||
|
||||||
|
Math.abs(rounded - original) >= SMALLEST_DELTA
|
||||||
|
) {
|
||||||
stateRef.current.lastUpdatedValue = updatedValue;
|
stateRef.current.lastUpdatedValue = updatedValue;
|
||||||
dragInputCallback({
|
dragInputCallback({
|
||||||
accumulatedChange: 0,
|
accumulatedChange: 0,
|
||||||
@@ -134,6 +150,7 @@ const StatsDragInput = <
|
|||||||
shouldChangeByStepSize: false,
|
shouldChangeByStepSize: false,
|
||||||
scene,
|
scene,
|
||||||
nextValue: rounded,
|
nextValue: rounded,
|
||||||
|
ratio,
|
||||||
property,
|
property,
|
||||||
originalAppState: appState,
|
originalAppState: appState,
|
||||||
setInputValue: (value) => setInputValue(String(value)),
|
setInputValue: (value) => setInputValue(String(value)),
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
originalAppState,
|
originalAppState,
|
||||||
shouldChangeByStepSize,
|
shouldChangeByStepSize,
|
||||||
nextValue,
|
nextValue,
|
||||||
|
ratio,
|
||||||
scene,
|
scene,
|
||||||
property,
|
property,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -202,9 +203,22 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
origElement &&
|
origElement &&
|
||||||
isPropertyEditable(latestElement, property)
|
isPropertyEditable(latestElement, property)
|
||||||
) {
|
) {
|
||||||
|
let _nextValue = nextValue;
|
||||||
|
let _property = property;
|
||||||
|
if (ratio) {
|
||||||
|
let _ratio = ratio;
|
||||||
|
if (ratio) {
|
||||||
|
_ratio = _property === "width" ? _ratio : [_ratio[1], _ratio[0]];
|
||||||
|
_nextValue = (origElement[_property] / _ratio[0]) * _ratio[1];
|
||||||
|
_property = _property === "width" ? "height" : "width";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let nextWidth =
|
let nextWidth =
|
||||||
property === "width" ? Math.max(0, nextValue) : latestElement.width;
|
_property === "width"
|
||||||
if (property === "width") {
|
? Math.max(0, _nextValue)
|
||||||
|
: latestElement.width;
|
||||||
|
if (_property === "width") {
|
||||||
if (shouldChangeByStepSize) {
|
if (shouldChangeByStepSize) {
|
||||||
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
|
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
|
||||||
} else {
|
} else {
|
||||||
@@ -213,10 +227,10 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
}
|
}
|
||||||
|
|
||||||
let nextHeight =
|
let nextHeight =
|
||||||
property === "height"
|
_property === "height"
|
||||||
? Math.max(0, nextValue)
|
? Math.max(0, _nextValue)
|
||||||
: latestElement.height;
|
: latestElement.height;
|
||||||
if (property === "height") {
|
if (_property === "height") {
|
||||||
if (shouldChangeByStepSize) {
|
if (shouldChangeByStepSize) {
|
||||||
nextHeight = getStepSizedValue(nextHeight, STEP_SIZE);
|
nextHeight = getStepSizedValue(nextHeight, STEP_SIZE);
|
||||||
} else {
|
} else {
|
||||||
@@ -234,7 +248,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
origElement,
|
origElement,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
scene,
|
scene,
|
||||||
property === "width" ? "e" : "s",
|
_property === "width" ? "e" : "s",
|
||||||
{
|
{
|
||||||
shouldInformMutation: false,
|
shouldInformMutation: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -385,53 +385,23 @@ export const ImageURLToFile = async (
|
|||||||
throw new Error("Error: unsupported file type", { cause: "UNSUPPORTED" });
|
throw new Error("Error: unsupported file type", { cause: "UNSUPPORTED" });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFilesFromEvent = async (
|
export const getFileFromEvent = async (
|
||||||
event: React.DragEvent<HTMLDivElement> | ClipboardEvent,
|
event: React.DragEvent<HTMLDivElement>,
|
||||||
) => {
|
) => {
|
||||||
let fileList: FileList | undefined = undefined;
|
const file = event.dataTransfer.files.item(0);
|
||||||
let items: DataTransferItemList | undefined = undefined;
|
const fileHandle = await getFileHandle(event);
|
||||||
|
|
||||||
if (event instanceof ClipboardEvent) {
|
return { file: file ? await normalizeFile(file) : null, fileHandle };
|
||||||
fileList = event.clipboardData?.files;
|
|
||||||
items = event.clipboardData?.items;
|
|
||||||
} else {
|
|
||||||
fileList = event.dataTransfer?.files;
|
|
||||||
items = event.dataTransfer?.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
const files: (File | null)[] = Array.from(fileList || []);
|
|
||||||
|
|
||||||
return await Promise.all(
|
|
||||||
files.map(async (file, idx) => {
|
|
||||||
const dataTransferItem = items?.[idx];
|
|
||||||
const fileHandle = dataTransferItem
|
|
||||||
? getFileHandle(dataTransferItem)
|
|
||||||
: null;
|
|
||||||
return file
|
|
||||||
? {
|
|
||||||
file: await normalizeFile(file),
|
|
||||||
fileHandle: await fileHandle,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
file: null,
|
|
||||||
fileHandle: null,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFileHandle = async (
|
export const getFileHandle = async (
|
||||||
event: DragEvent | React.DragEvent | DataTransferItem,
|
event: React.DragEvent<HTMLDivElement>,
|
||||||
): Promise<FileSystemHandle | null> => {
|
): Promise<FileSystemHandle | null> => {
|
||||||
if (nativeFileSystemSupported) {
|
if (nativeFileSystemSupported) {
|
||||||
try {
|
try {
|
||||||
const dataTransferItem =
|
const item = event.dataTransfer.items[0];
|
||||||
event instanceof DataTransferItem
|
|
||||||
? event
|
|
||||||
: (event as DragEvent).dataTransfer?.items?.[0];
|
|
||||||
|
|
||||||
const handle: FileSystemHandle | null =
|
const handle: FileSystemHandle | null =
|
||||||
(await (dataTransferItem as any).getAsFileSystemHandle()) || null;
|
(await (item as any).getAsFileSystemHandle()) || null;
|
||||||
|
|
||||||
return handle;
|
return handle;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
Reference in New Issue
Block a user