feat: wireframe-to-code (#7334)
This commit is contained in:
+14
-17
@@ -2,15 +2,11 @@ import {
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeleted,
|
||||
ExcalidrawFrameElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
} from "../element/types";
|
||||
import {
|
||||
getNonDeletedElements,
|
||||
getNonDeletedFrames,
|
||||
isNonDeletedElement,
|
||||
} from "../element";
|
||||
import { getNonDeletedElements, isNonDeletedElement } from "../element";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { isFrameElement } from "../element/typeChecks";
|
||||
import { isFrameLikeElement } from "../element/typeChecks";
|
||||
import { getSelectedElements } from "./selection";
|
||||
import { AppState } from "../types";
|
||||
import { Assert, SameType } from "../utility-types";
|
||||
@@ -107,8 +103,9 @@ class Scene {
|
||||
|
||||
private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
|
||||
private elements: readonly ExcalidrawElement[] = [];
|
||||
private nonDeletedFrames: readonly NonDeleted<ExcalidrawFrameElement>[] = [];
|
||||
private frames: readonly ExcalidrawFrameElement[] = [];
|
||||
private nonDeletedFramesLikes: readonly NonDeleted<ExcalidrawFrameLikeElement>[] =
|
||||
[];
|
||||
private frames: readonly ExcalidrawFrameLikeElement[] = [];
|
||||
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
|
||||
private selectedElementsCache: {
|
||||
selectedElementIds: AppState["selectedElementIds"] | null;
|
||||
@@ -179,8 +176,8 @@ class Scene {
|
||||
return selectedElements;
|
||||
}
|
||||
|
||||
getNonDeletedFrames(): readonly NonDeleted<ExcalidrawFrameElement>[] {
|
||||
return this.nonDeletedFrames;
|
||||
getNonDeletedFramesLikes(): readonly NonDeleted<ExcalidrawFrameLikeElement>[] {
|
||||
return this.nonDeletedFramesLikes;
|
||||
}
|
||||
|
||||
getElement<T extends ExcalidrawElement>(id: T["id"]): T | null {
|
||||
@@ -235,18 +232,18 @@ class Scene {
|
||||
mapElementIds = true,
|
||||
) {
|
||||
this.elements = nextElements;
|
||||
const nextFrames: ExcalidrawFrameElement[] = [];
|
||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||
this.elementsMap.clear();
|
||||
nextElements.forEach((element) => {
|
||||
if (isFrameElement(element)) {
|
||||
nextFrames.push(element);
|
||||
if (isFrameLikeElement(element)) {
|
||||
nextFrameLikes.push(element);
|
||||
}
|
||||
this.elementsMap.set(element.id, element);
|
||||
Scene.mapElementToScene(element, this);
|
||||
});
|
||||
this.nonDeletedElements = getNonDeletedElements(this.elements);
|
||||
this.frames = nextFrames;
|
||||
this.nonDeletedFrames = getNonDeletedFrames(this.frames);
|
||||
this.frames = nextFrameLikes;
|
||||
this.nonDeletedFramesLikes = getNonDeletedElements(this.frames);
|
||||
|
||||
this.informMutation();
|
||||
}
|
||||
@@ -277,7 +274,7 @@ class Scene {
|
||||
destroy() {
|
||||
this.nonDeletedElements = [];
|
||||
this.elements = [];
|
||||
this.nonDeletedFrames = [];
|
||||
this.nonDeletedFramesLikes = [];
|
||||
this.frames = [];
|
||||
this.elementsMap.clear();
|
||||
this.selectedElementsCache.selectedElementIds = null;
|
||||
|
||||
+24
-6
@@ -14,7 +14,12 @@ import { generateFreeDrawShape } from "../renderer/renderElement";
|
||||
import { isTransparent, assertNever } from "../utils";
|
||||
import { simplify } from "points-on-curve";
|
||||
import { ROUGHNESS } from "../constants";
|
||||
import { isLinearElement } from "../element/typeChecks";
|
||||
import {
|
||||
isEmbeddableElement,
|
||||
isIframeElement,
|
||||
isIframeLikeElement,
|
||||
isLinearElement,
|
||||
} from "../element/typeChecks";
|
||||
import { canChangeRoundness } from "./comparisons";
|
||||
|
||||
const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth];
|
||||
@@ -78,6 +83,7 @@ export const generateRoughOptions = (
|
||||
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "iframe":
|
||||
case "embeddable":
|
||||
case "diamond":
|
||||
case "ellipse": {
|
||||
@@ -109,13 +115,13 @@ export const generateRoughOptions = (
|
||||
}
|
||||
};
|
||||
|
||||
const modifyEmbeddableForRoughOptions = (
|
||||
const modifyIframeLikeForRoughOptions = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
isExporting: boolean,
|
||||
) => {
|
||||
if (
|
||||
element.type === "embeddable" &&
|
||||
(isExporting || !element.validated) &&
|
||||
isIframeLikeElement(element) &&
|
||||
(isExporting || (isEmbeddableElement(element) && !element.validated)) &&
|
||||
isTransparent(element.backgroundColor) &&
|
||||
isTransparent(element.strokeColor)
|
||||
) {
|
||||
@@ -125,6 +131,16 @@ const modifyEmbeddableForRoughOptions = (
|
||||
backgroundColor: "#d3d3d3",
|
||||
fillStyle: "solid",
|
||||
} as const;
|
||||
} else if (isIframeElement(element)) {
|
||||
return {
|
||||
...element,
|
||||
strokeColor: isTransparent(element.strokeColor)
|
||||
? "#000000"
|
||||
: element.strokeColor,
|
||||
backgroundColor: isTransparent(element.backgroundColor)
|
||||
? "#f4f4f6"
|
||||
: element.backgroundColor,
|
||||
};
|
||||
}
|
||||
return element;
|
||||
};
|
||||
@@ -143,6 +159,7 @@ export const _generateElementShape = (
|
||||
): Drawable | Drawable[] | null => {
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "iframe":
|
||||
case "embeddable": {
|
||||
let shape: ElementShapes[typeof element.type];
|
||||
// this is for rendering the stroke/bg of the embeddable, especially
|
||||
@@ -159,7 +176,7 @@ export const _generateElementShape = (
|
||||
h - r
|
||||
} L 0 ${r} Q 0 0, ${r} 0`,
|
||||
generateRoughOptions(
|
||||
modifyEmbeddableForRoughOptions(element, isExporting),
|
||||
modifyIframeLikeForRoughOptions(element, isExporting),
|
||||
true,
|
||||
),
|
||||
);
|
||||
@@ -170,7 +187,7 @@ export const _generateElementShape = (
|
||||
element.width,
|
||||
element.height,
|
||||
generateRoughOptions(
|
||||
modifyEmbeddableForRoughOptions(element, isExporting),
|
||||
modifyIframeLikeForRoughOptions(element, isExporting),
|
||||
false,
|
||||
),
|
||||
);
|
||||
@@ -373,6 +390,7 @@ export const _generateElementShape = (
|
||||
return shape;
|
||||
}
|
||||
case "frame":
|
||||
case "magicframe":
|
||||
case "text":
|
||||
case "image": {
|
||||
const shape: ElementShapes[typeof element.type] = null;
|
||||
|
||||
+18
-13
@@ -1,22 +1,25 @@
|
||||
import { isEmbeddableElement } from "../element/typeChecks";
|
||||
import { isIframeElement } from "../element/typeChecks";
|
||||
import {
|
||||
ExcalidrawEmbeddableElement,
|
||||
ExcalidrawIframeElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { ElementOrToolType } from "../types";
|
||||
|
||||
export const hasBackground = (type: string) =>
|
||||
export const hasBackground = (type: ElementOrToolType) =>
|
||||
type === "rectangle" ||
|
||||
type === "iframe" ||
|
||||
type === "embeddable" ||
|
||||
type === "ellipse" ||
|
||||
type === "diamond" ||
|
||||
type === "line" ||
|
||||
type === "freedraw";
|
||||
|
||||
export const hasStrokeColor = (type: string) =>
|
||||
type !== "image" && type !== "frame";
|
||||
export const hasStrokeColor = (type: ElementOrToolType) =>
|
||||
type !== "image" && type !== "frame" && type !== "magicframe";
|
||||
|
||||
export const hasStrokeWidth = (type: string) =>
|
||||
export const hasStrokeWidth = (type: ElementOrToolType) =>
|
||||
type === "rectangle" ||
|
||||
type === "iframe" ||
|
||||
type === "embeddable" ||
|
||||
type === "ellipse" ||
|
||||
type === "diamond" ||
|
||||
@@ -24,22 +27,24 @@ export const hasStrokeWidth = (type: string) =>
|
||||
type === "arrow" ||
|
||||
type === "line";
|
||||
|
||||
export const hasStrokeStyle = (type: string) =>
|
||||
export const hasStrokeStyle = (type: ElementOrToolType) =>
|
||||
type === "rectangle" ||
|
||||
type === "iframe" ||
|
||||
type === "embeddable" ||
|
||||
type === "ellipse" ||
|
||||
type === "diamond" ||
|
||||
type === "arrow" ||
|
||||
type === "line";
|
||||
|
||||
export const canChangeRoundness = (type: string) =>
|
||||
export const canChangeRoundness = (type: ElementOrToolType) =>
|
||||
type === "rectangle" ||
|
||||
type === "iframe" ||
|
||||
type === "embeddable" ||
|
||||
type === "arrow" ||
|
||||
type === "line" ||
|
||||
type === "diamond";
|
||||
|
||||
export const canHaveArrowheads = (type: string) => type === "arrow";
|
||||
export const canHaveArrowheads = (type: ElementOrToolType) => type === "arrow";
|
||||
|
||||
export const getElementAtPosition = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
@@ -67,7 +72,7 @@ export const getElementsAtPosition = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
isAtPositionFn: (element: NonDeletedExcalidrawElement) => boolean,
|
||||
) => {
|
||||
const embeddables: ExcalidrawEmbeddableElement[] = [];
|
||||
const iframeLikes: ExcalidrawIframeElement[] = [];
|
||||
// The parameter elements comes ordered from lower z-index to higher.
|
||||
// We want to preserve that order on the returned array.
|
||||
// Exception being embeddables which should be on top of everything else in
|
||||
@@ -75,13 +80,13 @@ export const getElementsAtPosition = (
|
||||
const elsAtPos = elements.filter((element) => {
|
||||
const hit = !element.isDeleted && isAtPositionFn(element);
|
||||
if (hit) {
|
||||
if (isEmbeddableElement(element)) {
|
||||
embeddables.push(element);
|
||||
if (isIframeElement(element)) {
|
||||
iframeLikes.push(element);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return elsAtPos.concat(embeddables);
|
||||
return elsAtPos.concat(iframeLikes);
|
||||
};
|
||||
|
||||
+25
-12
@@ -1,7 +1,7 @@
|
||||
import rough from "roughjs/bin/rough";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFrameElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
ExcalidrawTextElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
@@ -27,11 +27,16 @@ import {
|
||||
updateImageCache,
|
||||
} from "../element/image";
|
||||
import { elementsOverlappingBBox } from "../packages/withinBounds";
|
||||
import { getFrameElements, getRootElements } from "../frame";
|
||||
import { isFrameElement, newTextElement } from "../element";
|
||||
import {
|
||||
getFrameLikeElements,
|
||||
getFrameLikeTitle,
|
||||
getRootElements,
|
||||
} from "../frame";
|
||||
import { newTextElement } from "../element";
|
||||
import { Mutable } from "../utility-types";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import Scene from "./Scene";
|
||||
import { isFrameElement, isFrameLikeElement } from "../element/typeChecks";
|
||||
|
||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||
|
||||
@@ -100,10 +105,15 @@ const addFrameLabelsAsTextElements = (
|
||||
opts: Pick<AppState, "exportWithDarkMode">,
|
||||
) => {
|
||||
const nextElements: NonDeletedExcalidrawElement[] = [];
|
||||
let frameIdx = 0;
|
||||
let frameIndex = 0;
|
||||
let magicFrameIndex = 0;
|
||||
for (const element of elements) {
|
||||
if (isFrameElement(element)) {
|
||||
frameIdx++;
|
||||
if (isFrameLikeElement(element)) {
|
||||
if (isFrameElement(element)) {
|
||||
frameIndex++;
|
||||
} else {
|
||||
magicFrameIndex++;
|
||||
}
|
||||
let textElement: Mutable<ExcalidrawTextElement> = newTextElement({
|
||||
x: element.x,
|
||||
y: element.y - FRAME_STYLE.nameOffsetY,
|
||||
@@ -114,7 +124,10 @@ const addFrameLabelsAsTextElements = (
|
||||
strokeColor: opts.exportWithDarkMode
|
||||
? FRAME_STYLE.nameColorDarkTheme
|
||||
: FRAME_STYLE.nameColorLightTheme,
|
||||
text: element.name || `Frame ${frameIdx}`,
|
||||
text: getFrameLikeTitle(
|
||||
element,
|
||||
isFrameElement(element) ? frameIndex : magicFrameIndex,
|
||||
),
|
||||
});
|
||||
textElement.y -= textElement.height;
|
||||
|
||||
@@ -129,7 +142,7 @@ const addFrameLabelsAsTextElements = (
|
||||
};
|
||||
|
||||
const getFrameRenderingConfig = (
|
||||
exportingFrame: ExcalidrawFrameElement | null,
|
||||
exportingFrame: ExcalidrawFrameLikeElement | null,
|
||||
frameRendering: AppState["frameRendering"] | null,
|
||||
): AppState["frameRendering"] => {
|
||||
frameRendering = frameRendering || getDefaultAppState().frameRendering;
|
||||
@@ -148,7 +161,7 @@ const prepareElementsForRender = ({
|
||||
exportWithDarkMode,
|
||||
}: {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
exportingFrame: ExcalidrawFrameElement | null | undefined;
|
||||
exportingFrame: ExcalidrawFrameLikeElement | null | undefined;
|
||||
frameRendering: AppState["frameRendering"];
|
||||
exportWithDarkMode: AppState["exportWithDarkMode"];
|
||||
}) => {
|
||||
@@ -184,7 +197,7 @@ export const exportToCanvas = async (
|
||||
exportBackground: boolean;
|
||||
exportPadding?: number;
|
||||
viewBackgroundColor: string;
|
||||
exportingFrame?: ExcalidrawFrameElement | null;
|
||||
exportingFrame?: ExcalidrawFrameLikeElement | null;
|
||||
},
|
||||
createCanvas: (
|
||||
width: number,
|
||||
@@ -274,7 +287,7 @@ export const exportToSvg = async (
|
||||
files: BinaryFiles | null,
|
||||
opts?: {
|
||||
renderEmbeddables?: boolean;
|
||||
exportingFrame?: ExcalidrawFrameElement | null;
|
||||
exportingFrame?: ExcalidrawFrameLikeElement | null;
|
||||
},
|
||||
): Promise<SVGSVGElement> => {
|
||||
const tempScene = __createSceneForElementsHack__(elements);
|
||||
@@ -360,7 +373,7 @@ export const exportToSvg = async (
|
||||
const offsetX = -minX + exportPadding;
|
||||
const offsetY = -minY + exportPadding;
|
||||
|
||||
const frameElements = getFrameElements(elements);
|
||||
const frameElements = getFrameLikeElements(elements);
|
||||
|
||||
let exportingFrameClipPath = "";
|
||||
for (const frame of frameElements) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from "../element/types";
|
||||
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
||||
import { AppState, InteractiveCanvasAppState } from "../types";
|
||||
import { isBoundToContainer } from "../element/typeChecks";
|
||||
import { isBoundToContainer, isFrameLikeElement } from "../element/typeChecks";
|
||||
import {
|
||||
elementOverlapsWithFrame,
|
||||
getContainingFrame,
|
||||
@@ -27,7 +27,7 @@ export const excludeElementsInFramesFromSelection = <
|
||||
const framesInSelection = new Set<T["id"]>();
|
||||
|
||||
selectedElements.forEach((element) => {
|
||||
if (element.type === "frame") {
|
||||
if (isFrameLikeElement(element)) {
|
||||
framesInSelection.add(element.id);
|
||||
}
|
||||
});
|
||||
@@ -190,7 +190,7 @@ export const getSelectedElements = (
|
||||
if (opts?.includeElementsInFrames) {
|
||||
const elementsToInclude: ExcalidrawElement[] = [];
|
||||
selectedElements.forEach((element) => {
|
||||
if (element.type === "frame") {
|
||||
if (isFrameLikeElement(element)) {
|
||||
getFrameChildren(elements, element.id).forEach((e) =>
|
||||
elementsToInclude.push(e),
|
||||
);
|
||||
|
||||
@@ -98,6 +98,7 @@ export type ElementShapes = {
|
||||
rectangle: Drawable;
|
||||
ellipse: Drawable;
|
||||
diamond: Drawable;
|
||||
iframe: Drawable;
|
||||
embeddable: Drawable;
|
||||
freedraw: Drawable | null;
|
||||
arrow: Drawable[];
|
||||
@@ -105,4 +106,5 @@ export type ElementShapes = {
|
||||
text: null;
|
||||
image: null;
|
||||
frame: null;
|
||||
magicframe: null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user