├── .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 | 
2 |
3 | 
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 |
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 |
39 |
40 |
41 |
42 |
43 | Tips
44 | Make sure the various colours in the SVG are non-overlapping paths.
45 | In Inkscape:
46 |
47 | - Convert all objects into paths (Paths > Objects to Path)
48 | - Convert any strokes into paths (Paths > Stroke to path)
49 | -
50 | Duplicate any overlapping colour and subtract it from the covered colour (Edit > Duplicate; Path >
51 | Difference)
52 |
53 |
54 | If you want to have a solid coloured base:
55 |
56 | - Select the colour that bounds your object
57 | - Duplicate it
58 | - Path > Break apart
59 | - Delete all paths inside the bounding path
60 | - Make it a different colour from all others
61 | - Set depth to a negative number
62 |
63 | To preserve detail use 'Arachne' slicing.
64 |
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 |
105 |
--------------------------------------------------------------------------------
/assets/svg2solid-printable.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
83 |
--------------------------------------------------------------------------------
/src/example-svg.js:
--------------------------------------------------------------------------------
1 | export const exampleSvgData = `
2 |
3 |
4 |
83 | `;
84 |
--------------------------------------------------------------------------------
/assets/svg2solid-url-printable.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
76 |
--------------------------------------------------------------------------------