mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Initial commit
This commit is contained in:
parent
4ec812bc18
commit
c22407b3ae
5 changed files with 171 additions and 0 deletions
|
@ -65,6 +65,7 @@ export const getDefaultAppState = (): Omit<
|
||||||
gridStep: DEFAULT_GRID_STEP,
|
gridStep: DEFAULT_GRID_STEP,
|
||||||
gridModeEnabled: false,
|
gridModeEnabled: false,
|
||||||
isBindingEnabled: true,
|
isBindingEnabled: true,
|
||||||
|
isShapeSnapEnabled: true,
|
||||||
defaultSidebarDockedPreference: false,
|
defaultSidebarDockedPreference: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isResizing: false,
|
isResizing: false,
|
||||||
|
@ -186,6 +187,7 @@ const APP_STATE_STORAGE_CONF = (<
|
||||||
gridModeEnabled: { browser: true, export: true, server: true },
|
gridModeEnabled: { browser: true, export: true, server: true },
|
||||||
height: { browser: false, export: false, server: false },
|
height: { browser: false, export: false, server: false },
|
||||||
isBindingEnabled: { browser: false, export: false, server: false },
|
isBindingEnabled: { browser: false, export: false, server: false },
|
||||||
|
isShapeSnapEnabled: { browser: true, export: true, server: true }, // Add shape snapping config
|
||||||
defaultSidebarDockedPreference: {
|
defaultSidebarDockedPreference: {
|
||||||
browser: true,
|
browser: true,
|
||||||
export: false,
|
export: false,
|
||||||
|
|
|
@ -299,6 +299,7 @@ import {
|
||||||
maybeParseEmbedSrc,
|
maybeParseEmbedSrc,
|
||||||
getEmbedLink,
|
getEmbedLink,
|
||||||
} from "../element/embeddable";
|
} from "../element/embeddable";
|
||||||
|
import { convertToShape } from "@excalidraw/utils/snapToShape";
|
||||||
import type { ContextMenuItems } from "./ContextMenu";
|
import type { ContextMenuItems } from "./ContextMenu";
|
||||||
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
||||||
import LayerUI from "./LayerUI";
|
import LayerUI from "./LayerUI";
|
||||||
|
@ -8989,6 +8990,22 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
lastCommittedPoint: pointFrom<LocalPoint>(dx, dy),
|
lastCommittedPoint: pointFrom<LocalPoint>(dx, dy),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.state.isShapeSnapEnabled) {
|
||||||
|
const detectedElement = convertToShape(newElement);
|
||||||
|
if (detectedElement !== newElement) {
|
||||||
|
this.scene.replaceAllElements([
|
||||||
|
...this.scene
|
||||||
|
.getElementsIncludingDeleted()
|
||||||
|
.filter((el) => el.id !== newElement.id),
|
||||||
|
detectedElement,
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selectedElementIds: { [detectedElement.id]: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -315,6 +315,7 @@ export interface AppState {
|
||||||
currentHoveredFontFamily: FontFamilyValues | null;
|
currentHoveredFontFamily: FontFamilyValues | null;
|
||||||
currentItemRoundness: StrokeRoundness;
|
currentItemRoundness: StrokeRoundness;
|
||||||
currentItemArrowType: "sharp" | "round" | "elbow";
|
currentItemArrowType: "sharp" | "round" | "elbow";
|
||||||
|
isShapeSnapEnabled: boolean;
|
||||||
viewBackgroundColor: string;
|
viewBackgroundColor: string;
|
||||||
scrollX: number;
|
scrollX: number;
|
||||||
scrollY: number;
|
scrollY: number;
|
||||||
|
@ -894,3 +895,17 @@ export type Offsets = Partial<{
|
||||||
bottom: number;
|
bottom: number;
|
||||||
left: number;
|
left: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type ShapeDetectionType =
|
||||||
|
| "rectangle"
|
||||||
|
| "ellipse"
|
||||||
|
| "diamond"
|
||||||
|
| "arrow"
|
||||||
|
| "line"
|
||||||
|
| "freedraw";
|
||||||
|
|
||||||
|
export interface ShapeDetectionResult {
|
||||||
|
type: ShapeDetectionType;
|
||||||
|
points: readonly (readonly [number, number])[];
|
||||||
|
confidence: number;
|
||||||
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@amaplex-software/shapeit": "0.1.6",
|
||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
"@excalidraw/laser-pointer": "1.3.1",
|
"@excalidraw/laser-pointer": "1.3.1",
|
||||||
"browser-fs-access": "0.29.1",
|
"browser-fs-access": "0.29.1",
|
||||||
|
|
136
packages/utils/snapToShape.ts
Normal file
136
packages/utils/snapToShape.ts
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import type {
|
||||||
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawDiamondElement,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawEllipseElement,
|
||||||
|
ExcalidrawFreeDrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
|
ExcalidrawRectangleElement,
|
||||||
|
} from "../excalidraw/element/types";
|
||||||
|
import type { BoundingBox } from "../excalidraw/element/bounds";
|
||||||
|
import { getCommonBoundingBox } from "../excalidraw/element/bounds";
|
||||||
|
import { newElement } from "../excalidraw/element";
|
||||||
|
// @ts-ignore
|
||||||
|
import shapeit from "@amaplex-software/shapeit";
|
||||||
|
|
||||||
|
type Shape =
|
||||||
|
| ExcalidrawRectangleElement["type"]
|
||||||
|
| ExcalidrawEllipseElement["type"]
|
||||||
|
| ExcalidrawDiamondElement["type"]
|
||||||
|
// | ExcalidrawArrowElement["type"]
|
||||||
|
// | ExcalidrawLinearElement["type"]
|
||||||
|
| ExcalidrawFreeDrawElement["type"];
|
||||||
|
|
||||||
|
interface ShapeRecognitionResult {
|
||||||
|
type: Shape;
|
||||||
|
confidence: number;
|
||||||
|
boundingBox: BoundingBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recognizes common shapes from free-draw input
|
||||||
|
* @param element The freedraw element to analyze
|
||||||
|
* @returns Information about the recognized shape, or null if no shape is recognized
|
||||||
|
*/
|
||||||
|
export const recognizeShape = (
|
||||||
|
element: ExcalidrawFreeDrawElement,
|
||||||
|
): ShapeRecognitionResult => {
|
||||||
|
const boundingBox = getCommonBoundingBox([element]);
|
||||||
|
|
||||||
|
// We need at least a few points to recognize a shape
|
||||||
|
if (!element.points || element.points.length < 3) {
|
||||||
|
return { type: "freedraw", confidence: 1, boundingBox };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Recognizing shape from points:", element.points);
|
||||||
|
|
||||||
|
const shapethat = shapeit.new({
|
||||||
|
atlas: {},
|
||||||
|
output: {},
|
||||||
|
thresholds: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const shape = shapethat(element.points);
|
||||||
|
|
||||||
|
console.log("Shape recognized:", shape);
|
||||||
|
|
||||||
|
const mappedShape = (name: string): Shape => {
|
||||||
|
switch (name) {
|
||||||
|
case "rectangle":
|
||||||
|
return "rectangle";
|
||||||
|
case "square":
|
||||||
|
return "rectangle";
|
||||||
|
case "circle":
|
||||||
|
return "ellipse";
|
||||||
|
case "open polygon":
|
||||||
|
return "diamond";
|
||||||
|
default:
|
||||||
|
return "freedraw";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const recognizedShape: ShapeRecognitionResult = {
|
||||||
|
type: mappedShape(shape.name),
|
||||||
|
confidence: 0.8,
|
||||||
|
boundingBox,
|
||||||
|
};
|
||||||
|
|
||||||
|
return recognizedShape;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new element based on the recognized shape from a freedraw element
|
||||||
|
* @param freedrawElement The original freedraw element
|
||||||
|
* @param recognizedShape The recognized shape information
|
||||||
|
* @returns A new element of the recognized shape type
|
||||||
|
*/
|
||||||
|
export const createElementFromRecognizedShape = (
|
||||||
|
freedrawElement: ExcalidrawFreeDrawElement,
|
||||||
|
recognizedShape: ShapeRecognitionResult,
|
||||||
|
): ExcalidrawElement => {
|
||||||
|
if (!recognizedShape.type || recognizedShape.type === "freedraw") {
|
||||||
|
return freedrawElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (recognizedShape.type === "rectangle") {
|
||||||
|
return newElement({
|
||||||
|
...freedrawElement,
|
||||||
|
type: recognizedShape.type,
|
||||||
|
x: recognizedShape.boundingBox.minX,
|
||||||
|
y: recognizedShape.boundingBox.minY,
|
||||||
|
width: recognizedShape.boundingBox.width!,
|
||||||
|
height: recognizedShape.boundingBox.height!,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if shape recognition should be applied based on app state
|
||||||
|
* @param element The freedraw element to potentially snap
|
||||||
|
* @param minConfidence The minimum confidence level required to apply snapping
|
||||||
|
* @returns Whether to apply shape snapping
|
||||||
|
*/
|
||||||
|
export const shouldApplyShapeSnapping = (
|
||||||
|
recognizedShape: ShapeRecognitionResult,
|
||||||
|
minConfidence: number = 0.75,
|
||||||
|
): boolean => {
|
||||||
|
return (
|
||||||
|
!!recognizedShape.type && (recognizedShape.confidence || 0) >= minConfidence
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a freedraw element to the detected shape
|
||||||
|
*/
|
||||||
|
export const convertToShape = (
|
||||||
|
freeDrawElement: ExcalidrawFreeDrawElement,
|
||||||
|
): ExcalidrawElement => {
|
||||||
|
const recognizedShape = recognizeShape(freeDrawElement);
|
||||||
|
|
||||||
|
if (shouldApplyShapeSnapping(recognizedShape)) {
|
||||||
|
return createElementFromRecognizedShape(freeDrawElement, recognizedShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add more shape conversions as needed
|
||||||
|
return freeDrawElement;
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue