diff --git a/src/App.svelte b/src/App.svelte index b638731..5daa890 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -18,6 +18,7 @@ import ShapingSection from './lib/ShapingSection.svelte'; import Instructions from './lib/Instructions.svelte'; import FilamentChart from './lib/FilamentChart.svelte'; + import InfoBox from './lib/presentation/Info.svelte'; import { serialize, deserialize } from './lib/serialize'; import { trackPageView, trackEvent } from './lib/telemetry'; @@ -43,6 +44,7 @@ let generatingCSG = false; let generatingSCAD = false; let generatingSTL = false; + let generatingSVG = false; let generatingSCADSTL = false; let stlDialogOpen = false; let sponsorOpen = false; @@ -100,6 +102,11 @@ myWorker.postMessage({type: "stl", data: state }); } + function downloadSVG() { + generatingSVG = true; + myWorker.postMessage({type: "svg", data: state }); + } + function downloadSCADSTL() { generatingSCADSTL = true; logs = ['Loading OpenSCAD...'] @@ -135,6 +142,11 @@ trackEvent('dactyl-render', { time: window.performance.now() - renderBegin }) const blob = new Blob([e.data.data], { type: "application/octet-stream" }) download(blob, "model.stl") + } else if (e.data.type == 'svg') { + // SVG generation finished. Download it! + generatingSVG = false; + const blob = new Blob([e.data.data], { type: "image/svg+xml" }) + download(blob, "plate.svg") } else if (e.data.type == 'csg') { // Preview finished. Show it! generatingCSG = false; @@ -194,6 +206,16 @@ {#each section.fields as key} {/each} + {#if section.var == "connector" && state.options.connector.external} + +

You'll also need to print one of the external holders listed here.

+
+ {:else if section.var == "misc" && state.options.misc.plate} + +

If you plan to laser cut the base instead of 3D printing, you'll need a 2D drawing of the plate. that you can open in Inkscape or Illustrator and send to the cutter.

+

Please ensure the line labeled 1 cm is really 1 cm long before sending the file. Afterwards you can safely delete it.

+
+ {/if} {/if} {/each} diff --git a/src/connectors.md b/src/connectors.md new file mode 100644 index 0000000..4393c4d --- /dev/null +++ b/src/connectors.md @@ -0,0 +1,22 @@ +List of External Holders for the Dactyl +==== + +While I personally don't use external holders, they are a popular way to securely fasten a microcontroller and connector to your keyboard, especially if you don't have access to a drill to widen the holes in the model. + +If you're using one of these, make sure to set the connector type to *anything* but RJ9. + +| Image | Board | Model Downloads | Author | +|-----------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------|-----------------------------------------------------------------------|------------------------------| +| Pro Micro Holder | Pro Micro
(micro USB) | [[left (mesh fixed)][pm-leftf]] [[left][pm-left]] [[right][pm-right]] | [Blue Ye (@yejianfengblue )] | +| | Pro Micro
(USB-C) | [[left][pmc-left]] [[right][pmc-right]] | [Blue Ye (@yejianfengblue )] | +| | Elite-C | [[right][ec-right]] | Unknown | + +[pm-leftf]: https://github.com/yejianfengblue/dactyl-generator-demo/blob/main/stl/promicro-holder-v3-left-mesh-fixed.stl +[pm-left]: https://github.com/yejianfengblue/dactyl-generator-demo/blob/main/stl/promicro-holder-v3-left.stl +[pm-right]: https://github.com/yejianfengblue/dactyl-generator-demo/blob/main/stl/promicro-holder-v3-right.stl +[pmc-left]: https://github.com/yejianfengblue/dactyl-generator-demo/blob/main/stl/promicro-holder-typec-untested-left.stl +[pmc-right]: https://github.com/yejianfengblue/dactyl-generator-demo/blob/main/stl/promicro-holder-typec-untested-right.stl +[Blue Ye (@yejianfengblue )]: https://github.com/yejianfengblue +[ec-right]: https://web.archive.org/web/20220607031927/https://dactyl.siskam.link/loligagger-external-holder-elite-c-v1.stl + +If you found one that isn't listed here, please submit a pull request! I'd like to make this list as complete as possible. diff --git a/src/lib/SupportDialog.svelte b/src/lib/SupportDialog.svelte index a346ea1..e117878 100644 --- a/src/lib/SupportDialog.svelte +++ b/src/lib/SupportDialog.svelte @@ -1,5 +1,5 @@ +
+
+ +
+
+
+ +
+
diff --git a/src/worker/SVGExporter.ts b/src/worker/SVGExporter.ts new file mode 100644 index 0000000..7c317ba --- /dev/null +++ b/src/worker/SVGExporter.ts @@ -0,0 +1,85 @@ +/** + * Export a planar mesh to SVG. + * + * The formats are very different (a mesh is made of tesselated triangles, whereas SVG is a single face), + * so some preprocessing needs to be done. + * 1) The mesh is filtered so that only faces on the z plane are exported + * 2) The triangles of the mesh are combined into polygons + * 3) The polygons are written out to the SVG file. + */ + +import type { Mesh } from 'manifold-3d'; + +interface Options { + margin?: number + color?: string +} + +export default function svgExport(mesh: Mesh, options: Options) { + const margin = options?.margin ?? 10 + const color = options?.color ?? "#8080F7" + + const flatFaces: [number, number, number][] = [] + for (let i = 0; i < mesh.triVerts.length; i+=3) { + // a, b, and c are the points on face + const [a, b, c] = mesh.triVerts.slice(i, i+3) + // If the z coordinates of all points are 0, add it. + if (mesh.vertProperties[a*3+2] == 0 && + mesh.vertProperties[b*3+2] == 0 && + mesh.vertProperties[c*3+2] == 0) { + flatFaces.push([a, b, c]) + } + } + + const boundary = new Set() + for (const tri of flatFaces) { + for (const [e0, e1] of [[tri[0], tri[1]], [tri[1], tri[2]], [tri[2], tri[0]]]) { + if (boundary.has(e1 + ',' + e0)) { + boundary.delete(e1 + ',' + e0) + } else { + boundary.add(e0 + ',' + e1) + } + } + } + + // To process the boundary, I create a queue of edges to their next edge (this map). + const next = new Map() + for (const b of boundary) { + const [e0, e1] = b.split(',').map(v => Number(v)) + next.set(e0, e1) + } + + let minX = 0 + let maxX = 0 + let minY = 0 + let maxY = 0 + + const paths: string[] = [] + // Extract a vertex in the boundary, follow the next edges until there is no more vertex, then repeat. + while (next.size > 0) { + let vertex = next.keys().next().value + let path = '' + while (next.has(vertex)) { + // Flip the y to preserve how things look in the preview + const [x, y] = [mesh.vertProperties[vertex*3], -mesh.vertProperties[vertex*3+1]] + path += `${path.length==0 ? 'M' : 'L'}${x},${y}` + minX = Math.min(minX, x-margin) + maxX = Math.max(maxX, x+margin) + minY = Math.min(minY, y-margin) + maxY = Math.max(maxY, y+margin) + const newVertex = next.get(vertex) + next.delete(vertex) + vertex = newVertex + } + paths.push(path + 'Z') + } + + return ` + + += 1cm + +` +} diff --git a/src/worker/index.ts b/src/worker/index.ts index 647bee5..48d14e6 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -7,6 +7,7 @@ import createGC, { type GC } from './gc' import scadWasmUrl from '../assets/openscad.wasm?url' import stlExport from './STLExporter' import { supportManifold } from './supports' +import svgExport from "./SVGExporter.js" (Module as (a: any) => Promise)({ locateFile: () => manifoldWasmUrl, @@ -29,6 +30,7 @@ function main(manifold: ManifoldStatic) { switch (type) { case 'csg': return generateCSG(data, modeling, cleanup) case 'stl': return generateSTL(data, modeling, cleanup) + case 'svg': return generateSVG(data, modeling, cleanup) case 'scad': return generateSCAD(data) case 'scadstl': return generateSCAD_STL(data) } @@ -58,6 +60,16 @@ function generateSTL(config: any, modeling: Modeling, cleanup: GC) { } } +function generateSVG(config: any, modeling: Modeling, cleanup: GC) { + try { + const mesh: Mesh = Dactyl.generateManifold(config, modeling).getMesh() + message('svg', svgExport(mesh)) + cleanup() + } catch (e) { + console.error(e) + } +} + function generateSCAD(config: any) { message('scad', Dactyl.generateSCAD(config)) }