diff --git a/packages/core/src/renderer/Renderer.ts b/packages/core/src/renderer/Renderer.ts index 2219c2ca58..d2a5791971 100644 --- a/packages/core/src/renderer/Renderer.ts +++ b/packages/core/src/renderer/Renderer.ts @@ -185,6 +185,7 @@ export const RenderStatic = async ( svg.setAttribute("version", "1.2"); svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); svg.setAttribute("viewBox", `0 0 ${canvas.width} ${canvas.height}`); + return Promise.all( computeShapes(varyingValues).map((shape) => RenderShape({ diff --git a/packages/editor/package.json b/packages/editor/package.json index fdb4af6f98..ff82168b6b 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -57,6 +57,7 @@ "react": "^18.0.0", "react-data-table-component": "^6.11.7", "react-dom": "^18.0.0", + "react-drag-drop-files": "^2.3.8", "react-hot-toast": "^2.2.0", "react-inspector": "^4.0.1", "react-responsive": "^9.0.0", diff --git a/packages/editor/src/App.tsx b/packages/editor/src/App.tsx index e0a894fa09..c0abb49d18 100644 --- a/packages/editor/src/App.tsx +++ b/packages/editor/src/App.tsx @@ -23,6 +23,7 @@ import RogerPanel from "./components/RogerPanel"; import SavedFilesBrowser from "./components/SavedBrowser"; import Settings from "./components/Settings"; import StateInspector from "./components/StateInspector"; +import SvgUploader from "./components/SvgUploader"; import TopBar from "./components/TopBar"; import { currentRogerState, @@ -112,6 +113,11 @@ export const layoutModel = Model.fromJson({ name: "examples", component: "examplesPanel", }, + { + type: "tab", + name: "upload", + component: "svgUploader", + }, { type: "tab", name: "settings", @@ -152,6 +158,8 @@ function App() { switch (node.getComponent()) { case "programEditor": return ; + case "svgUploader": + return ; case "diagram": return ; case "savedFiles": diff --git a/packages/editor/src/components/DiagramPanel.tsx b/packages/editor/src/components/DiagramPanel.tsx index 19cd6252f8..df2965ae61 100644 --- a/packages/editor/src/components/DiagramPanel.tsx +++ b/packages/editor/src/components/DiagramPanel.tsx @@ -13,8 +13,11 @@ import { useRecoilCallback, useRecoilState, useRecoilValue } from "recoil"; import { v4 as uuid } from "uuid"; import { currentRogerState, + DiagramMetadata, diagramMetadataSelector, diagramState, + fileContentsSelector, + ProgramFile, WorkspaceMetadata, workspaceMetadataSelector, } from "../state/atoms"; @@ -27,8 +30,14 @@ import BlueButton from "./BlueButton"; */ export const DownloadSVG = ( svg: SVGSVGElement, - title = "illustration" + title = "illustration", + dslStr: string, + subStr: string, + styleStr: string, + versionStr: string, + variationStr: string ): void => { + SVGaddCode(svg, dslStr, subStr, styleStr, versionStr, variationStr); const blob = new Blob([svg.outerHTML], { type: "image/svg+xml;charset=utf-8", }); @@ -41,6 +50,76 @@ export const DownloadSVG = ( document.body.removeChild(downloadLink); }; +/** + * Given an SVG, program triple, and version and variation strings, + * appends penrose tags to the SVG so the SVG can be reuploaded and edited. + * + * @param svg + * @param dslStr the domain file + * @param subStr the substance file + * @param styleStr the style file + * @param versionStr + * @param variationStr + */ +const SVGaddCode = ( + svg: SVGSVGElement, + dslStr: string, + subStr: string, + styleStr: string, + versionStr: string, + variationStr: string +): void => { + svg.setAttribute("penrose", "0"); + + // Create custom tag to store metadata + const metadata = document.createElementNS( + "https://penrose.cs.cmu.edu/metadata", + "penrose" + ); + + // Create tag for penrose version + const version = document.createElementNS( + "https://penrose.cs.cmu.edu/version", + "version" + ); + version.insertAdjacentText("afterbegin", versionStr); + + // Create tag for variation string + const variation = document.createElementNS( + "https://penrose.cs.cmu.edu/variation", + "variation" + ); + variation.insertAdjacentText("afterbegin", variationStr); + + // Create tag to store substance (.sub) code + const substance = document.createElementNS( + "https://penrose.cs.cmu.edu/substance", + "sub" + ); + substance.insertAdjacentText("afterbegin", subStr); + + // Create tag to store style (.sty) code + const style = document.createElementNS( + "https://penrose.cs.cmu.edu/style", + "sty" + ); + style.insertAdjacentText("afterbegin", styleStr); + + // Create tag to store .dsl code + const dsl = document.createElementNS("https://penrose.cs.cmu.edu/dsl", "dsl"); + dsl.insertAdjacentText("afterbegin", dslStr); + + // Add these new tags under the metadata tag + metadata.appendChild(version); + metadata.appendChild(variation); + metadata.appendChild(substance); + metadata.appendChild(style); + metadata.appendChild(dsl); + + // Add the metadata tag to the parent tag + svg.appendChild(metadata); +}; + /** * (browser-only) Downloads any given exported PNG to the user's computer * @param svg @@ -167,7 +246,24 @@ export default function DiagramPanel() { if (svg !== null) { const metadata = snapshot.getLoadable(workspaceMetadataSelector) .contents as WorkspaceMetadata; - DownloadSVG(svg, metadata.name); + const diagram = snapshot.getLoadable(diagramMetadataSelector) + .contents as DiagramMetadata; + const domain = snapshot.getLoadable(fileContentsSelector("domain")) + .contents as ProgramFile; + const substance = snapshot.getLoadable( + fileContentsSelector("substance") + ).contents as ProgramFile; + const style = snapshot.getLoadable(fileContentsSelector("style")) + .contents as ProgramFile; + DownloadSVG( + svg, + metadata.name, + domain.contents, + substance.contents, + style.contents, + metadata.editorVersion.toString(), + diagram.variation + ); } } }); diff --git a/packages/editor/src/components/SvgUploader.tsx b/packages/editor/src/components/SvgUploader.tsx new file mode 100644 index 0000000000..42db655dfb --- /dev/null +++ b/packages/editor/src/components/SvgUploader.tsx @@ -0,0 +1,105 @@ +import { FileUploader } from "react-drag-drop-files"; +import toast from "react-hot-toast"; +import { useRecoilState } from "recoil"; +import { diagramMetadataSelector, fileContentsSelector } from "../state/atoms"; +import { useCompileDiagram } from "../state/callbacks"; + +export default function SvgUploader() { + const setDomain = useRecoilState(fileContentsSelector("domain"))[1]; + const setSubstance = useRecoilState(fileContentsSelector("substance"))[1]; + const setStyle = useRecoilState(fileContentsSelector("style"))[1]; + const [diagramMetadata, setDiagramMetadata] = useRecoilState( + diagramMetadataSelector + ); + const compileDiagram = useCompileDiagram(); + + const handleChange = (svg: File) => { + const reader = new FileReader(); + reader.readAsText(svg); + reader.onabort = () => console.log("file reading was aborted"); + reader.onerror = () => console.log("file reading has failed"); + reader.onload = async () => { + const svgText = reader.result?.toString(); + if (!svgText) { + return; + } + + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(svgText, "text/xml"); + const styElem = xmlDoc.getElementsByTagName("sty"); + const subElem = xmlDoc.getElementsByTagName("sub"); + const dslElem = xmlDoc.getElementsByTagName("dsl"); + const variationElem = xmlDoc.getElementsByTagName("variation"); + + if (variationElem.length === 0) { + toast.error( + "Could not load SVG. Make sure the SVG was exported from Penrose." + ); + return; + } + + setDiagramMetadata((metadata) => ({ + ...metadata, + variation: (variationElem[0].textContent ?? "").trim(), + })); + + if (styElem.length === 0) { + toast.error( + "Could not load SVG. Make sure the SVG was exported from Penrose." + ); + return; + } + + setStyle({ + name: "SVG import", + contents: (styElem[0].textContent ?? "").trim(), + }); + + if (subElem.length === 0) { + toast.error( + "Could not load SVG. Make sure the SVG was exported from Penrose." + ); + return; + } + + setSubstance({ + name: "SVG import", + contents: (subElem[0].textContent ?? "").trim(), + }); + + if (dslElem.length === 0) { + toast.error( + "Could not load SVG. Make sure the SVG was exported from Penrose." + ); + return; + } + + setDomain({ + name: "SVG import", + contents: (dslElem[0].textContent ?? "").trim(), + }); + + await compileDiagram(); + toast.success("Sucessfully uploaded SVG to editor"); + }; + }; + + return ( + +
+

+ Upload or drop a + Penrose exported SVG here +

+
+ + ); +} diff --git a/yarn.lock b/yarn.lock index 8809250332..6ea97c06f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19710,6 +19710,14 @@ react-dom@^18.0.0: loose-envify "^1.1.0" scheduler "^0.22.0" +react-drag-drop-files@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/react-drag-drop-files/-/react-drag-drop-files-2.3.8.tgz#909e78de96e3300d6b2bdca5fdb3c93140d09b7b" + integrity sha512-/tA1FEGhw6IwG5DlogYu0y3uLkyZ6np8cbzihW7Hy/k3b1T022T7zjn/4elZ6+M69GwnJnUUO0Utisu1xZFATA== + dependencies: + prop-types "^15.7.2" + styled-components "^5.3.0" + react-element-to-jsx-string@^14.3.4: version "14.3.4" resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.4.tgz#709125bc72f06800b68f9f4db485f2c7d31218a8" @@ -22025,6 +22033,22 @@ style-to-object@0.3.0, style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" +styled-components@^5.3.0: + version "5.3.6" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.6.tgz#27753c8c27c650bee9358e343fc927966bfd00d1" + integrity sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/traverse" "^7.4.5" + "@emotion/is-prop-valid" "^1.1.0" + "@emotion/stylis" "^0.8.4" + "@emotion/unitless" "^0.7.4" + babel-plugin-styled-components ">= 1.12.0" + css-to-react-native "^3.0.0" + hoist-non-react-statics "^3.0.0" + shallowequal "^1.1.0" + supports-color "^5.5.0" + styled-components@^5.3.5: version "5.3.5" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-5.3.5.tgz#a750a398d01f1ca73af16a241dec3da6deae5ec4"