curv3d / curv

a language for making art using mathematics
Apache License 2.0
1.14k stars 73 forks source link

Request: Splines? #136

Open lf94 opened 2 years ago

lf94 commented 2 years ago

Is your feature request related to a problem? Please describe.

I'd like to be able to specify a spline. Right now I'm trying to create an ergonomic controller, and I'm hitting limitations in CadQuery, which have led me here to Curv.

Describe the solution you'd like

Add a new function: spline list_of_points list_of_tangents. That's it!

Describe alternatives you've considered

None

Additional context

It may be necessary to add other sketching functions like hLine, vLine, ellipse, etc, like from CadQuery, to create enclosed shapes. https://cadquery.readthedocs.io/en/latest/apireference.html#d-operations

This is the design so far but it's lacking, as I want everything to be smooth:

1631231666

In the meantime I've been recreating a soroban design in curv to compare it to cadquery and I'm really loving what I'm seeing!

A-G-D commented 2 years ago

I'm curious what particular limitations of cadquery are you facing?

doug-moen commented 2 years ago

Splines are possible. Exactly duplicating the CadQuery API is not possible, because CadQuery and Curv use quite different internal representations. Curv is a volumetric (V-rep) solid-modelling tool, meaning that the fundamental primitives are solid objects. CadQuery uses boundary representation (B-rep), meaning that the primitives describe the boundaries of solid objects. So it changes the set of primitives that are available, and it changes the way you design a model using those primitives.

We already have an ellipse primitive. Try curv -x 'ellipse[2,1]'

The CadQuery vLine, hLine etc appear to be used for specifying the boundary of a polygon, one face at a time. (I don't know CadQuery.) If so, then the equivalent in Curv is polygon, where you specify all of the vertices of a polygon at once. On the other hand, if vLine, hLine etc are stroked line primitives, where the strokes have thickness (like in SVG), then the corresponding 2D primitives are stroke and polyline.

The original paper describing the math behind Curv (signed distance fields or SDF) describes a spline primitive. See docs/papers in the Curv repo. But I never learned the math behind splines, so I've just never implemented it. Most of the Curv primitives are copied from code written by other SDF researchers. So it may just be a matter of finding SDF spline code on the internet and porting it to Curv. Sources I've used in the past include https://shadertoy.com and https://iquilezles.org

lf94 commented 2 years ago

Thank you very much @doug-moen !

Luckily I have looked into the math behind splines to implement my own spline in OpenSCAD:

// P = (1−t)³P1 + 3(1−t)²tP2 +3(1−t)t²P3 + t³P4
function bezier4(points) =
let (s = 1.0 / $fn)
[for(t = 0, i = 0; i < $fn + 1; t = t + s, i = i + 1)
       (pow(1 - t, 3)             * points[0])
+  (3 * pow(1 - t, 2) * pow(t, 1) * points[1])
+  (3 * pow(1 - t, 1) * pow(t, 2) * points[2])
+  (                    pow(t, 3) * points[3])
];

Well, "spline", it's for a 4-point bezier curve.

I'm curious, how would you model something like this (an ergonomic controller arm)?

I'm curious what particular limitations of cadquery are you facing?

In particular creating complex curved surfaces is not CadQuery's forté. While I can get pretty far using splines, arcs, and fillet, it gets increasingly hard for CadQuery to accept what I'm asking of it (it will simply say "cannot do B-rep operation" or something like that).

After literally playing with Curv for 1 hour last night and learning just from looking at examples and the docs I'm very impressed with how easy it was to pick up. It may have to do with how I have experience with functional PLs and OpenSCAD though (the latter I doubt because I feel the mental model is different compared to it).

doug-moen commented 2 years ago

OpenSCAD uses boundary representation, which is different from Curv. Your OpenSCAD bezier4 function converts a spline into a collection of line segments, which you could feed into the Curv polygon or polyline function. But this is a polygonal approximation to a curved line, instead of directly representing a curved line (which is what you really want). Curv will encounter performance problems during preview if you subdivide the spline too much (don't try to feed 1000 vertexes to polygon, it will be too slow). Another approach is needed. This is the point where it is helpful to understand how to write SDF distance functions.

Here's the code for polygon, from lib/curv/std.curv in the repo:

// Finite polygon. May be nonconvex. Edges may cross, and the polygon may
// self intersect. Uses the "crossing number" method to determine which
// points are inside/outside the polygon. It doesn't matter if the vertexes
// are listed in clockwise or counterclockwise order.
// (http://geomalgorithms.com/a03-_inclusion.html)
// Exact distance field.
// From https://www.shadertoy.com/view/wdBXRW
// which is Copyright 2019 Inigo Quilez (The MIT Licence)
polygon v = make_shape {
  dist p =
    do
      local p = p@[X,Y];
      local num = count v;
      local d = dot[p-v@0, p-v@0];
      local s = 1;
      local j = num-1;
      for (i in 0..<num) (
        local e = v@j - v@i;
        local w = p - v@i;
        local b = w - e*clamp[dot[w,e]/dot[e,e], 0, 1];
        d := min[d, dot[b,b]];
        local cond = [p@Y >= v@i@Y, p@Y < v@j@Y, e@X*w@Y > e@Y*w@X];
        if (and cond || and(not cond)) s := -s;
        j := i;
      );
    in s * sqrt d;
  bbox = [[...min v,0], [...max v,0]];
  is_2d = true;
};

The dist function computes the distance from an arbitrary point [x,y] to the closest point on the boundary of the polygon. The result is positive if [x,y] is outside the polygon. It is 0 if the point is on the boundary. It is negative if [x,y] is inside the polygon. There are lots of tutorials explaining the SDF representation, here's the one I wrote: https://github.com/curv3d/curv/blob/master/docs/Theory.rst

The polygon code is magic, I didn't invent it, I just copied it from shadertoy.com and transliterated the code into Curv. The spline code needed for Curv will be magic in a similar way. I hope there is some SDF spline code on the internet that can be adopted.

lf94 commented 2 years ago

Excellent, I will look for an SDF spline then! Thank you for all the excellent help Doug. Your project is crazy awesome.

Edit: I'll leave this issue open until someone or me has implemented it.

theohonohan commented 2 years ago

Hi,

I'm not sure if this is the right place to mention this. I had a look at the current code for sweeping along Bezier curves. I am also aware of a 2D analytic SDF for quadratic Beziers: https://www.shadertoy.com/view/MlKcDD which seems like it could be mentioned here.

lf94 commented 1 year ago

Thank @theohonohan - I've tried such SDFs but they don't work well for the reason of needing to make a closed shape.

lf94 commented 1 year ago

It seems a few smart people have come along and figured out some potential SDFs for this! https://www.shadertoy.com/view/3dtBR4

theohonohan commented 1 year ago

@doug-moen posted some code which referred to Inigo Quilez. He founded ShaderToy and his site is a good place to start: https://iquilezles.org/articles/distfunctions/ https://iquilezles.org/articles/distfunctions2d/

I am not sure what you mean by "the reason of needing to make a closed shape". It's hard not to end up with a closed contour when dealing with SDFs. See https://iquilezles.org/articles/interiordistance/

lf94 commented 1 year ago

@theohonohan you can't just arbitrarily combine SDFs. There are "proper SDFs" that are necessary for further manipulations. An example is the offset manipulation. If you don't have a proper SDF, this will fail.

That example is not using bezier curves. I have studied those exact materials over a year ago and they were not sufficient to create proper SDFs for arbitrary closed shapes.

theohonohan commented 1 year ago

RIght, the third link does discuss the difficulty of making a valid SDF. But the link https://www.shadertoy.com/view/3dtBR4 which you posted just uses min() to intersect SDFs, which is what gave me the idea that you were OK with approximate results.

The problem is not that the resulting shape isn't "closed", though.

doug-moen commented 1 year ago

Here's another one. 3 Bezier splines joined into a closed 2D shape. https://www.shadertoy.com/view/llyXDV