jes notes Index Gallery . Signed Distance Functions Shaft passers Snap issues

2025-03-24

Last modified: 2025-03-25 09:41:02

< 2025-03-23 2025-03-25 >

Meshing

For a first pass, just do straightforward marching cubes in some fixed volume and save out an STL file.

Then:

OK, wow, Claude is writing out this entire thing off the top of its head, including the data tables for marching cubes.

I wonder if it is making mistakes here, or it has memorised the characters it needs to write, or it recognises the structure of the algorithm so it is deriving it on-the-fly?

Damn, Anthropic cut it off before it finished.

I pasted what it did and asked it to complete it and it got cut off early again!

I wonder if they think this is some kind of model abuse?

If I just ask it to add a few lines at a time it is working.

Meh, I'll just copy from https://gist.github.com/dwilliamson/c041e3454a713e58baf6e4f8e5fffecd

First export of default model looks like this:

I can kind of recognise that this has some triangles in places that correspond to the default model. It's obviously not working though.

OK, big improvement from mucking about with Cursor:

Can now very clearly recognise the shape, though it still has a few holes in it.

Looking at just this section:

It is obvious that a lot of the triangles are being placed correctly, but one of them isn't joining up. So is the triangle table from that github gist wrong? Or my code is wrong?

ChatGPT suggested logging for "skipping degenerate triangle", and indeed it is logging a lot of this. So that can easily explain why some triangles are missing.

At higher resolution we can see it is basically right, just with holes in it:

It also looks like half of them might have inverted normals.

OK, Claude-3.7-sonnet-thinking has fixed the normals. Still have half the triangles missing:

By disabling vertex interpolation I get a very blocky mesh that makes it easier to work out which triangles are missing:

Flat faces perpendicular to X seem to be handled properly. Some triangles are missing on flat faces perpendicular to Y. All triangles are missing on flat faces perpendicular to Z.

I gave that description to Claude, and a copy-paste of the comment from the github gist describing the ordering, and it managed to fix it:

Great success!

It is truncated because the bounding volume for the voxel evaluation is too small.

And both binary and ASCII STL exporting seems to be working correctly.

Gyroid sphere:

The binary STL file for the gyroid sphere is 21 megabytes and it took about 3 seconds to generate.

I think we could improve on the time spent to generate it by using the sparse voxel octree thing to cut down on the number of cubes. Probably wouldn't decrease the file size though, because it genuinely is curved everywhere. But I probably don't care about the file size.

First Isoform 3d print

Now that I have meshing, let's print something!

This is a half-capsule with a shell of a gyroid subtracted from it. That means it is actually 2 disconnected parts.

Great, worked:

Node types

I might want to use wrap/unwrap like in https://github.com/jes/stlwrap

Also some way to create text.

How could I create something like a 2d text node or a sketch node, and then "fit" it to a surface? Maybe you could approximate it with "Distance Deform Inside"? Extrude your text really far, make sure the extrusion goes into the surface, and then "Distance Deform Inside" to make the surface bulge outwards/inwards where the text intersects it?

Kind of a crude tool. Maybe crude tools is just the nature of SDFs.

Also "Pattern along transformed domain". I guess like LinearPattern, but it somehow references a "Modifer" tree, but the eventual child of the modifier tree is not an SDF but is just something that captures the transformed coordinate so that it knows where to place the object.

Still need to figure out the UI for selecting modifier trees. I think primitives and combinators don't make any sense in this context, so restricting it to modifiers is correct. I guess you attach a modifier tree with no eventual leaf primitive, and then somehow when you evaluate it you just capture out the transformed coordinate.

In fact it's tighter than just modifier nodes: they need to be domain modifying nodes. Some modifiers only modify the distance, and they would have no effect here. Maybe a way for a node to flag itself as a domain modifier?

Also, I should make the nodes somehow register themselves somewhere so that I don't have to explicitly list them all in the UI thing that makes the context menu.

InvoluteGear (CycloidGear?).

Rough edges

Where do I want and not want interval arithmetic? I can maybe delete the GLSL interval code generation, since it is unfinished and probably not wanted.

Do I want or not want bounding spheres? Either make sure they are all there and all work correctly, and use them in LinearPattern etc., or delete them.

Icons in the document tree should probably be images instead of emojis.

Axis display doesn't respect aspect ratio.

Node names should be numbered as small as possible to make them unique, rather than all going up in number.

Sketch editor should work better.

Save/load your work.

Toggle visibility of selected node with spacebar.

More key shortcuts in general.

Step factor in view toolbar.

Navigation help on a hover popup or something.

Make a "modal" abstraction over the property editor or something, so that you get to specify more complex inputs and have the properties computed from them.

Pass the properties into the shader with uniforms instead of recompiling, where possible.

"Direct modelling" by dragging nodes around the 3d view, or typing in text boxes overlaid on the 3d view, instead of always typing in the property editor.

Make some fake abstraction where we classify things at the top level under Document as "bodies", which are then treated slightly differently in the UI, but really it's just a union?

Timeline-like version of the tree view? So each operation is either additive or subtractive, and we build up Union/Subtraction accordingly.

Prospero challenge

https://www.mattkeeter.com/projects/prospero/

This would probably be good practice for working with big arithmetic expressions, even if I don't actually beat Matt's time.

OK, I wrote a naive implementation in perl just to have somewhere to start, expecting it to be similarly performant to the Python example. But I hadn't recognised that the Python example uses numpy. My perl implementation still hasn't finished running, lol. This challenge is way harder than the page makes it sound!

One idea is to recognise that we only want to know which pixels are positive and which are negative. What if we start at the end and work backwards, and turn it into inequalities instead of arithmetic?

So then we get a set of ranges for x and y in which the overal expression is less than 0, and then plot those ranges?

For all of the operations:

So actually this all turns into straightforward boolean operations except for add and subtract.

One idea would be to have "only one variable", so let's say we fix x and then we ask for the ranges of y for which the expression is below zero. And then addition and subtraction tell us what values of b give us results below zero as a function of a. So for example add(5,b) would give us the interval b = -inf .. -5.

But then we'll end up having intervals in both arguments to a future add call, at which point we can't give an answer. Although is that still fine? If everything is a function of x.

Let's say x = 0. For what values of y is this function below zero?

(x+y) * (x-y)

We have:

_0 var-x
_1 var-y
_2 sub _0 _1
_3 add _0 _1
_4 mul _2 _3

And then annotating it with the ranges of y for which the result of that operation is <= zero:

_0 var-x       y = (-inf .. inf)
_1 var-y       y = (-inf .. 0)
_2 sub _0 _1   y = (0 .. inf)
_3 add _0 _1   y = (-inf .. 0)
_4 mul _2 _3   y = (-inf .. inf)

So for x=0 we've proven that this function is <=0 for all y.

I actually think this might work. And maybe 2-dimensional ranges are possible as well? Not clear, because add(x,y) would not have axis-aligned edges for example.

But what if both arguments to add are complicated functions of y?

Maybe we just brute-force evaluate it for all possible values of y, and then list those exact values for which it evaluates <= 0? Seems like it's not going to help, we'll be brute-force evaluating almost the entire tree.

So the 2 ideas I have are:

The first idea still evaluates almost the entire tree. The second idea doesn't work because of add/sub.

So the first is saying "the overall expression will be <0 for these values of Y", and the second is saying "this specific operation will be <0 for these values of Y".

Let's imagine a simpler language that only has var-x/const/add/sub/min/max. (No y).

How do we make something tell us for which ranges of x the expression is <0? It's easier to make it return an anonymous function that will tell us whether it is <0 for a particular value of x.

We only need to handle add(a,b), because we define sub(a,b) as add(a,neg(b)).

add(a,b) < 0 if a < -b

We can rule out add producing a result that is less than 0 if neither of its operands can produce a result that is less than 0.

It might be more elegant to define add(a,b) = sub(a,neg(b)), and

sub(a,b) < 0 if a < b

Maybe we say that for each y value for which a < 0 and b > 0, we propagate that y value through to the set of y values for which this expression is below zero. For those where a > 0 and b < 0, we exclude them, and for ambiguous ones we brute force? And maybe memoise. (Is there a bottom-up dynamic programming version?).

We'll say that the "range" of y values is 0 to 1023 or whatever, but the var-y node does the mapping to -1 .. +1 space. That way we're just dealing with integer y ranges when we're tracking which y values can give negative results.

Or maybe -512 to +511, so that testing <0 is still easy.

For subtrees that don't depend on x, we can keep the memoised result across all x coordinates.

Obviously we also want to do constant folding, and equivalent subexpression elimination with a DAG or whatever.

I made a start on this in Perl but it is too messy. I should do it in Go probably. Is there a good Go library for integer Ranges? I think the data structure I'm looking for is either https://en.wikipedia.org/wiki/Interval_tree or https://en.wikipedia.org/wiki/Segment_tree

Sketch editor

The sketch editor has somehow become broken. I haven't looked at it in a while so not sure when. I suspect the commit that made camera.js make use of Vec3 and Mat3 broke it, unsure.

Oh! Somehow it works on the default object's sketch, but if you make a new document and a blank sketch it is broken?

Hmm, no, it's just working properly now. Not sure what was wrong before.

There are bugs if you try to edit the sketch on the XZ/YZ planes though.

Also if you disable a sketch node it kind of breaks everything.

Also when you open the sketch the rest of the model disappears.

OK, the model disappearing thing was caused by setting the background colour on <canvas>: need to only do that on #glCanvas.

The model getting broken when you disable a sketch is because noop() is P.const(10000042.0) instead of actually somehow "disabling" the object. I wonder how to fix that. When the object is disabled it should act like it doesn't exist in the tree. But more than that, when a node returns this.noop(), that node should act like it doesn't exist in the tree as well. But that means we don't get to decide whether a node should act like it exists or not until after we've already decided to evaluate it.

Could we make Peptide have a "noop" operation? It would act like 0 for adding/subtracting, 1 for multiplying/dividing, infinity for min(), -infinity for max(), etc.

Or would it propagate up and turn everything it touches into a noop? And then you have to check explicitly?

Or we have an exists() method in TreeNode. And instead of merely checking whether a child exists in the children array, you check whether this.children[i].exists(). And that node can decide whether it exists or not based on whether its own children exist. And exists() is automatically false when the node is disabled.

And maybe we have 2 different ways to access the "children"? So one gives you all the children (the current one) and one only gives you the enabled children?

Tidying up

I'm actually going to delete the bounding spheres and the exactness values. They're not rigorously correct, and they're not used for anything, so they're better off gone.

< 2025-03-23 2025-03-25 >