Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Functions that can query a given 3D shape and provide size or location info #301

Open
filipmu opened this Issue · 35 comments
@filipmu

All openscad functions take parameters as input and produce shapes. It would be useful to have functions that take shapes as input and produce parameters. This would be useful for aligning shapes. Some examples are max/min dimensions on each axis, center of gravity, etc. There are a number of uses of this feature: 1-since all shapes in openscad are polyhedrons, spheres and cylinders do not render exactly as specified. So there is no way to exactly align shapes based only on input parameters. 2-To align two objects created by separate modules, the user needs to know the inner workings of each module to calculate how to align them. 3- Application of complex transformations like Minkowski and Convex Hull or many CSG combinations create shapes with very complex boundaries that can't be easily calculated.

Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.

@ivoknutsel

What would the syntax for these functions and their use look like ?

@GilesBathgate

For now It doesn't really matter how they look like until someone can come up with a sensible way to implement it.

The problem is this:

To find the boundaries of objects you have to first evaluate the script into its CSG result. At this point you then have to feed those boundary values back into the script and evaluate the CSG result again. If the value from the first evaluation effects the result of the boundary in the second evaluation you will have to feed those boundary values back into the script and evaluate the CSG again, and of course this process would go in forever in an infinite loop.

At least that's the fundamental problem with it the way I see it. Perhaps I've misunderstood.

Regards,
Giles

@ivoknutsel

I have more or less the same problem, i create geometries in nested modules. In the root i need to position the geometries relative to each other based on the length or another function of those geometries.

As i understand OpenSCAD, geometries are always leaves in a model tree and the tree and all variables are calculated compile time. There is no way to assign a geometry or the result of a module to a variable, modules don't return values but can only (with "child()") modify or disable branches in the model tree.

I am an OpenSCAD novice and may be completely wrong.

@nophead
@TakeItAndRun
@nophead
@GilesBathgate

Rapcad implements a bounds module. It just echos the bounds of its children to the console, which you can then copy back into the script. Its still hardcoded numbers but I think its better than nothing. Still can't get my head around how it would feed the value back into the script without re-evaluation.

On the other hand as nophead points out a stl_dim() function (equivalent to dxf_dim()) would be nice.

Regards,
Giles

@nophead
@kintel
Owner

If we handled imports similar to how we handle use and include, this would work.
Imported objects would then become resources which can be used by the script.
Naturally, the script then cannot be evaluated before files are loaded, but that's decent enough I'd think.

RapCad actually started on doing this by introducing a new import syntax: import <myfile.stl> as myfile.

Ideally, myfile would in this case become a variable which could be queried, e.g. myfile.bounds (not sure what else would be interesting to query for).

Also, there already is a similar hack in OpenSCAD: We have have dxfdim() function, which reads dimensions from a DXF file and makes it available to the script. I really don't like that function, but it's there :/

@kintel
Owner

Related to this, the new resize() module might provide a workflow for some of the cases where a query would be the obvious solution.

See https://github.com/openscad/openscad/blob/master/testdata/scad/features/resize-2d-tests.scad and https://github.com/openscad/openscad/blob/master/testdata/scad/features/resize-tests.scad

@NateTG

... At this point you then have to feed those boundary values back into the script and evaluate the CSG result
again.

My understanding is that variables can only have their value set once in OpenSCAD, so if you could assign a mesh to a variable, and then operate on the variable, you should never get in trouble. Since you can't alter the underlying variable after getting its dimensions.

@nophead
@kintel
Owner

Assignments are collected over the entire scope and evaluated first (but in individual order of each variable's last assignment). I don't see how this would cause problems.
However, to be able to assign meshes to variables, we'd need to be able to evaluate the corresponding geometry inline with the parser, which is a major architecture change.

@ablapo

Here is my idea. Just make a rough estimate, that would be sufficient for most things.
Do you think it is possible to use the hull() function on an imported STL-file and remember that hull-data for later function calls. And is it possible to get some info from that hull data?
hull_import (hulldata, "3D_Object.stl");

length= outbound_length ( angle [x,y,z], hulldata); -> gives the length to boarder from the center (0,0,0)
vertex=outbound_vertex ( angle [x,y,z], hulldata); -> gives the 3d-vertex of the hitting face of the hull

With that tools you could arrange elements around a stl-file. Make Masks from 3D scanned humans. Make cloth around scanned bodies.

Maybe split that into two parts, if its too difficult.
1. Step: generate a .dat -file with a hull data from a stl.
2. Step: use that .dat file to get the important values

@GilesBathgate

Can we add a FAQ to the main OpenSCAD website? this question/idea keeps being asked/suggested.

@kintel
Owner

Yes, we should start a FAQ

@blobule

I just did an interesting experiment on the issue of "measuring"...

We all known that openscad parse the code first, builds a csg tree, the evaluate the csg tree to obtain the final geometry. Because of that, any measure on the geometry is only available long after the parsing is done.

However, nothing prevents a partial evaluation of the csg tree during parsing... Here what I did to achieve this:

The command "render()" now does the csg rendering of its subtree (its childs) immediately during parsing, and it computes the bounding box of the resulting polyset geometry.
To make this information available to the code, it simply defines variables ($xmin, $xmax, ...) in the context of its parsing parent. This means that this will work:

render() sphere(r=10);
echo("bbox is ",$xmin,$xmax,$ymin,$ymax,$zmin,$zmax);

Of course, since this is done during parsing, those information are only available to what follows render() in the code.
Because there is a cache system for geometry, this "early evaluation" for render does not seem to impact performance.

Here is an example adding walls around an object to illustrate the bounding box:

bbox-a

The code looks like this:

module openbbox(ep=3,sc=0.7) {
    render() child(0); // draw and measure the child
    echo("bbox X is ",$xmin,$xmax);
    echo("bbox Y is ",$ymin,$ymax);
    echo("bbox Z is ",$zmin,$zmax);
    color("blue",0.2) union() {
        translate([$xmin-ep/2,0,0]) scale([1,sc,sc]) cube([ep,$ymax-$ymin,$zmax-$zmin],center=true);
        translate([$xmax+ep/2,0,0]) scale([1,sc,sc]) cube([ep,$ymax-$ymin,$zmax-$zmin],center=true);
        translate([0,$ymin-ep/2,0]) scale([sc,1,sc]) cube([$xmax-$xmin,ep,$zmax-$zmin],center=true);
        translate([0,$ymax+ep/2,0]) scale([sc,1,sc]) cube([$xmax-$xmin,ep,$zmax-$zmin],center=true);
        translate([0,0,$zmin-ep/2]) scale([sc,sc,1]) cube([$xmax-$xmin,$ymax-$ymin,ep],center=true);
        translate([0,0,$zmax+ep/2]) scale([sc,sc,1]) cube([$xmax-$xmin,$ymax-$ymin,ep],center=true);
    }
}
openbbox() rotate(rands(-90,90,3,1234)) cylinder(r=10,h=30,center=true);

This means that once an object is created, it is easy to place other objects relative to it. What about measuring geometry before using it, so we can place the object according to its size? What we need is a way to "render but don' t use the resulting geometry" . I ended up using '%', which seems to do exactly that:

%render() sphere(r=10);

Here is an example where I stack objects according to their measured sizes:

bbox-b

The code (a little ugly, sorry) :

module randcylinder(seed=1234) { rotate(rands(-90,90,3,seed)) cylinder(r=10,h=30,center=true); }
module base(h=5) { cube([30,30,h],center=true); }

// stack the second child so its resting on top of second child
module stack() {
        render() child(0); // the "base"
        // use the zmax of this object as a reference
        assign(alt=$zmax) {
                %render() child(1); // do not keep this geometry
                translate([0,0,-$zmin+alt]) child(1); // show the child at the right place
        }
}

stack() {
        stack() {
                stack() {
                        stack() {
                                stack() {
                                        stack() {
                                                base(h=5);
                                                randcylinder(100);
                                        }
                                        base(h=10);
                                }
                                randcylinder(101);
                        }
                        base(h=2);
                }
                randcylinder(102);
        }
        base(h=4);
}

Before I submit this, I would like to know what people think of this approach to measuring. Also, my changes are too much of a "hack" at this point and need a bit of work to integrate nicely.

@TakeItAndRun
@nophead
@blobule

Indeed, it does break the assignment rule.

How about, instead of changing render(), we define a function similar to assign(), say assignbbox(), which will define in a local context the bounding box variables (xmin,xmax,...) according to the immediate rendering of its first child?

assignbbox() {
     sphere(r=5); // first child, used to compute bbox
     // from here, any geometry can use xmin,xmax,...
     // The bounding box info is only valid inside the assignbbox
}

Would this work?

In general, maybe we could have a "probe()" command which would create a local context just like assign, but were all defined variables would come from analyzing the immediate rendering of the first child. The parameters of probe() would determine what we wish to compute:

probe(boundingbox=true) {
    geometryToProbe();
    geometryUsingTheComputedBoundingBox();
}

As mentioned in previous posts, we could think of other options, like finding the intersection of the geometry with one axis, or an extreme point in some direction, well, any computation on the first child would be assigned as local variables for the following childs.

@MichaelAtOz

While I like the possibilities, calling render() defeats the concept of OpenSCAD.
It uses OpenCSG (F5) for fast preview & CGAL (F6) for proper geometry.
Using render() defeats F5's benefits. (but does allow additional functions)
... two minds...
I assume OpenCSG can't do a bounding box??

@kintel
Owner

The OpenSCAD architecture is based around evaluating the source code into a concrete CSG tree before attempting to render. The render() module is really a workaround for rendering artifacts and performance issues and will hopefully eventually become less and less needed.

This suggestion attempts to bring compiling and rendering closer together in order to create some feedback loop opportunities. It's worth a discussion, but I feel that if we were to go in this direction we should take a more careful look at why/if we should continue to keep compiling and rendering as separate as we do today, rather than patching it up to fix symptoms.

@blobule

I like the simplicity we get by separating compiling and rendering. This approach puts an emphasis on building an object using known components, which is a great way to approach CSG modelling, in my opinion.

However, occasionally, a design will require feedback information about one of its components. Right now, such design can' t be done on openscad. The workaround is to render the component and externally analyze it, then re-compile the design, which is tedious.

By adding a command such as "probe_bounding_box() { }", you force the designer to explicitly ask for this information, and thus clearly state that this should be used only when really needed. This is ideal in my opinion since automatically computing the bounding box of all components (i.e. rendering while compiling) would hurt performance and change the approach of openscad to CSG modelling.

So basically I consider the "separate compile/render" model very good, but an occasional "on-demand component information" when the design needs it would add a lot of power.

@nophead
@MichaelAtOz

you never need to query the geometry unless it is imported as an STL or DXF

I found resize() handy in those situations, make the STL fit to your spec. Won't help for all.

@MichaelAtOz

Bigger picture.
I had though that a capability to produce multiple parts (exports e.g. STLs) from one scad program would be handy, something akin to Thingiverse Customizer 'Part' (which I believe just runs the .scad multiple times in batch mode).
So if there was something like

export(file=str("myfile",part,".stl")
    render() // maybe this should just be implicitly done as part of export
        my_object(part);

Program logic (for/if etc) can then handle an assembly, and if combined with Marius idea from above

import <myfile.stl> as myfile
myfile would in this case become a variable which could be queried, e.g. myfile.bounds

Could then be used to iteratively build upon previous 'part's, automate a build layout based on size etc.

@blobule

It is true that, in theory, the declarative approach of openscad implies that everything can be computed "in advance", before the shape is actually rendered. This is what makes the "fast preview" (F5) possible and useful.

If we had to render() everything with CGAL before getting a preview, that would defeat the original idea of a "fast preview" using openCSG.

However, even simple shapes can be very hard to compute "by hand" just to get a boundary point. As an example, consider this model I just made:

probe6b

The "pistons" use the bounding box of the green shape to select their position. The shape is the projection of a cone, and even if it is simple, there is no obvious (i.e. easy to derive) equation to get the boundary points.
In this case, rendering the shape while parsing is quite cheap, so there is no problem with the fast preview.
In my opinion, the code is much simpler to write and more "natural" than with lots of equations.

Here is the code:

module forme(c=true) {
    projection(cut=c) {
        rotate([45,0,0]) cylinder(r1=20,r2=0,h=40,center=true,$fn=64);
    }
}

module cam(t=0) {
    rotate([t*360,0,0]) translate([-10,0,0]) rotate([0,90,0]) linear_extrude(height=20) forme(false);
}

module piston(t=0.5) {
    probe() {
                // check along the z-axis by intersecting with a cube
        % render() intersection() {
            child(0);
            translate([0,0,0]) cube([0.001,0.001,80],center=true);
        }
                // variables xmin,ymin,zmin,xmax,ymax,zmax are available for the following nodes
        echo(zmin);
        translate([0,0,-40+zmin]) {
            cylinder(r1=5,r2=0,h=40);   
            translate([0,0,-10]) cylinder(r=30,h=10);
        }
    }
    %color("red",0.2) translate([0,0,-32-48]) cylinder(r=31,h=32);
}

color("limegreen") cam($t);
color("limegreen") rotate([$t*360,0,0]) rotate([0,90,0]) cylinder(r=3,h=100,center=true,$fn=6);
rotate([-120,0,0]) piston($t,120)  rotate([120,0,0])  cam($t);
rotate([0,0,0])       piston($t,0)       rotate([0,0,0])       cam($t);
rotate([120,0,0])  piston($t,-120) rotate([-120,0,0]) cam($t);

The probe() function I coded is similar to assign(), but simply performs a CGAL evaluation of its first child, then defines variables related to the geometry (like xmin,xmax,...) which are only available to the following children inside probe() (in the local context). This way, there is no violation of assignment rules.

So this is my latest attempt at tackling the querying of shapes.

@MichaelAtOz

Nice model!

@GilesBathgate

@MichaelAtOz Sure its a nice model, the fact that he has implemented a probe() module is much more impressive. I like the idea, it solves a lot of problems (relating to recursion) that I couldn't get my head around when considering this before.

@blobule

(following a comment in issue #586 ) The code for probe() is on github, in the branch "probe" of my fork of openscad ( https://github.com/blobule/openscad ). Feel free to check it out. It is not fully tested (not ready for a pull request I assume), but it already works well.

Quick documentation of the module:

probe() {
      some_geometry; // this is the reference geometry
      some_other_geometry; // this and following can use information about reference geometry
}

probe() will render the first child with CGAL, gather some information about the geometry, define in a local context some variables holding this information, and then evaluate the remaining children with this context.
The variables defined are related to the bounding box of the first child:
xmin, xmax, ymin, ymax, zmin, zmax, and also center=[(xmax+xmin)/2,...] and boxsize=[xmax-xmin,...]

Note that the first child of probe() is discarded after it is computed, to make it easy to use objects for the only purpose of aligning things.

Examples:

probe() {
    cylinder(r=10,h=40);
    echo(center,boxsize);
    #translate(center) cube(boxsize,center=true);
}
////////////////////////////////
// place child(0) on top of child(1)
module stack() {
    probe() {
        children(1);
        translate([0,0,zmax]) probe() {
            children(0);
            translate([0,0,-zmin]) children(0);
        }
    }
    children(1);
}
stack() {
    rotate([10,20,30]) cylinder(r=10,h=40,center=true);
    cube([30,30,5],center=true);
}
///////////////////////////////
// simple resize module
module setsize(sz=[10,10,10]) {
    probe() {
        children(0);
        echo(center,boxsize);
        translate(center)
        multmatrix(m = [ 
            [1/boxsize[0], 0, 0, 0],
            [0, 1/boxsize[1], 0,0],
            [0, 0, 1/boxsize[2], 0],
            [0, 0, 0,  1]
            ])
        scale(sz)
        translate(-center)
        children(0);
    }
}
setsize([5,10,15]) sphere(r=4);
setsize([5,10,25]) translate([10,20,30]) sphere(r=4);
//////////////////////////
// to move an object's center at the origine
module centerize() {
    probe() {
        children(0);
        echo(center,boxsize);
        translate(-center) children(0);
    }
}
centerize() translate([10,20,30]) sphere(r=10);
@nophead
@blobule

yes, the cache is on the "rendering" side of things, so the cache is working in this case.

@obobo2
@blobule

As I understand, the internal representation is always polygonal.

This is why the bounding box of a sphere(r=2,$fn=16) is +- 3.696 while for the sphere(r=2,$fn=128) it is +-3.999.

@lordofhyphens

#1155 might be a useful step towards a solution, especially if you allow for iterative compilation.

@blobule blobule referenced this issue
Open

Probe #1388

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.