├── .gitignore ├── assets ├── printed.jpeg ├── svg2solid.png ├── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── mstile-150x150.png │ ├── android-chrome-96x96.png │ ├── browserconfig.xml │ ├── site.webmanifest │ └── safari-pinned-tab.svg ├── svg2solid.svg ├── svg2solid-printable.svg └── svg2solid-url-printable.svg ├── .prettierrc ├── sass ├── _colors.scss ├── style.scss ├── _form.scss ├── _reset.scss ├── _layout.scss ├── _typography.scss └── _sizes.scss ├── README.md ├── Makefile ├── .github └── workflows │ └── build-and-deploy.yml ├── package.json ├── LICENSE └── src ├── setup-scene.js ├── render-svg.js ├── index.html ├── index.js └── example-svg.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.parcel-cache/ 3 | /dist/ -------------------------------------------------------------------------------- /assets/printed.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erkannt/svg2solid/HEAD/assets/printed.jpeg -------------------------------------------------------------------------------- /assets/svg2solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erkannt/svg2solid/HEAD/assets/svg2solid.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erkannt/svg2solid/HEAD/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /sass/_colors.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --black: #212529; 3 | --off-white: #f8f9fa; 4 | --link: #3b5bdb; 5 | } 6 | -------------------------------------------------------------------------------- /assets/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erkannt/svg2solid/HEAD/assets/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /assets/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erkannt/svg2solid/HEAD/assets/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /assets/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erkannt/svg2solid/HEAD/assets/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erkannt/svg2solid/HEAD/assets/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /assets/favicon/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erkannt/svg2solid/HEAD/assets/favicon/android-chrome-96x96.png -------------------------------------------------------------------------------- /sass/style.scss: -------------------------------------------------------------------------------- 1 | @use './reset'; 2 | @use './sizes'; 3 | @use './typography'; 4 | @use './colors'; 5 | @use './layout'; 6 | @use './form'; 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://github.com/erkannt/svg2solid/assets/19282025/65200d2f-2a2d-45d9-9f23-b912aabdc154) 2 | 3 | ![](assets/printed.jpeg) 4 | -------------------------------------------------------------------------------- /assets/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /assets/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/assets/favicon/android-chrome-96x96.png", 7 | "sizes": "96x96", 8 | "type": "image/png" 9 | } 10 | ], 11 | "theme_color": "#ffffff", 12 | "background_color": "#ffffff", 13 | "display": "standalone" 14 | } 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean dev format typescript watch-typescript build 2 | 3 | dev: node_modules 4 | npx parcel -p 8080 src/index.html 5 | 6 | node_modules: package.json package-lock.json 7 | npm install 8 | touch node_modules 9 | 10 | format: node_modules 11 | npx prettier --ignore-unknown --write '**' 12 | 13 | build: 14 | npx parcel build src/index.html --no-source-maps --public-url https://svg2solid.rknt.de 15 | 16 | clean: 17 | rm -rf node_modules 18 | rm -rf .parcel-cache 19 | rm -rf dist -------------------------------------------------------------------------------- /.github/workflows/build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: [push] 3 | permissions: 4 | contents: write 5 | jobs: 6 | build-and-deploy: 7 | concurrency: ci-${{ github.ref }} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 🛎️ 11 | uses: actions/checkout@v2 12 | 13 | - name: Install and Build 🔧 14 | run: | 15 | make format typescript build 16 | 17 | - name: Deploy 🚀 18 | uses: JamesIves/github-pages-deploy-action@v4.2.3 19 | with: 20 | branch: gh-pages 21 | folder: dist 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crop-and-rotate", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "@parcel/packager-raw-url": "^2.10.2", 9 | "@parcel/packager-xml": "^2.10.2", 10 | "@parcel/transformer-sass": "^2.10.2", 11 | "@parcel/transformer-webmanifest": "^2.10.2", 12 | "@parcel/transformer-xml": "^2.10.2", 13 | "buffer": "^6.0.3", 14 | "parcel": "^2.10.2", 15 | "prettier": "^3.0.3", 16 | "process": "^0.11.10" 17 | }, 18 | "dependencies": { 19 | "file-saver": "^2.0.5", 20 | "jszip": "^3.10.1", 21 | "three": "^0.159.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sass/_form.scss: -------------------------------------------------------------------------------- 1 | fieldset { 2 | padding: var(--space-xs); 3 | display: flex; 4 | flex-direction: column; 5 | 6 | ul { 7 | flex-grow: 1; 8 | list-style: none; 9 | padding: 0; 10 | margin-bottom: var(--space-l); 11 | } 12 | 13 | span { 14 | border: 1px solid var(--black); 15 | height: 1rem; 16 | width: 1rem; 17 | } 18 | 19 | li { 20 | display: flex; 21 | gap: var(--space-xs); 22 | text-transform: uppercase; 23 | font-family: monospace; 24 | align-items: center; 25 | margin-bottom: var(--space-2xs); 26 | 27 | label { 28 | margin: 0; 29 | font-weight: normal; 30 | } 31 | } 32 | label { 33 | font-weight: bold; 34 | } 35 | } 36 | 37 | #svgFile { 38 | max-width: 35ch; 39 | } 40 | 41 | label { 42 | display: block; 43 | margin-bottom: var(--space-2xs); 44 | } 45 | 46 | input { 47 | display: block; 48 | margin-bottom: var(--space-l); 49 | } 50 | 51 | input:last-child { 52 | margin-bottom: 0; 53 | } 54 | -------------------------------------------------------------------------------- /sass/_reset.scss: -------------------------------------------------------------------------------- 1 | /* 2 | taken from: https://www.joshwcomeau.com/css/custom-css-reset/ 3 | */ 4 | 5 | /* 6 | 1. Use a more-intuitive box-sizing model. 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | /* 14 | 2. Remove default margin 15 | */ 16 | * { 17 | margin: 0; 18 | } 19 | /* 20 | Typographic tweaks! 21 | 3. Add accessible line-height 22 | 4. Improve text rendering 23 | */ 24 | body { 25 | line-height: 1.5; 26 | -webkit-font-smoothing: antialiased; 27 | } 28 | /* 29 | 5. Improve media defaults 30 | */ 31 | img, 32 | picture, 33 | video, 34 | canvas, 35 | svg { 36 | display: block; 37 | max-width: 100%; 38 | } 39 | /* 40 | 6. Remove built-in form typography styles 41 | */ 42 | input, 43 | button, 44 | textarea, 45 | select { 46 | font: inherit; 47 | } 48 | /* 49 | 7. Avoid text overflows 50 | */ 51 | p, 52 | h1, 53 | h2, 54 | h3, 55 | h4, 56 | h5, 57 | h6 { 58 | overflow-wrap: break-word; 59 | } 60 | /* 61 | 8. Create a root stacking context 62 | */ 63 | #root, 64 | #__next { 65 | isolation: isolate; 66 | } 67 | -------------------------------------------------------------------------------- /sass/_layout.scss: -------------------------------------------------------------------------------- 1 | body { 2 | padding: var(--space-2xl) var(--space-s); 3 | margin: 0 auto; 4 | max-width: 1200px; 5 | min-height: 100vh; 6 | display: flex; 7 | flex-direction: column; 8 | 9 | main { 10 | flex-grow: 1; 11 | } 12 | } 13 | 14 | .application { 15 | display: flex; 16 | flex-wrap: wrap; 17 | gap: var(--space-m); 18 | margin-bottom: var(--space-2xl); 19 | } 20 | 21 | .application__inputs { 22 | border: 1px solid var(--black); 23 | width: 100%; 24 | } 25 | 26 | .application__rendering { 27 | border: 1px solid var(--black); 28 | width: 100%; 29 | min-height: 600px; 30 | } 31 | 32 | @media (min-width: 1000px) { 33 | .application { 34 | flex-wrap: nowrap; 35 | } 36 | 37 | .application__inputs { 38 | max-width: 20vw; 39 | } 40 | 41 | .application__rendering { 42 | min-width: 600px; 43 | } 44 | } 45 | 46 | .tips { 47 | ul { 48 | margin-bottom: var(--space-m); 49 | } 50 | margin-bottom: var(--space-m); 51 | } 52 | 53 | footer { 54 | border-top: 1px solid var(--black); 55 | padding-top: var(--space-xl); 56 | margin-top: var(--space-3xl); 57 | } 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Daniel Haarhoff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sass/_typography.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif; 3 | font-weight: normal; 4 | font-weight: normal; 5 | line-height: 1.6; 6 | color: var(--black); 7 | background-color: var(--off-white); 8 | 9 | h1, 10 | h2, 11 | h3, 12 | h4, 13 | h5, 14 | h6 { 15 | font-weight: bold; 16 | line-height: 1.1; 17 | } 18 | 19 | h1 { 20 | font-size: var(--step-5); 21 | margin-bottom: var(--space-l); 22 | } 23 | h2 { 24 | font-size: var(--step-4); 25 | margin-bottom: var(--space-m); 26 | } 27 | h3 { 28 | font-size: var(--step-3); 29 | margin-bottom: var(--space-s); 30 | } 31 | h4 { 32 | font-size: var(--step-2); 33 | margin-bottom: var(--space-xs); 34 | } 35 | h5 { 36 | font-size: var(--step-1); 37 | margin-bottom: var(--space-xs); 38 | } 39 | h6 { 40 | font-size: var(--step-1); 41 | margin-bottom: var(--space-xs); 42 | } 43 | 44 | a { 45 | color: var(--link); 46 | text-underline-offset: 0.1578em; 47 | 48 | &:hover { 49 | text-decoration-thickness: 2px; 50 | } 51 | } 52 | 53 | p { 54 | margin-bottom: var(--space-xs); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/setup-scene.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; 3 | 4 | export const setupScene = (container) => { 5 | const scene = new THREE.Scene(); 6 | const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); 7 | const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.01, 1e5); 8 | const ambientLight = new THREE.AmbientLight('#888888'); 9 | const pointLight = new THREE.PointLight('#ffffff', 2, 800); 10 | const controls = new OrbitControls(camera, renderer.domElement); 11 | const animate = () => { 12 | renderer.render(scene, camera); 13 | controls.update(); 14 | 15 | requestAnimationFrame(animate); 16 | }; 17 | 18 | renderer.setSize(container.offsetWidth, container.offsetHeight); 19 | scene.add(ambientLight, pointLight); 20 | camera.position.z = 50; 21 | camera.position.x = 50; 22 | camera.position.y = 50; 23 | controls.enablePan = true; 24 | 25 | container.append(renderer.domElement); 26 | window.addEventListener('resize', () => { 27 | renderer.setSize(container.offsetWidth, container.offsetHeight); 28 | camera.aspect = container.offsetWidth / container.offsetHeight; 29 | camera.updateProjectionMatrix(); 30 | renderer.setSize(window.innerWidth, window.innerHeight); 31 | }); 32 | animate(); 33 | 34 | return { scene, camera, controls }; 35 | }; 36 | -------------------------------------------------------------------------------- /assets/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/render-svg.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader'; 3 | 4 | const stokeMaterial = new THREE.LineBasicMaterial({ 5 | color: '#adb5bd', 6 | }); 7 | 8 | export const renderSVG = (svg) => { 9 | const defaultExtrusion = 1; 10 | const loader = new SVGLoader(); 11 | const svgData = loader.parse(svg); 12 | const svgGroup = new THREE.Group(); 13 | const updateMap = []; 14 | const byColor = new Map(); 15 | 16 | svgGroup.scale.y *= -1; 17 | svgData.paths.forEach((path) => { 18 | const shapes = SVGLoader.createShapes(path); 19 | 20 | shapes.forEach((shape) => { 21 | const meshGeometry = new THREE.ExtrudeGeometry(shape, { 22 | depth: defaultExtrusion, 23 | bevelEnabled: false, 24 | }); 25 | const linesGeometry = new THREE.EdgesGeometry(meshGeometry); 26 | const fillMaterial = new THREE.MeshBasicMaterial({ color: path.color }); 27 | const mesh = new THREE.Mesh(meshGeometry, fillMaterial); 28 | const lines = new THREE.LineSegments(linesGeometry, stokeMaterial); 29 | 30 | const colorHex = path.color.getHexString(); 31 | if (!byColor.has(colorHex)) { 32 | byColor.set(colorHex, [{ mesh, shape, lines, depth: defaultExtrusion }]); 33 | } else { 34 | byColor.get(colorHex).push({ mesh, shape, lines, depth: defaultExtrusion }); 35 | } 36 | 37 | updateMap.push({ shape, mesh, lines }); 38 | svgGroup.add(mesh, lines); 39 | }); 40 | }); 41 | 42 | const box = new THREE.Box3().setFromObject(svgGroup); 43 | const size = box.getSize(new THREE.Vector3()); 44 | const yOffset = size.y / -2; 45 | const xOffset = size.x / -2; 46 | 47 | svgGroup.children.forEach((item) => { 48 | item.position.x = xOffset; 49 | item.position.y = yOffset; 50 | }); 51 | svgGroup.rotateX(-Math.PI / 2); 52 | 53 | return { 54 | object: svgGroup, 55 | byColor, 56 | update(extrusion, colorHex) { 57 | console.log('>>>', colorHex, extrusion); 58 | const toUpdate = byColor.get(colorHex); 59 | console.log('>>>', toUpdate); 60 | toUpdate.forEach((updateDetails) => { 61 | const meshGeometry = new THREE.ExtrudeGeometry(updateDetails.shape, { 62 | depth: extrusion, 63 | bevelEnabled: false, 64 | }); 65 | const linesGeometry = new THREE.EdgesGeometry(meshGeometry); 66 | 67 | updateDetails.mesh.geometry.dispose(); 68 | updateDetails.lines.geometry.dispose(); 69 | updateDetails.mesh.geometry = meshGeometry; 70 | updateDetails.lines.geometry = linesGeometry; 71 | }); 72 | }, 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /sass/_sizes.scss: -------------------------------------------------------------------------------- 1 | /* @link https://utopia.fyi/type/calculator?c=320,16,1.2,1240,18,1.25,5,2,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12 */ 2 | 3 | :root { 4 | /* Step -2: 11.11px → 11.52px */ 5 | --step--2: clamp(0.6944rem, 0.6855rem + 0.0446vw, 0.72rem); 6 | 7 | /* Step -1: 13.33px → 14.40px */ 8 | --step--1: clamp(0.8331rem, 0.8099rem + 0.1163vw, 0.9rem); 9 | 10 | /* Step 0: 16.00px → 18.00px */ 11 | --step-0: clamp(1rem, 0.9565rem + 0.2174vw, 1.125rem); 12 | 13 | /* Step 1: 19.20px → 22.50px */ 14 | --step-1: clamp(1.2rem, 1.1283rem + 0.3587vw, 1.4063rem); 15 | 16 | /* Step 2: 23.04px → 28.13px */ 17 | --step-2: clamp(1.44rem, 1.3293rem + 0.5533vw, 1.7581rem); 18 | 19 | /* Step 3: 27.65px → 35.16px */ 20 | --step-3: clamp(1.7281rem, 1.5649rem + 0.8163vw, 2.1975rem); 21 | 22 | /* Step 4: 33.18px → 43.95px */ 23 | --step-4: clamp(2.0738rem, 1.8396rem + 1.1707vw, 2.7469rem); 24 | 25 | /* Step 5: 39.81px → 54.93px */ 26 | --step-5: clamp(2.4881rem, 2.1594rem + 1.6435vw, 3.4331rem); 27 | } 28 | 29 | /* @link https://utopia.fyi/space/calculator?c=320,16,1.2,1240,18,1.25,5,2,&s=0.75|0.5|0.25,1.5|2|3|4|6,&g=s,l,xl,12 */ 30 | 31 | :root { 32 | /* Space 3xs: 4px → 5px */ 33 | --space-3xs: clamp(0.25rem, 0.2283rem + 0.1087vw, 0.3125rem); 34 | 35 | /* Space 2xs: 8px → 9px */ 36 | --space-2xs: clamp(0.5rem, 0.4783rem + 0.1087vw, 0.5625rem); 37 | 38 | /* Space xs: 12px → 14px */ 39 | --space-xs: clamp(0.75rem, 0.7065rem + 0.2174vw, 0.875rem); 40 | 41 | /* Space s: 16px → 18px */ 42 | --space-s: clamp(1rem, 0.9565rem + 0.2174vw, 1.125rem); 43 | 44 | /* Space m: 24px → 27px */ 45 | --space-m: clamp(1.5rem, 1.4348rem + 0.3261vw, 1.6875rem); 46 | 47 | /* Space l: 32px → 36px */ 48 | --space-l: clamp(2rem, 1.913rem + 0.4348vw, 2.25rem); 49 | 50 | /* Space xl: 48px → 54px */ 51 | --space-xl: clamp(3rem, 2.8696rem + 0.6522vw, 3.375rem); 52 | 53 | /* Space 2xl: 64px → 72px */ 54 | --space-2xl: clamp(4rem, 3.8261rem + 0.8696vw, 4.5rem); 55 | 56 | /* Space 3xl: 96px → 108px */ 57 | --space-3xl: clamp(6rem, 5.7391rem + 1.3043vw, 6.75rem); 58 | 59 | /* One-up pairs */ 60 | /* Space 3xs-2xs: 4px → 9px */ 61 | --space-3xs-2xs: clamp(0.25rem, 0.1413rem + 0.5435vw, 0.5625rem); 62 | 63 | /* Space 2xs-xs: 8px → 14px */ 64 | --space-2xs-xs: clamp(0.5rem, 0.3696rem + 0.6522vw, 0.875rem); 65 | 66 | /* Space xs-s: 12px → 18px */ 67 | --space-xs-s: clamp(0.75rem, 0.6196rem + 0.6522vw, 1.125rem); 68 | 69 | /* Space s-m: 16px → 27px */ 70 | --space-s-m: clamp(1rem, 0.7609rem + 1.1957vw, 1.6875rem); 71 | 72 | /* Space m-l: 24px → 36px */ 73 | --space-m-l: clamp(1.5rem, 1.2391rem + 1.3043vw, 2.25rem); 74 | 75 | /* Space l-xl: 32px → 54px */ 76 | --space-l-xl: clamp(2rem, 1.5217rem + 2.3913vw, 3.375rem); 77 | 78 | /* Space xl-2xl: 48px → 72px */ 79 | --space-xl-2xl: clamp(3rem, 2.4783rem + 2.6087vw, 4.5rem); 80 | 81 | /* Space 2xl-3xl: 64px → 108px */ 82 | --space-2xl-3xl: clamp(4rem, 3.0435rem + 4.7826vw, 6.75rem); 83 | } 84 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | svg2solid 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |

SVG ➠ multicolour print

29 |
30 |
31 | 32 | 33 | 34 | 35 |
    36 | 37 | 38 |
    39 | 40 |
    41 |
    42 |
    43 |

    Tips

    44 |

    Make sure the various colours in the SVG are non-overlapping paths.

    45 |

    In Inkscape:

    46 | 54 |

    If you want to have a solid coloured base:

    55 | 63 |

    To preserve detail use 'Arachne' slicing.

    64 | An FDM print of the example SVG 65 |
    66 |
    67 | 68 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { renderSVG } from './render-svg'; 2 | import { exampleSvgData } from './example-svg'; 3 | import { setupScene } from './setup-scene'; 4 | import * as THREE from 'three'; 5 | import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'; 6 | import { saveAs } from 'file-saver'; 7 | import JSZip from 'jszip'; 8 | 9 | const App = (() => { 10 | const { scene, camera, controls } = setupScene(document.querySelector('#sceneContainer')); 11 | var state = { 12 | scene, 13 | camera, 14 | controls, 15 | }; 16 | 17 | const fitCamera = () => { 18 | const boundingBox = new THREE.Box3().setFromObject(state.extrusions); 19 | const center = boundingBox.getCenter(new THREE.Vector3()); 20 | const size = boundingBox.getSize(new THREE.Vector3()); 21 | const offset = 0.5; 22 | const maxDim = Math.max(size.x, size.y, size.z); 23 | const fov = camera.fov * (Math.PI / 180); 24 | const cameraZ = Math.abs((maxDim / 4) * Math.tan(fov * 2)) * offset; 25 | const minZ = boundingBox.min.z; 26 | const cameraToFarEdge = minZ < 0 ? -minZ + cameraZ : cameraZ - minZ; 27 | 28 | state.controls.target = center; 29 | state.controls.maxDistance = cameraToFarEdge * 2; 30 | state.controls.minDistance = cameraToFarEdge * 0.5; 31 | state.controls.saveState(); 32 | state.camera.position.z = cameraZ; 33 | state.camera.far = cameraToFarEdge * 3; 34 | state.camera.updateProjectionMatrix(); 35 | }; 36 | 37 | const loadSvg = (svgData) => { 38 | const { object, update, byColor } = renderSVG(svgData); 39 | while (state.scene.children.length > 0) { 40 | state.scene.remove(state.scene.children[0]); 41 | } 42 | state.extrusions = object; 43 | state.scene.add(object); 44 | state.sceneUpdate = update; 45 | state.byColor = byColor; 46 | }; 47 | 48 | const renderDepthInputs = () => { 49 | const depthsContainer = document.querySelector('#depths'); 50 | depthsContainer.innerHTML = ''; 51 | for (const [color, colorShapeData] of state.byColor) { 52 | const item = document.createElement('li'); 53 | const label = document.createElement('label'); 54 | const swatch = document.createElement('span'); 55 | const input = document.createElement('input'); 56 | label.innerHTML = color; 57 | label.setAttribute('for', color); 58 | swatch.setAttribute('style', `background-color: #${color}`); 59 | input.setAttribute('type', 'number'); 60 | input.setAttribute('step', '0.1'); 61 | input.setAttribute('id', color); 62 | input.value = colorShapeData[0].depth; 63 | input.addEventListener('input', (event) => { 64 | state.sceneUpdate(Number(event.currentTarget.value), color); 65 | }); 66 | 67 | item.appendChild(label); 68 | item.appendChild(swatch); 69 | item.appendChild(input); 70 | depthsContainer.appendChild(item); 71 | } 72 | }; 73 | 74 | const download = () => { 75 | const exporter = new STLExporter(); 76 | const zip = new JSZip(); 77 | for (const [color, colorShapeData] of state.byColor) { 78 | const scene = new THREE.Scene(); 79 | colorShapeData.forEach((data) => { 80 | const clone = data.mesh.clone(); 81 | clone.rotation.z = Math.PI/2; 82 | scene.add(clone); 83 | }); 84 | scene.updateMatrixWorld(true) 85 | const result = exporter.parse(scene, { binary: false }); 86 | zip.file(`${color}.stl`, result); 87 | } 88 | zip 89 | .generateAsync({ 90 | type: 'blob', 91 | }) 92 | .then(function (content) { 93 | saveAs(content, 'svg2solid.zip'); 94 | }); 95 | }; 96 | 97 | return { 98 | loadSvg, 99 | fitCamera, 100 | renderDepthInputs, 101 | download, 102 | }; 103 | })(); 104 | 105 | App.loadSvg(exampleSvgData); 106 | App.renderDepthInputs(); 107 | App.fitCamera(); 108 | 109 | const svgFileInput = document.querySelector('#svgFile'); 110 | const downloadButton = document.querySelector('#download'); 111 | 112 | svgFileInput.addEventListener('change', function (event) { 113 | var reader = new FileReader(); 114 | reader.onload = function (event) { 115 | App.loadSvg(event.target.result); 116 | App.renderDepthInputs(); 117 | App.fitCamera(); 118 | }; 119 | reader.readAsText(event.target.files[0]); 120 | }); 121 | 122 | downloadButton.addEventListener('click', () => { 123 | App.download(); 124 | }); 125 | -------------------------------------------------------------------------------- /assets/svg2solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 38 | 40 | 46 | 52 | 58 | 64 | 65 | 69 | 73 | 77 | 82 | 86 | 91 | 96 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /assets/svg2solid-printable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 20 | 26 | 32 | 38 | 39 | 41 | 43 | 47 | 51 | 55 | 59 | 63 | 67 | 71 | 76 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/example-svg.js: -------------------------------------------------------------------------------- 1 | export const exampleSvgData = ` 2 | 3 | 4 | 12 | 14 | 20 | 26 | 32 | 38 | 39 | 41 | 43 | 47 | 51 | 55 | 59 | 63 | 67 | 71 | 76 | 81 | 82 | 83 | `; 84 | -------------------------------------------------------------------------------- /assets/svg2solid-url-printable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 76 | --------------------------------------------------------------------------------