mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: wireframe-to-code (#7334)
This commit is contained in:
parent
d1e4421823
commit
c7ee46e7f8
63 changed files with 2106 additions and 444 deletions
14
src/element/ElementCanvasButtons.scss
Normal file
14
src/element/ElementCanvasButtons.scss
Normal file
|
@ -0,0 +1,14 @@
|
|||
.excalidraw {
|
||||
.excalidraw-canvas-buttons {
|
||||
position: absolute;
|
||||
|
||||
box-shadow: 0px 2px 4px 0 rgb(0 0 0 / 30%);
|
||||
z-index: var(--zIndex-canvasButtons);
|
||||
background: var(--island-bg-color);
|
||||
border-radius: var(--border-radius-lg);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
}
|
60
src/element/ElementCanvasButtons.tsx
Normal file
60
src/element/ElementCanvasButtons.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { AppState } from "../types";
|
||||
import { sceneCoordsToViewportCoords } from "../utils";
|
||||
import { NonDeletedExcalidrawElement } from "./types";
|
||||
import { getElementAbsoluteCoords } from ".";
|
||||
import { useExcalidrawAppState } from "../components/App";
|
||||
|
||||
import "./ElementCanvasButtons.scss";
|
||||
|
||||
const CONTAINER_PADDING = 5;
|
||||
|
||||
const getContainerCoords = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
appState: AppState,
|
||||
) => {
|
||||
const [x1, y1] = getElementAbsoluteCoords(element);
|
||||
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: x1 + element.width, sceneY: y1 },
|
||||
appState,
|
||||
);
|
||||
const x = viewportX - appState.offsetLeft + 10;
|
||||
const y = viewportY - appState.offsetTop;
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
export const ElementCanvasButtons = ({
|
||||
children,
|
||||
element,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
element: NonDeletedExcalidrawElement;
|
||||
}) => {
|
||||
const appState = useExcalidrawAppState();
|
||||
|
||||
if (
|
||||
appState.contextMenu ||
|
||||
appState.draggingElement ||
|
||||
appState.resizingElement ||
|
||||
appState.isRotating ||
|
||||
appState.openMenu ||
|
||||
appState.viewModeEnabled
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { x, y } = getContainerCoords(element, appState);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="excalidraw-canvas-buttons"
|
||||
style={{
|
||||
top: `${y}px`,
|
||||
left: `${x}px`,
|
||||
// width: CONTAINER_WIDTH,
|
||||
padding: CONTAINER_PADDING,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -6,7 +6,7 @@
|
|||
justify-content: space-between;
|
||||
position: absolute;
|
||||
box-shadow: 0px 2px 4px 0 rgb(0 0 0 / 30%);
|
||||
z-index: 100;
|
||||
z-index: var(--zIndex-hyperlinkContainer);
|
||||
background: var(--island-bg-color);
|
||||
border-radius: var(--border-radius-md);
|
||||
box-sizing: border-box;
|
||||
|
|
|
@ -121,7 +121,7 @@ export const Hyperlink = ({
|
|||
setToast({ message: embedLink.warning, closable: true });
|
||||
}
|
||||
const ar = embedLink
|
||||
? embedLink.aspectRatio.w / embedLink.aspectRatio.h
|
||||
? embedLink.intrinsicSize.w / embedLink.intrinsicSize.h
|
||||
: 1;
|
||||
const hasLinkChanged =
|
||||
embeddableLinkCache.get(element.id) !== element.link;
|
||||
|
@ -210,6 +210,7 @@ export const Hyperlink = ({
|
|||
};
|
||||
const { x, y } = getCoordsForPopover(element, appState);
|
||||
if (
|
||||
appState.contextMenu ||
|
||||
appState.draggingElement ||
|
||||
appState.resizingElement ||
|
||||
appState.isRotating ||
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawRectangleElement,
|
||||
ExcalidrawEmbeddableElement,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawEllipseElement,
|
||||
|
@ -27,7 +26,8 @@ import {
|
|||
ExcalidrawImageElement,
|
||||
ExcalidrawLinearElement,
|
||||
StrokeRoundness,
|
||||
ExcalidrawFrameElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
ExcalidrawIframeLikeElement,
|
||||
} from "./types";
|
||||
|
||||
import {
|
||||
|
@ -41,7 +41,8 @@ import { Drawable } from "roughjs/bin/core";
|
|||
import { AppState } from "../types";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isEmbeddableElement,
|
||||
isFrameLikeElement,
|
||||
isIframeLikeElement,
|
||||
isImageElement,
|
||||
} from "./typeChecks";
|
||||
import { isTextElement } from ".";
|
||||
|
@ -64,7 +65,7 @@ const isElementDraggableFromInside = (
|
|||
const isDraggableFromInside =
|
||||
!isTransparent(element.backgroundColor) ||
|
||||
hasBoundTextElement(element) ||
|
||||
isEmbeddableElement(element);
|
||||
isIframeLikeElement(element);
|
||||
if (element.type === "line") {
|
||||
return isDraggableFromInside && isPathALoop(element.points);
|
||||
}
|
||||
|
@ -186,7 +187,7 @@ export const isPointHittingElementBoundingBox = (
|
|||
// by its frame, whether it has been selected or not
|
||||
// this logic here is not ideal
|
||||
// TODO: refactor it later...
|
||||
if (element.type === "frame") {
|
||||
if (isFrameLikeElement(element)) {
|
||||
return hitTestPointAgainstElement({
|
||||
element,
|
||||
point: [x, y],
|
||||
|
@ -255,6 +256,7 @@ type HitTestArgs = {
|
|||
const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
|
||||
switch (args.element.type) {
|
||||
case "rectangle":
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "image":
|
||||
case "text":
|
||||
|
@ -282,7 +284,8 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
|
|||
"This should not happen, we need to investigate why it does.",
|
||||
);
|
||||
return false;
|
||||
case "frame": {
|
||||
case "frame":
|
||||
case "magicframe": {
|
||||
// check distance to frame element first
|
||||
if (
|
||||
args.check(
|
||||
|
@ -314,8 +317,10 @@ export const distanceToBindableElement = (
|
|||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
return distanceToRectangle(element, point);
|
||||
case "diamond":
|
||||
return distanceToDiamond(element, point);
|
||||
|
@ -346,8 +351,8 @@ const distanceToRectangle = (
|
|||
| ExcalidrawTextElement
|
||||
| ExcalidrawFreeDrawElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawFrameElement,
|
||||
| ExcalidrawIframeLikeElement
|
||||
| ExcalidrawFrameLikeElement,
|
||||
point: Point,
|
||||
): number => {
|
||||
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
|
||||
|
@ -662,8 +667,10 @@ export const determineFocusDistance = (
|
|||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
ret = c / (hwidth * (nabs + q * mabs));
|
||||
break;
|
||||
case "diamond":
|
||||
|
@ -700,8 +707,10 @@ export const determineFocusPoint = (
|
|||
case "image":
|
||||
case "text":
|
||||
case "diamond":
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
point = findFocusPointForRectangulars(element, focus, adjecentPointRel);
|
||||
break;
|
||||
case "ellipse":
|
||||
|
@ -752,8 +761,10 @@ const getSortedElementLineIntersections = (
|
|||
case "image":
|
||||
case "text":
|
||||
case "diamond":
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
const corners = getCorners(element);
|
||||
intersections = corners
|
||||
.flatMap((point, i) => {
|
||||
|
@ -788,8 +799,8 @@ const getCorners = (
|
|||
| ExcalidrawImageElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawFrameElement,
|
||||
| ExcalidrawIframeLikeElement
|
||||
| ExcalidrawFrameLikeElement,
|
||||
scale: number = 1,
|
||||
): GA.Point[] => {
|
||||
const hx = (scale * element.width) / 2;
|
||||
|
@ -798,8 +809,10 @@ const getCorners = (
|
|||
case "rectangle":
|
||||
case "image":
|
||||
case "text":
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
return [
|
||||
GA.point(hx, hy),
|
||||
GA.point(hx, -hy),
|
||||
|
@ -948,8 +961,8 @@ export const findFocusPointForRectangulars = (
|
|||
| ExcalidrawImageElement
|
||||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawFrameElement,
|
||||
| ExcalidrawIframeLikeElement
|
||||
| ExcalidrawFrameLikeElement,
|
||||
// Between -1 and 1 for how far away should the focus point be relative
|
||||
// to the size of the element. Sign determines orientation.
|
||||
relativeDistance: number,
|
||||
|
|
|
@ -11,7 +11,7 @@ import Scene from "../scene/Scene";
|
|||
import {
|
||||
isArrowElement,
|
||||
isBoundToContainer,
|
||||
isFrameElement,
|
||||
isFrameLikeElement,
|
||||
} from "./typeChecks";
|
||||
|
||||
export const dragSelectedElements = (
|
||||
|
@ -33,7 +33,7 @@ export const dragSelectedElements = (
|
|||
selectedElements,
|
||||
);
|
||||
const frames = selectedElements
|
||||
.filter((e) => isFrameElement(e))
|
||||
.filter((e) => isFrameLikeElement(e))
|
||||
.map((f) => f.id);
|
||||
|
||||
if (frames.length > 0) {
|
||||
|
|
|
@ -6,25 +6,19 @@ import { getFontString, updateActiveTool } from "../utils";
|
|||
import { setCursorForShape } from "../cursor";
|
||||
import { newTextElement } from "./newElement";
|
||||
import { getContainerElement, wrapText } from "./textElement";
|
||||
import { isEmbeddableElement } from "./typeChecks";
|
||||
import {
|
||||
isFrameLikeElement,
|
||||
isIframeElement,
|
||||
isIframeLikeElement,
|
||||
} from "./typeChecks";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawEmbeddableElement,
|
||||
ExcalidrawIframeLikeElement,
|
||||
IframeData,
|
||||
NonDeletedExcalidrawElement,
|
||||
Theme,
|
||||
} from "./types";
|
||||
|
||||
type EmbeddedLink =
|
||||
| ({
|
||||
aspectRatio: { w: number; h: number };
|
||||
warning?: string;
|
||||
} & (
|
||||
| { type: "video" | "generic"; link: string }
|
||||
| { type: "document"; srcdoc: (theme: Theme) => string }
|
||||
))
|
||||
| null;
|
||||
|
||||
const embeddedLinkCache = new Map<string, EmbeddedLink>();
|
||||
const embeddedLinkCache = new Map<string, IframeData>();
|
||||
|
||||
const RE_YOUTUBE =
|
||||
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
|
||||
|
@ -67,11 +61,13 @@ const ALLOWED_DOMAINS = new Set([
|
|||
"dddice.com",
|
||||
]);
|
||||
|
||||
const createSrcDoc = (body: string) => {
|
||||
export const createSrcDoc = (body: string) => {
|
||||
return `<html><body>${body}</body></html>`;
|
||||
};
|
||||
|
||||
export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
|
||||
export const getEmbedLink = (
|
||||
link: string | null | undefined,
|
||||
): IframeData | null => {
|
||||
if (!link) {
|
||||
return null;
|
||||
}
|
||||
|
@ -104,8 +100,12 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
|
|||
break;
|
||||
}
|
||||
aspectRatio = isPortrait ? { w: 315, h: 560 } : { w: 560, h: 315 };
|
||||
embeddedLinkCache.set(originalLink, { link, aspectRatio, type });
|
||||
return { link, aspectRatio, type };
|
||||
embeddedLinkCache.set(originalLink, {
|
||||
link,
|
||||
intrinsicSize: aspectRatio,
|
||||
type,
|
||||
});
|
||||
return { link, intrinsicSize: aspectRatio, type };
|
||||
}
|
||||
|
||||
const vimeoLink = link.match(RE_VIMEO);
|
||||
|
@ -119,8 +119,12 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
|
|||
aspectRatio = { w: 560, h: 315 };
|
||||
//warning deliberately ommited so it is displayed only once per link
|
||||
//same link next time will be served from cache
|
||||
embeddedLinkCache.set(originalLink, { link, aspectRatio, type });
|
||||
return { link, aspectRatio, type, warning };
|
||||
embeddedLinkCache.set(originalLink, {
|
||||
link,
|
||||
intrinsicSize: aspectRatio,
|
||||
type,
|
||||
});
|
||||
return { link, intrinsicSize: aspectRatio, type, warning };
|
||||
}
|
||||
|
||||
const figmaLink = link.match(RE_FIGMA);
|
||||
|
@ -130,27 +134,35 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
|
|||
link,
|
||||
)}`;
|
||||
aspectRatio = { w: 550, h: 550 };
|
||||
embeddedLinkCache.set(originalLink, { link, aspectRatio, type });
|
||||
return { link, aspectRatio, type };
|
||||
embeddedLinkCache.set(originalLink, {
|
||||
link,
|
||||
intrinsicSize: aspectRatio,
|
||||
type,
|
||||
});
|
||||
return { link, intrinsicSize: aspectRatio, type };
|
||||
}
|
||||
|
||||
const valLink = link.match(RE_VALTOWN);
|
||||
if (valLink) {
|
||||
link =
|
||||
valLink[1] === "embed" ? valLink[0] : valLink[0].replace("/v", "/embed");
|
||||
embeddedLinkCache.set(originalLink, { link, aspectRatio, type });
|
||||
return { link, aspectRatio, type };
|
||||
embeddedLinkCache.set(originalLink, {
|
||||
link,
|
||||
intrinsicSize: aspectRatio,
|
||||
type,
|
||||
});
|
||||
return { link, intrinsicSize: aspectRatio, type };
|
||||
}
|
||||
|
||||
if (RE_TWITTER.test(link)) {
|
||||
let ret: EmbeddedLink;
|
||||
let ret: IframeData;
|
||||
// assume embed code
|
||||
if (/<blockquote/.test(link)) {
|
||||
const srcDoc = createSrcDoc(link);
|
||||
ret = {
|
||||
type: "document",
|
||||
srcdoc: () => srcDoc,
|
||||
aspectRatio: { w: 480, h: 480 },
|
||||
intrinsicSize: { w: 480, h: 480 },
|
||||
};
|
||||
// assume regular tweet url
|
||||
} else {
|
||||
|
@ -160,7 +172,7 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
|
|||
createSrcDoc(
|
||||
`<blockquote class="twitter-tweet" data-dnt="true" data-theme="${theme}"><a href="${link}"></a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`,
|
||||
),
|
||||
aspectRatio: { w: 480, h: 480 },
|
||||
intrinsicSize: { w: 480, h: 480 },
|
||||
};
|
||||
}
|
||||
embeddedLinkCache.set(originalLink, ret);
|
||||
|
@ -168,14 +180,14 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
|
|||
}
|
||||
|
||||
if (RE_GH_GIST.test(link)) {
|
||||
let ret: EmbeddedLink;
|
||||
let ret: IframeData;
|
||||
// assume embed code
|
||||
if (/<script>/.test(link)) {
|
||||
const srcDoc = createSrcDoc(link);
|
||||
ret = {
|
||||
type: "document",
|
||||
srcdoc: () => srcDoc,
|
||||
aspectRatio: { w: 550, h: 720 },
|
||||
intrinsicSize: { w: 550, h: 720 },
|
||||
};
|
||||
// assume regular url
|
||||
} else {
|
||||
|
@ -190,26 +202,26 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
|
|||
.gist .gist-file { height: calc(100vh - 2px); padding: 0px; display: grid; grid-template-rows: 1fr auto; }
|
||||
</style>
|
||||
`),
|
||||
aspectRatio: { w: 550, h: 720 },
|
||||
intrinsicSize: { w: 550, h: 720 },
|
||||
};
|
||||
}
|
||||
embeddedLinkCache.set(link, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
embeddedLinkCache.set(link, { link, aspectRatio, type });
|
||||
return { link, aspectRatio, type };
|
||||
embeddedLinkCache.set(link, { link, intrinsicSize: aspectRatio, type });
|
||||
return { link, intrinsicSize: aspectRatio, type };
|
||||
};
|
||||
|
||||
export const isEmbeddableOrLabel = (
|
||||
export const isIframeLikeOrItsLabel = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
): Boolean => {
|
||||
if (isEmbeddableElement(element)) {
|
||||
if (isIframeLikeElement(element)) {
|
||||
return true;
|
||||
}
|
||||
if (element.type === "text") {
|
||||
const container = getContainerElement(element);
|
||||
if (container && isEmbeddableElement(container)) {
|
||||
if (container && isFrameLikeElement(container)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -217,10 +229,16 @@ export const isEmbeddableOrLabel = (
|
|||
};
|
||||
|
||||
export const createPlaceholderEmbeddableLabel = (
|
||||
element: ExcalidrawEmbeddableElement,
|
||||
element: ExcalidrawIframeLikeElement,
|
||||
): ExcalidrawElement => {
|
||||
const text =
|
||||
!element.link || element?.link === "" ? "Empty Web-Embed" : element.link;
|
||||
let text: string;
|
||||
if (isIframeElement(element)) {
|
||||
text = "IFrame element";
|
||||
} else {
|
||||
text =
|
||||
!element.link || element?.link === "" ? "Empty Web-Embed" : element.link;
|
||||
}
|
||||
|
||||
const fontSize = Math.max(
|
||||
Math.min(element.width / 2, element.width / text.length),
|
||||
element.width / 30,
|
||||
|
|
|
@ -2,7 +2,6 @@ import {
|
|||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeleted,
|
||||
ExcalidrawFrameElement,
|
||||
} from "./types";
|
||||
import { isInvisiblySmallElement } from "./sizeHelpers";
|
||||
import { isLinearElementType } from "./typeChecks";
|
||||
|
@ -50,11 +49,7 @@ export {
|
|||
getDragOffsetXY,
|
||||
dragNewElement,
|
||||
} from "./dragElements";
|
||||
export {
|
||||
isTextElement,
|
||||
isExcalidrawElement,
|
||||
isFrameElement,
|
||||
} from "./typeChecks";
|
||||
export { isTextElement, isExcalidrawElement } from "./typeChecks";
|
||||
export { textWysiwyg } from "./textWysiwyg";
|
||||
export { redrawTextBoundingBox } from "./textElement";
|
||||
export {
|
||||
|
@ -74,17 +69,10 @@ export const getVisibleElements = (elements: readonly ExcalidrawElement[]) =>
|
|||
(el) => !el.isDeleted && !isInvisiblySmallElement(el),
|
||||
) as readonly NonDeletedExcalidrawElement[];
|
||||
|
||||
export const getNonDeletedElements = (elements: readonly ExcalidrawElement[]) =>
|
||||
elements.filter(
|
||||
(element) => !element.isDeleted,
|
||||
) as readonly NonDeletedExcalidrawElement[];
|
||||
|
||||
export const getNonDeletedFrames = (
|
||||
frames: readonly ExcalidrawFrameElement[],
|
||||
export const getNonDeletedElements = <T extends ExcalidrawElement>(
|
||||
elements: readonly T[],
|
||||
) =>
|
||||
frames.filter(
|
||||
(frame) => !frame.isDeleted,
|
||||
) as readonly NonDeleted<ExcalidrawFrameElement>[];
|
||||
elements.filter((element) => !element.isDeleted) as readonly NonDeleted<T>[];
|
||||
|
||||
export const isNonDeletedElement = <T extends ExcalidrawElement>(
|
||||
element: T,
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
ExcalidrawTextContainer,
|
||||
ExcalidrawFrameElement,
|
||||
ExcalidrawEmbeddableElement,
|
||||
ExcalidrawMagicFrameElement,
|
||||
ExcalidrawIframeElement,
|
||||
} from "../element/types";
|
||||
import {
|
||||
arrayToMap,
|
||||
|
@ -143,6 +145,16 @@ export const newEmbeddableElement = (
|
|||
};
|
||||
};
|
||||
|
||||
export const newIframeElement = (
|
||||
opts: {
|
||||
type: "iframe";
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawIframeElement> => {
|
||||
return {
|
||||
..._newElementBase<ExcalidrawIframeElement>("iframe", opts),
|
||||
};
|
||||
};
|
||||
|
||||
export const newFrameElement = (
|
||||
opts: {
|
||||
name?: string;
|
||||
|
@ -160,6 +172,23 @@ export const newFrameElement = (
|
|||
return frameElement;
|
||||
};
|
||||
|
||||
export const newMagicFrameElement = (
|
||||
opts: {
|
||||
name?: string;
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawMagicFrameElement> => {
|
||||
const frameElement = newElementWith(
|
||||
{
|
||||
..._newElementBase<ExcalidrawMagicFrameElement>("magicframe", opts),
|
||||
type: "magicframe",
|
||||
name: opts?.name || null,
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return frameElement;
|
||||
};
|
||||
|
||||
/** computes element x/y offset based on textAlign/verticalAlign */
|
||||
const getTextElementPositionOffsets = (
|
||||
opts: {
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
import {
|
||||
isArrowElement,
|
||||
isBoundToContainer,
|
||||
isFrameElement,
|
||||
isFrameLikeElement,
|
||||
isFreeDrawElement,
|
||||
isImageElement,
|
||||
isLinearElement,
|
||||
|
@ -163,7 +163,7 @@ const rotateSingleElement = (
|
|||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
let angle: number;
|
||||
if (isFrameElement(element)) {
|
||||
if (isFrameLikeElement(element)) {
|
||||
angle = 0;
|
||||
} else {
|
||||
angle = (5 * Math.PI) / 2 + Math.atan2(pointerY - cy, pointerX - cx);
|
||||
|
@ -900,7 +900,7 @@ const rotateMultipleElements = (
|
|||
}
|
||||
|
||||
elements
|
||||
.filter((element) => element.type !== "frame")
|
||||
.filter((element) => !isFrameLikeElement(element))
|
||||
.forEach((element) => {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawElementType,
|
||||
ExcalidrawTextContainer,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
|
@ -867,7 +868,7 @@ const VALID_CONTAINER_TYPES = new Set([
|
|||
]);
|
||||
|
||||
export const isValidTextContainer = (element: {
|
||||
type: ExcalidrawElement["type"];
|
||||
type: ExcalidrawElementType;
|
||||
}) => VALID_CONTAINER_TYPES.has(element.type);
|
||||
|
||||
export const computeContainerDimensionForBoundText = (
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Bounds, getElementAbsoluteCoords } from "./bounds";
|
|||
import { rotate } from "../math";
|
||||
import { InteractiveCanvasAppState, Zoom } from "../types";
|
||||
import { isTextElement } from ".";
|
||||
import { isFrameElement, isLinearElement } from "./typeChecks";
|
||||
import { isFrameLikeElement, isLinearElement } from "./typeChecks";
|
||||
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
||||
|
||||
export type TransformHandleDirection =
|
||||
|
@ -257,7 +257,7 @@ export const getTransformHandles = (
|
|||
}
|
||||
} else if (isTextElement(element)) {
|
||||
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
|
||||
} else if (isFrameElement(element)) {
|
||||
} else if (isFrameLikeElement(element)) {
|
||||
omitSides = {
|
||||
rotation: true,
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ROUNDNESS } from "../constants";
|
||||
import { AppState } from "../types";
|
||||
import { ElementOrToolType } from "../types";
|
||||
import { MarkNonNullable } from "../utility-types";
|
||||
import { assertNever } from "../utils";
|
||||
import {
|
||||
|
@ -8,7 +8,6 @@ import {
|
|||
ExcalidrawEmbeddableElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawGenericElement,
|
||||
ExcalidrawFreeDrawElement,
|
||||
InitializedExcalidrawImageElement,
|
||||
ExcalidrawImageElement,
|
||||
|
@ -16,21 +15,13 @@ import {
|
|||
ExcalidrawTextContainer,
|
||||
ExcalidrawFrameElement,
|
||||
RoundnessType,
|
||||
ExcalidrawFrameLikeElement,
|
||||
ExcalidrawElementType,
|
||||
ExcalidrawIframeElement,
|
||||
ExcalidrawIframeLikeElement,
|
||||
ExcalidrawMagicFrameElement,
|
||||
} from "./types";
|
||||
|
||||
export const isGenericElement = (
|
||||
element: ExcalidrawElement | null,
|
||||
): element is ExcalidrawGenericElement => {
|
||||
return (
|
||||
element != null &&
|
||||
(element.type === "selection" ||
|
||||
element.type === "rectangle" ||
|
||||
element.type === "diamond" ||
|
||||
element.type === "ellipse" ||
|
||||
element.type === "embeddable")
|
||||
);
|
||||
};
|
||||
|
||||
export const isInitializedImageElement = (
|
||||
element: ExcalidrawElement | null,
|
||||
): element is InitializedExcalidrawImageElement => {
|
||||
|
@ -49,6 +40,20 @@ export const isEmbeddableElement = (
|
|||
return !!element && element.type === "embeddable";
|
||||
};
|
||||
|
||||
export const isIframeElement = (
|
||||
element: ExcalidrawElement | null,
|
||||
): element is ExcalidrawIframeElement => {
|
||||
return !!element && element.type === "iframe";
|
||||
};
|
||||
|
||||
export const isIframeLikeElement = (
|
||||
element: ExcalidrawElement | null,
|
||||
): element is ExcalidrawIframeLikeElement => {
|
||||
return (
|
||||
!!element && (element.type === "iframe" || element.type === "embeddable")
|
||||
);
|
||||
};
|
||||
|
||||
export const isTextElement = (
|
||||
element: ExcalidrawElement | null,
|
||||
): element is ExcalidrawTextElement => {
|
||||
|
@ -61,6 +66,21 @@ export const isFrameElement = (
|
|||
return element != null && element.type === "frame";
|
||||
};
|
||||
|
||||
export const isMagicFrameElement = (
|
||||
element: ExcalidrawElement | null,
|
||||
): element is ExcalidrawMagicFrameElement => {
|
||||
return element != null && element.type === "magicframe";
|
||||
};
|
||||
|
||||
export const isFrameLikeElement = (
|
||||
element: ExcalidrawElement | null,
|
||||
): element is ExcalidrawFrameLikeElement => {
|
||||
return (
|
||||
element != null &&
|
||||
(element.type === "frame" || element.type === "magicframe")
|
||||
);
|
||||
};
|
||||
|
||||
export const isFreeDrawElement = (
|
||||
element?: ExcalidrawElement | null,
|
||||
): element is ExcalidrawFreeDrawElement => {
|
||||
|
@ -68,7 +88,7 @@ export const isFreeDrawElement = (
|
|||
};
|
||||
|
||||
export const isFreeDrawElementType = (
|
||||
elementType: ExcalidrawElement["type"],
|
||||
elementType: ExcalidrawElementType,
|
||||
): boolean => {
|
||||
return elementType === "freedraw";
|
||||
};
|
||||
|
@ -86,7 +106,7 @@ export const isArrowElement = (
|
|||
};
|
||||
|
||||
export const isLinearElementType = (
|
||||
elementType: AppState["activeTool"]["type"],
|
||||
elementType: ElementOrToolType,
|
||||
): boolean => {
|
||||
return (
|
||||
elementType === "arrow" || elementType === "line" // || elementType === "freedraw"
|
||||
|
@ -105,7 +125,7 @@ export const isBindingElement = (
|
|||
};
|
||||
|
||||
export const isBindingElementType = (
|
||||
elementType: AppState["activeTool"]["type"],
|
||||
elementType: ElementOrToolType,
|
||||
): boolean => {
|
||||
return elementType === "arrow";
|
||||
};
|
||||
|
@ -121,8 +141,10 @@ export const isBindableElement = (
|
|||
element.type === "diamond" ||
|
||||
element.type === "ellipse" ||
|
||||
element.type === "image" ||
|
||||
element.type === "iframe" ||
|
||||
element.type === "embeddable" ||
|
||||
element.type === "frame" ||
|
||||
element.type === "magicframe" ||
|
||||
(element.type === "text" && !element.containerId))
|
||||
);
|
||||
};
|
||||
|
@ -144,7 +166,7 @@ export const isTextBindableContainer = (
|
|||
export const isExcalidrawElement = (
|
||||
element: any,
|
||||
): element is ExcalidrawElement => {
|
||||
const type: ExcalidrawElement["type"] | undefined = element?.type;
|
||||
const type: ExcalidrawElementType | undefined = element?.type;
|
||||
if (!type) {
|
||||
return false;
|
||||
}
|
||||
|
@ -152,12 +174,14 @@ export const isExcalidrawElement = (
|
|||
case "text":
|
||||
case "diamond":
|
||||
case "rectangle":
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "ellipse":
|
||||
case "arrow":
|
||||
case "freedraw":
|
||||
case "line":
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
case "image":
|
||||
case "selection": {
|
||||
return true;
|
||||
|
@ -190,7 +214,7 @@ export const isBoundToContainer = (
|
|||
};
|
||||
|
||||
export const isUsingAdaptiveRadius = (type: string) =>
|
||||
type === "rectangle" || type === "embeddable";
|
||||
type === "rectangle" || type === "embeddable" || type === "iframe";
|
||||
|
||||
export const isUsingProportionalRadius = (type: string) =>
|
||||
type === "line" || type === "arrow" || type === "diamond";
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
VERTICAL_ALIGN,
|
||||
} from "../constants";
|
||||
import { MarkNonNullable, ValueOf } from "../utility-types";
|
||||
import { MagicCacheData } from "../data/magic";
|
||||
|
||||
export type ChartType = "bar" | "line";
|
||||
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
|
||||
|
@ -97,6 +98,26 @@ export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase &
|
|||
validated: boolean | null;
|
||||
}>;
|
||||
|
||||
export type ExcalidrawIframeElement = _ExcalidrawElementBase &
|
||||
Readonly<{
|
||||
type: "iframe";
|
||||
// TODO move later to AI-specific frame
|
||||
customData?: { generationData?: MagicCacheData };
|
||||
}>;
|
||||
|
||||
export type ExcalidrawIframeLikeElement =
|
||||
| ExcalidrawIframeElement
|
||||
| ExcalidrawEmbeddableElement;
|
||||
|
||||
export type IframeData =
|
||||
| {
|
||||
intrinsicSize: { w: number; h: number };
|
||||
warning?: string;
|
||||
} & (
|
||||
| { type: "video" | "generic"; link: string }
|
||||
| { type: "document"; srcdoc: (theme: Theme) => string }
|
||||
);
|
||||
|
||||
export type ExcalidrawImageElement = _ExcalidrawElementBase &
|
||||
Readonly<{
|
||||
type: "image";
|
||||
|
@ -117,6 +138,15 @@ export type ExcalidrawFrameElement = _ExcalidrawElementBase & {
|
|||
name: string | null;
|
||||
};
|
||||
|
||||
export type ExcalidrawMagicFrameElement = _ExcalidrawElementBase & {
|
||||
type: "magicframe";
|
||||
name: string | null;
|
||||
};
|
||||
|
||||
export type ExcalidrawFrameLikeElement =
|
||||
| ExcalidrawFrameElement
|
||||
| ExcalidrawMagicFrameElement;
|
||||
|
||||
/**
|
||||
* These are elements that don't have any additional properties.
|
||||
*/
|
||||
|
@ -138,6 +168,8 @@ export type ExcalidrawElement =
|
|||
| ExcalidrawFreeDrawElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawFrameElement
|
||||
| ExcalidrawMagicFrameElement
|
||||
| ExcalidrawIframeElement
|
||||
| ExcalidrawEmbeddableElement;
|
||||
|
||||
export type NonDeleted<TElement extends ExcalidrawElement> = TElement & {
|
||||
|
@ -170,8 +202,10 @@ export type ExcalidrawBindableElement =
|
|||
| ExcalidrawEllipseElement
|
||||
| ExcalidrawTextElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawIframeElement
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawFrameElement;
|
||||
| ExcalidrawFrameElement
|
||||
| ExcalidrawMagicFrameElement;
|
||||
|
||||
export type ExcalidrawTextContainer =
|
||||
| ExcalidrawRectangleElement
|
||||
|
@ -217,3 +251,5 @@ export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
|
|||
}>;
|
||||
|
||||
export type FileId = string & { _brand: "FileId" };
|
||||
|
||||
export type ExcalidrawElementType = ExcalidrawElement["type"];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue