OpenVDE
- 10 Devlogs
- 57 Total hours
I’ve been building up to this feature for a while but I finally got lossless and bidirectional conversion between the AST and UI representations. This means you can design something, pull up just the Python file on another device, and continue working seamlessly. This also means you can edit the code in real-time and see it immediately reflect in the UI and vice versa.
Completely forgot to log for a while but got a lot done. I wanted to add the fillet feature and in order to get there had to be able to select edges of the model.
This was mostly just duplicating the same logic that I used for face selection. I basically just export the edge as it’s own poly-line in the .GLB file. Adding the fillet feature was pretty easy after this since the framework I made before makes it pretty simple.
Then I added back the script view on the left to show a live preview of the code. Each feature exports python AST, which depending on the context can be run on Pyodide or be ran through a quick transformation to export.
I’m currently working on making the script editable. Each feature and expression now has a fromAST method which gives an instance of that feature/expression based on the code. This gives a bi-directional and lossless conversion between the UI representation and code.
I generally cleaned up a lot of the jank from my rush to get an MVP working. Big thing is I actually have a somewhat working “timeline” now so each feature saves its output and it doesn’t get overwritten by the next one. Also added a new feature called “Script” which is sort of a back-up feature which allows you to inject arbitrary build123d code into the project and build off of it. This allowed me to remove the code editor along with the weird code to deal with manual code generation that wasn’t a feature.
Overall, much less buggy experience and stuff working!
It works! I expanded a lot on the idea I had of Primitives and chunked the entire code-generation architecture into “Tools”, “Expressions”, “Primitives”, and “Bindings”. These all work with py-ast to generate an AST for Python and the AST get analyzed for dependencies (eg. the model its operating on, the face its extruding) and the deps are looked up in a “symbol table” which Pyodide then injects into the global namespace.
All that to say, I can write features and have them operate on the model without rerunning the whole script and without duplicating a bunch of code.
Here is an example of me selecting a face on the auto-generated key cap and extruding it 10mm. Since we can’t sketch yet, I need to auto-generate a model to be able to get faces in the scene.
I started using Comlink to implement RPC with the worker. The idea was to be able to interact with PyProxies from the main thread through Comlink, but unfortunately since PyProxies are already proxies it makes the whole system incredibly weird. Instead, I started using a “registry” for PyProxies and referenced them with a string-type key in the main thread.
This all needed a lot of refactoring and I also improved the types across the project. No new features have been added but I should be able to actually implement working features/tools now.
P.S: Model regen is taking 200sec+ on my Mac which is very concerning, I need to look into it.
I expanded on the feature templates, honestly went through 3 different iterations before settling on this one. This new one introduces dynamic fields, meaning sections can be shown or hidden based on the values of other fields.
I also wrote a nicer interface for the menu, and got the a lot of different data-type inputs working. The nice thing about my custom schema-format for the fields is primitive inputs like numbers and check boxes are just as seamless as custom ones like face selectors.
The next obvious step is making the feature actually work, which will require a small bit of refactoring. It would be really inefficient to recompute the entire script each time a feature is modified (especially if I want live updates while editing the feature) so I’d like to use Pyodide’s ability to interact with Python objects directly. Unfortunatly, the object responsible for that (PyProxy) is not serializable and can’t be shared between the worker and UI.
The fix is setting up an RPC system and having all the features/tools live on the worker and the UI just uses comlink to call them.
I want to start getting sketching and other features working and created a reusable object oriented way to make them.
This forced me to learn a lot about TypeScript and I’m not even sure if I understand it now. But the end result is a Tool
class that has a Zod schema defining the parameter types for the tool’s
callback function. The function is automatically typed based on the
schema, as well as a defaults object. This was crazy
complicated because I had to juggle generic types, static vs instance
properties, and a bunch of other weird typing stuff.
There’s not much to show here but now when I click a feature a pop-up
shows the parameter names and types. Next I’ll write a UI “generator”
which has inputs for the parameter, even custom types like faces/planes.
I was going to start working on sketching and other features but realized I needed to be able to select faces first. Three.js does have a Raycaster to select objects in the scene but it returns the whole object or triangles since it has no knowledge of the B-REP model (which stores the actual faces we want rather than triangle approximations ) after exporting it.
The solution was replacing build123d’s builtin export for a more manual process. I used the trimesh library to manually add each face of the model as it’s own mesh and exporting them all together as a scene. Three.js load’s the whole scene as a Group which is practically identical to a normal mesh in code. Now when the Raycaster returns a Mesh, it’s actually a face.
I got the regenerate+re-render times down to about 800ms. The biggest part was actually just letting the browser’s JIT compile the hot parts so that subsequent regens would run way faster. It only takes 1 eval to get there, although that one eval is about 15 seconds. After that its super fast.
Another thing I did was switch to the GLB file format for transfering mesh data between build123d and three.js. GLB is very compact and encodes+decodes super fast. This took the encode + parse time from 3 seconds to about 100ms. I’d really like to be able to cache the “JIT”ed WASM between page loads and apparently the browser should do this already but it’s clearly not.
I also worked on the toolbar and used Bits UI to get a really nice search function with the features. I want to have a nice reusable interface for defining features with an automatic UI builder which would also make an equivalent to OnShape’s FeatureScripts super easy to implement (literally just a build123d generator with some UI bindings).
OpenVDE (need a better name) is on the surface a standard CAD software to design parts and assemblies.
I was frustrated with existing solutions like OnShape because it’s completely cloud-based (you will own nothing and be happy), and only runs in the browser which often lags a lot with WebGL and both of these mean offline use is impossible. However, these together do give the convenience of seamless collaboration.
On the other hand, apps like Fusion360 (and I’m assuming SolidWorks although I’ve never used it) do run locally but run like garbage because they’re from the 20th century (I’m not even kidding) and will crash unless you make a sacrifice to Andrew Anagnost himself. As a side effect of running locally, the collaboration features are lacking and don’t work well with modern workflows.
My solution is CodeCAD, which is an approach to designing parts using code to call a CAD kernel in order to generate parts. OpenVDE is a generator for build123d Python scripts. You use it just like any other CAD software but it saves to a Git repository (or whatever you want to use) which allows for advanced version control and collaboration (eg. live editing of the same file). Storing designs as text also unlocks true parametric design and can be used to also write generators for parts using the logic flows programming languages provide.
I want this app to be an cross-platform and not look like crap, so I decided to go with a Tauri app with a Svelte front-end. A big feature of OpenVDE is the hybrid RPC CAD kernel, which is just jargon for you can run the heavy CAD operations natively, in WASM, in a cloud server, or on your own self-hosted server. This works because at it’s core OpenVDE just generates Python code and it can either be run through Pyodide in WASM, Py03 for native via Rust, or by calling a server.
I setup the project with some basic styles, Three.js, and Pyodide. Huge thanks to this project which provides the WASM compiled wheels for build123d (although I’m pretty sure the project is vibe coded). After getting that all setup plus IndexedDB caching of the packages and an async Web Worker to free the main thread, I got a bare-minimum example of a build123d sandbox in the browser.
I’ll try optimizing it, but the WASM version is running extremely slowly right now (15sec+ to generate this simple part).