Inspiration for SDF3UI

Introduction: Life before sdf3ui

sdfx is an amazing library. We've used it for generating 3D printable parts for work at an aerospace firm. The parts We've printed have been used to support PCBs, make DIY ATX power supplies and even for pipe-fitting designs for industrial processes. It is an exciting time to be alive in the maker industry. The process for generating these shapes is quite simple:

  1. Create a .go file and run go mod init project
  2. go get github.com/deadsy/sdfx and start coding your shape and have it save the shape to an STL file
  3. Compile and run the program and visualize the resulting shape it in your program of choice.
  4. Print shape when happy with result.

We were using CURA by Ultimaker to visualize changes, it had a handy "reload shape" button that showed up when changes were detected in the shape. This was fine for the most part, but it was a hassle to switch between windows, having to refocus the editor window before resuming programming. CURA does not have an autoreload setting. Shape generation also had to be manually run with go run. We knew it could be done better.

Existing tools

There exists a tool that seeks to solve this problem called https://github.com/Yeicor/sdfx-ui. It autoreloads on code edit, so no running the program nor hitting "reload file"! This sounds great on paper but truth was that sdfx-ui used CPU rendering of 3D shapes, so it could not do fluid user-interactive rotations or zoom. It felt like much was being lost coming from CURA, so the idea of using a GPU library to implement an auto-loading UI was considered.

GPU rendering library of choice

The choice of the GPU facing library was easy enough. For the last year Patricio has been working on Go WASM bindings for the excellent three.js library. Patricio has found it works consistently on every device thrown at it, and fast. Even without a dedicated GPU one regularly gets great frame rates.

sdfx rewrite

So without further due we set out to read the sdfx code to find out how to implement the renderer-browser interface. Speaking as Patricio: Boy did I not know what I was getting myself into. You can find the dramatized version of what happened after I started reading the sdfx code here. Long story short, I rewrote sdfx because of creative differences with Jason, the lead author of sdfx. The library I was to use now in place of sdfx was sdf.

What sdf3ui does

You run it via command line with the name of the file with the Go code that defines the 3D shape.

When sdf3ui is running it

  1. Spins up an http server on http://[::]:8080 (equivalent of localhost).
  2. It go run's the shape file and stores the rendered shape.
  3. When one navigates to said URL with a browser one is greeted with an UI with a 3D visualization of the designed 3D shape. This visualization is interacive friendly (use mouse right/left click and drag, mouse scroll, etc.)
  4. It registers a operating system listener for changes on the file. If file is modified and saved the file is re-run, the new shape saved and a status message is broadcast to all browsers viewing the shape is sent out. Browsers that receive this broadcast will automatically fetch the new shape and display the changes as soon as possible.

This means that one can be editing the shape on a computer while your phone or second monitor displays the latest and updated shape. This makes working with sdf a breeze. It also means many people can visualize the latest shape iteration at the same time on different computers. Talk about maker collaboration.

How we built it

The application consists of two programs running simulataneously:

  • The CLI is the desktop application that generates the shape data from your .go project and serves it on a TCP server (websocket+http).
  • The App, or colloquially named browser, is a WASM/http/js program that runs in your web browser. It requests the shape data via websocket and http requests and displays the 3D shape to the user.

Infrastructure

A lot of time was spent on building the infrastructure for this project (a.k.a. yak shaving). Over a week (April 18-29) was spent just rewriting sdfx so that it was easier to build on top of and also much faster (performance). Tweaks to the new sdf library were prevalent during the whole project.

Designing the CLI-Browser interface

Some high level concepts of designing the sdf3ui tool will be detailed here:

Shape lifetime on the CLI:

  1. The first shape is rendered on running sdf3ui with the .go project as is and stored in memory.
  2. The shape is rendered upon changes to the .go project and stored as a ShapeModel in memory. Any previous existing ShapeModel is invalidated and discarded (becomes "stale") via use of context.Context.
  3. Shape is stored and used when requested until invalidated by a new rendered shape.

Shape status protocol. Necessary for letting browsers know exactly when there is a change to the shape.

  1. On starting the CLI a websocket listener is opened with the sdf3ui subprotocol.
  2. A browser will listen idly on this websocket for messages.
  3. When the CLI renders a new shape a message will be sent over this websocket to all listeners (browsers).
  4. Browsers will receive the status message and can judge whether to request new ShapeModel based on the Sequence number. For now the browser updates the ShapeModel regardless. The ShapeModel update is a simple http.Get endpoint which streams the ShapeModel as an encoding/gob payload.
  5. Browsers will continue listening for new updates idly and repeat steps 2-4.

Browser shape visualization steps:

  1. Upon updating the ShapeModel the browser makes sure the Sequence number has changed and then must process the ShapeModel data into data suitable for WebGL rendering (float64->float32 conversion).
  2. After the data conversion and 3D model generation the browser displays the updated shape.

Frontend framework

Vecty was used as the frontend framework for it's simple and idiomatic API. A big bonus is the absence of tooling necessary to work with it- it's just pure Go.

Vecty manages the HTML rendering and event handlers to Go functions. The program is in the app folder under sdf3ui.

Thanks

Special thanks to Jason who wrote the sdfx library, which is still something that blows us away in scope and execution to this day.

Another special thanks to the reddit community, whose abrasive suggestions led us to adopt a better error handling methodology.

Challenges we ran into

Faceoffs with sdfx maintainer

It started as a simple demonstration draft PR with suggested changes to the unfortunate design of the sdfx renderer interface. Patricio feels that the suggestions were not met with an open mind nor a desire to improve the library where it mattered.

Silly event-loop bugs

2 false positive bugs were filed, one in golang/go and another in nhooyr/websocket, due to lack of understanding of how the javascript event loop works.

Downloading data from browser

To this day there is no Go WASM reliable way of downloading data as a file without using an existing javascript library. A stack overflow inspired implementation by Patricio has bugs and is known to corrupt data for large files. Because of this there is no Download Shape button on the sdf3ui user interface, instead the shape is saved in the folder of whomever is running the CLI application.

Accomplishments that we're proud of

Fast & Furious UX

The user experience is much better than in the previously mentioned existing tools. Once the renderer is up and running it usually takes 1-2 seconds between the time the user hits the save button on the editor and the moment the new 3D shape is displayed on screen. The visualization can be rotated, zoomed and even saved to file with a given name.

This is a game-changer for developing with sdfx or sdf.

Rethinking of sdfx from the ground up

The sdfx library was overhauled and little was left unchanged. The comparison speaks for itself:

_Comparison sdfx vs. sdf _

Advantages of deadsy/sdfx:

  • Widely used
  • More helper functions
  • Working 2D renderer

Advantages of soypat/sdf:

  • Very fast rendering
    • deadsy/sdfx is over 2 times slower and has ~5 times more allocations.
  • Minimal and idiomatic API
  • Renderer interface is dead-simple, idiomatic Go and not limited to SDFs
    • deadsy/sdfx Renderer3 interface has filled render package with technical debt.
  • Has SDFUnion and SDFDiff interfaces for blending shapes easily
  • No nil valued SDFs
    • deadsy/sdfx internally makes use of nil SDFs as "empty" objects. This can later cause panics during rendering well after the point of failure causing hard to debug issues.
  • Well defined package organization.
    • deadsy/sdfx dumps helper and utility functions in sdf
  • End-to-end tested.
    • Ensures functioning renderer and SDF functions using image comparison preventing accidental changes.
  • Error-free API under must3 and must2 packages for makers.
    • For simple projects these packages allow for streamlined error handling process using panic instead of returned errors.
    • deadsy/sdfx only allows for Go-style error handling like the form3 and form2 packages.
  • Sound use of math package for best precision and overflow prevention.
    • math.Hypot used for all length calculations. deadsy/sdfx does not use math.Hypot.
    • spatial types from gonum library with correct Triangle degeneracy calculation. deadsy/sdfx's Degenerate calculation is incorrect.
  • Uses gonum's spatial package

Contributions to gonum

Due to the user/programmer close interaction with the spatial types (i.e. Triangles, Vec, Boxes) these had to have special treatment to ensure the best developer experience.

Using gonum for all spatial types is high priority since it gives the module

  • Stability: gonum abides by the Go compatibility promise.
  • Correctness: gonum favors correct code and is held in high regard in the Go community.
  • Familiarity: gonum is widely used.

Gonum issues related to sdf:

What we learned

Conflict resolution via forks

Forking projects is a normal endeavour. Sometimes its necessary for code to evolve into something new and maybe even better. The prospect of joining the forks in the future is always an option.

Websockets are easier than you think

Who would have thunk websockets were such a pleasure to use in Go! The way we've been using them in sdf3ui is to synchronize http requests for the new shape, much like a channel synchronizes goroutines in Go!

What's next for SDF 3D shape visualizer

During the project we were approached by two people interested in the sdf project (not the UI per se).

Ultimate UX experience

Gerard wanted to develop an desktop application that streamed SDF shapes from a centralized server in such a way that the changes happened in the server and should be propagated to hundreds of users that could be editing/viewing the shape or assembly of shapes. Together we talked about the sdf design decisions and direction. Gerard was kind enough to instruct us in the use of websockets and gave us future ideas for streaming "diffing" changes to shapes. This could provide the ultimate user experience by reducing (to the order of the millisecond) the time between a change in the program and the time it takes for that change to be visible in the browser. Right now the main source of delay is the time it takes to stream all the model triangles to the browser.

This change requires a hefty amount of engineering and the sdf3ui authors would be more comfortable using an existing library with this functionality.

Gerard was also keen on telling us he wanted to start learning aframe to implement a virtual reality CAD package using sdf sometime in the future.

Go beyond the server room

So far the community response to the sdf package has been overwhelmingly positive (with the exception of Reddit). We look forward to seeing more and more people use it to design their 3D printed parts and more! This is where sdf3ui might provide an important weight in the decision matrix to use Go. sdf3ui has the potential to make Go a no-brainer choice for makers wanting to design 3D printed parts. Right now it needs users who can bring needs of those other than the author of sdf3ui and formulate them as issues and pull requests.

Link to SDF3UI.

Built With

Share this project:

Updates