feat: wireframe-to-code (#7334)

This commit is contained in:
David Luzar
2023-11-23 23:07:53 +01:00
committed by GitHub
parent d1e4421823
commit c7ee46e7f8
63 changed files with 2106 additions and 444 deletions
+14 -17
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+3 -3
View File
@@ -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),
);
+2
View File
@@ -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;
};