discuss@lists.openscad.org

OpenSCAD general discussion Mailing-list

View all threads

Why Lazy Union cannot survive in its current form

JB
Jordan Brown
Fri, Sep 25, 2026 1:58 PM

On 9/25/2026 12:49 AM, Cory Cross via Discuss wrote:

On 9/24/26 6:45 PM, Jordan Brown via Discuss wrote:

Preserving LU at the top level only would of course be incompatible
with its existing behavior.

If you did as I said (export options) and it was possible (caveat what
you pointed out), it would be backward-compatible. All existing
non-LU-code would render and export the exact same STL.

It would be backward-incompatible with current Lazy Union behavior.

If you're going to be having the model encode this amount of metadata,
I'm thinking you're also going to want to encode modifiers as well,
which means having non-unioned objects taking up the same volume. I
guess you're right, it makes more sense to have an explicit label
("group(part=...)") rather than magically have the top level do this.

Yep, my thinking exactly.

And I had thought that for multi-color/multi-material you would want
union to give the overlap to one side or the other, so that each point
in the shape has exactly one color/material/whatever. But then I learned
about slicers interleaving overlapping colors/materials to form stronger
joints, and between that and the modifier use case I changed over to a
"parts can overlap" model.

Maybe however LU is implemented has some reusable code, or maybe this
is so trivial it doesn't matter.

I'm not sure yet.  I've started some implementation, but haven't gotten
to the geometry end of it yet.

Either you'll need to ban STL export or union things for export;
you'll still need to address the point you made about render vs export.

I figured that when had a multi-part model and did Export-As-STL, it
would in some fashion export multiple files.  (Maybe it would prompt N
times, or maybe it would prompt for a base name and append part names to
it.)

On 9/25/2026 12:49 AM, Cory Cross via Discuss wrote: > On 9/24/26 6:45 PM, Jordan Brown via Discuss wrote: >> Preserving LU at the top level only would of course be incompatible >> with its existing behavior. > If you did as I said (export options) and it was possible (caveat what > you pointed out), it would be backward-compatible. All existing > non-LU-code would render and export the exact same STL. It would be backward-incompatible with current *Lazy Union* behavior. > If you're going to be having the model encode this amount of metadata, > I'm thinking you're also going to want to encode modifiers as well, > which means having non-unioned objects taking up the same volume. I > guess you're right, it makes more sense to have an explicit label > ("group(part=...)") rather than magically have the top level do this. Yep, my thinking exactly. And I had thought that for multi-color/multi-material you would want union to give the overlap to one side or the other, so that each point in the shape has exactly one color/material/whatever. But then I learned about slicers interleaving overlapping colors/materials to form stronger joints, and between that and the modifier use case I changed over to a "parts can overlap" model. > Maybe however LU is implemented has some reusable code, or maybe this > is so trivial it doesn't matter. I'm not sure yet.  I've started some implementation, but haven't gotten to the geometry end of it yet. > Either you'll need to ban STL export or union things for export; > you'll still need to address the point you made about render vs export. I figured that when had a multi-part model and did Export-As-STL, it would in some fashion export multiple files.  (Maybe it would prompt N times, or maybe it would prompt for a base name and append part names to it.)
JB
Jordan Brown
Fri, Sep 25, 2026 2:15 PM

On 9/25/2026 2:23 AM, Guenther Sohler via Discuss wrote:

I think the easiest fix for OpenSCAD would be the formerly proposed
group("name_of_part") approach.

My current thinking is a new marker part(name); see OEP9
https://github.com/openscad/openscad/wiki/OEP9:-Generating-Multiple-%E2%80%9CParts%E2%80%9D-in-One-Run.
group() has a lot of history of being a synonym for union(), and making
it have an additional meaning bothers me.  Also I think the original
intent (never completed) was that group() meant "put these children into
a list without unioning them", and that would be overloading it further.

Unless OpenSCAD some time in the future makes solids 1st class object,

I do want to do that, ref PR#4478.  However, I don't think it's the
right answer here.  I think it's important that you be able to have a
deeply nested "part", that you aren't required to have all of the parts
be directly visible at the top level.

For instance... my house model has a wall() module that builds a wall.
You can give it arguments saying where to put windows, and it cuts holes
in the walls for the windows.  It adds dividers (what are apparently
called mullions, muntins, and transoms
https://en.wikipedia.org/wiki/Transom_(architecture)#/media/File:Casement_window_parts.svg)
to those windows.  You can supply children that add interior and
exterior trim pieces.  I want to be able to make the wall, dividers, and
trim pieces different parts, since they can in real life be different
colors.

This need is especially important for BOSL2, where deeply nesting
components, positioning them relative to one another, is a fundamental
piece of the design.  You want to be able to say something like:

part("red") cuboid(10)
     attach(TOP, BOTTOM)
     part("blue")
     cuboid(10);

to put a blue cube on top of a red cube.

On 9/25/2026 2:23 AM, Guenther Sohler via Discuss wrote: > I think the easiest fix for OpenSCAD would be the formerly proposed > group("name_of_part") approach. My current thinking is a new marker part(name); see OEP9 <https://github.com/openscad/openscad/wiki/OEP9:-Generating-Multiple-%E2%80%9CParts%E2%80%9D-in-One-Run>. group() has a lot of history of being a synonym for union(), and making it have an additional meaning bothers me.  Also I think the original intent (never completed) was that group() meant "put these children into a list without unioning them", and that would be overloading it further. > Unless OpenSCAD some time in the future makes solids 1st class object, I do want to do that, ref PR#4478.  However, I don't think it's the right answer here.  I think it's important that you be able to have a deeply nested "part", that you aren't required to have all of the parts be directly visible at the top level. For instance... my house model has a wall() module that builds a wall. You can give it arguments saying where to put windows, and it cuts holes in the walls for the windows.  It adds dividers (what are apparently called mullions, muntins, and transoms <https://en.wikipedia.org/wiki/Transom_(architecture)#/media/File:Casement_window_parts.svg>) to those windows.  You can supply children that add interior and exterior trim pieces.  I want to be able to make the wall, dividers, and trim pieces different parts, since they can in real life be different colors. This need is especially important for BOSL2, where deeply nesting components, positioning them relative to one another, is a fundamental piece of the design.  You want to be able to say something like: part("red") cuboid(10) attach(TOP, BOTTOM) part("blue") cuboid(10); to put a blue cube on top of a red cube.
JL
Jamie LL
Sat, Sep 26, 2026 12:58 AM

On 9/24/26 5:00 PM, Jordan Brown wrote:

On 9/24/2026 11:24 AM, Jamie Larkby-Lahet via Discuss wrote:

another way of looking at it would be that the implicit unions in
various parts of openscad are the issue, no?

I did pre-emptively answer that:

 Q.  Isn't the LU behavior better?
 A.  For reasons too complicated to get into right now (having to
 do with children()), I don't think so.  But it doesn't matter,
 because LU behavior is not base OpenSCAD's behavior, and breaking
 models in subtle ways is highly undesirable.

if every union was explicit,

Sure.  But there are many years, thousands of users, and thousands of
models that say that it isn't.

my thought was a new operator like your proposed part("name"), but that
is immune to implicit unions. so when it counts, we can ask for
explicit-union style behavior, in a backwards compatible way (actually I
was thinking of named sets of geometry, which has the potential for
producing hierarchical structures, which might be good or bad. but
naming the contents of the set individually as you propose also works
and keeps the namespace flat).

children() can list every item in the curly braces as before, including
parts, but we could also add list_parts() and $num_parts that allow
processing of parts specially. and list_parts("name") would allow
operating on specific parts, if present. interestingly this also
provides a limited form of object references.

for:

A() B(){

    part("A") cube();

    part("B") cube();

    cube();

    sphere();

}

A and B would see 2 child parts, but A would see 3 children and B would
see 4. another way to think of it is that the cube() and sphere() are
implicitly assigned an identical private, internal part name, and this
part does respect implicit unions.

Most transformations would operate on parts individually even when a
group of them are passed though, and they would be individually
difference()'d when several are passed in on the first line of difference().

I would argue that explicit unions, as they may exist in legacy code,
esp for difference()s, should also not apply to parts. instead you
would need a join() operator, which could also potentially select
specific part names for the joining to apply to.

It looks like your proposal implicitly unions parts with the same name.
Hierarchical naming of sets of parts would be a way around that, if it
is deemed desirable.

translate(...) { cube(); sphere(); } either implicitly unions, or
doesn't.  And existing practice is that it doesn't.

interesting, because:

intersection() {
  translate([0,0,0]){
    for(x=[-1,1])
        translate([x,0,0]*10)
            sphere(15);
  }
  cube(25, center=true);
}

with lazy union on behaves identically to lazy union off.

same goes for:

intersection() {
  a(){
    for(x=[-1,1])
        translate([x,0,0]*10)
            sphere(15);
  }
  cube(25, center=true);
}

module a() {
  children();
}

On 9/24/26 5:00 PM, Jordan Brown wrote: > On 9/24/2026 11:24 AM, Jamie Larkby-Lahet via Discuss wrote: >> another way of looking at it would be that the implicit unions in >> various parts of openscad are the issue, no? > > I did pre-emptively answer that: > > Q.  Isn't the LU behavior better? > A.  For reasons too complicated to get into right now (having to > do with children()), I don't think so.  But it doesn't matter, > because LU behavior is not base OpenSCAD's behavior, and breaking > models in subtle ways is highly undesirable. > >> if every union was explicit, > > Sure.  But there are many years, thousands of users, and thousands of > models that say that it isn't. > my thought was a new operator like your proposed part("name"), but that is immune to implicit unions. so when it counts, we can ask for explicit-union style behavior, in a backwards compatible way (actually I was thinking of named sets of geometry, which has the potential for producing hierarchical structures, which might be good or bad. but naming the contents of the set individually as you propose also works and keeps the namespace flat). children() can list every item in the curly braces as before, including parts, but we could also add list_parts() and $num_parts that allow processing of parts specially. and list_parts("name") would allow operating on specific parts, if present. interestingly this also provides a limited form of object references. for: A() B(){     part("A") cube();     part("B") cube();     cube();     sphere(); } A and B would see 2 child parts, but A would see 3 children and B would see 4. another way to think of it is that the cube() and sphere() are implicitly assigned an identical private, internal part name, and this part does respect implicit unions. Most transformations would operate on parts individually even when a group of them are passed though, and they would be individually difference()'d when several are passed in on the first line of difference(). I would argue that explicit unions, as they may exist in legacy code, esp for difference()s, should _also_ not apply to parts. instead you would need a join() operator, which could also potentially select specific part names for the joining to apply to. It looks like your proposal implicitly unions parts with the same name. Hierarchical naming of sets of parts would be a way around that, if it is deemed desirable. > translate(...) { cube(); sphere(); } either implicitly unions, or > doesn't.  And existing practice is that it doesn't. > interesting, because: intersection() {   translate([0,0,0]){     for(x=[-1,1])         translate([x,0,0]*10)             sphere(15);   }   cube(25, center=true); } with lazy union on behaves identically to lazy union off. same goes for: intersection() {   a(){     for(x=[-1,1])         translate([x,0,0]*10)             sphere(15);   }   cube(25, center=true); } module a() {   children(); }
JB
Jordan Brown
Sat, Sep 26, 2026 2:57 AM

On 9/25/2026 6:58 PM, Jamie LL wrote:

my thought was a new operator like your proposed part("name"), but
that is immune to implicit unions. so when it counts, we can ask for
explicit-union style behavior, in a backwards compatible way (actually
I was thinking of named sets of geometry, which has the potential for
producing hierarchical structures, which might be good or bad. but
naming the contents of the set individually as you propose also works
and keeps the namespace flat).

part(), as proposed, is in a sense immune to implicit union. Actually,
it's mostly immune to explicit union too.  Not completely, because if
you union two pieces that have the same part name, they do get unioned.

To a first approximation, you could say that all pieces that are inside
part("A") are in their own isolated universe, interacting with one
another but not with pieces in other parts.  (And it is tempting to
define it exactly that way, since it would provide a simple and uniform
definition across all operations.  But I'm not sure it's the most useful
definition.)

I think you want that, because I think that if you have ten separated
pieces that are all to be green, you want them to end up in one part in
the resulting mesh file (typically 3MF) so that you only need to
associate that one part with the green filament.

But we are mostly agreeing.  Lazy Union in its current form is not the
answer.  We need something new.  Exactly what... that's a good
question.  part() is my stab, but I don't claim that its implications
are fully thought-out.

we could also add list_parts() and $num_parts that allow processing of
parts specially.

I believe you'll find that there is still a chicken-and-egg problem. 
You need to evaluate the child before you can determine whether it
contains parts, or how many of them... and we don't have a way to
evaluate a child without adding the result to the model.  If we had a
way to evaluate a child and temporarily store the result, so that we
could examine its structure, things would be different... but we don't.

It looks like your proposal implicitly unions parts with the same name.

Yes.  (Give or take some uncertainty as to how parts interact with
intersection, difference, hull, minkowski, projection, et cetera.)

 Hierarchical naming of sets of parts would be a way around that, if
it is deemed desirable.

Mostly, don't call part() with the same name if you don't intend the
results to be (eventually) unioned.  How you come up with unique names
is your business.  The current generation of the proposal says that
part() with no name will construct a unique name, so if you don't need
to identify the part and you want it to be separated, you can use that
and not have to come up with unique names on your own.

translate(...) { cube(); sphere(); } either implicitly unions, or
doesn't.  And existing practice is that it doesn't.

Oh, oops.  My error.  "doesn't" should be "does".

translate() implicitly unions, with or without lazy union. There's only
a limited set of operators that don't implicitly union, even with LU
enabled:  children(), echo(), assert(), let(), for(), if().

What I was saying was that getting rid of implicit union is
intrinsically incompatible with standard OpenSCAD.  You can come up with
schemes that explicitly don't union (like part()), but then you haven't
gotten rid of implicit union.

same goes for:

intersection() {
  a(){
    for(x=[-1,1])
        translate([x,0,0]*10)
            sphere(15);
  }
  cube(25, center=true);
}

module a() {
  children();
}

The results of the module call are wrapped in a group().  I don't know
for sure whether they are unioned or not (I think they are), but they
end up as a single child for the intersection.  Check the CSG tree:

intersection() {
   group() {
     multmatrix([[1, 0, 0, -10], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) {
       sphere($fn = 0, $fe = 0, $fa = 12, $fs = 2, r = 15);
     }
     multmatrix([[1, 0, 0, 10], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) {
       sphere($fn = 0, $fe = 0, $fa = 12, $fs = 2, r = 15);
     }
   }
   cube(size = [25, 25, 25], center = true);
}

But note that LU kept the for() from generating its own group().

As best I can tell, the semantics of Lazy Union in its current form can
be described as:

Some, but not all, operations will not implicitly union their
children.  Those un-unioned children will be considered to be
separate children when processed by some, but not all, parents.
Un-unioned children that survive to the top level will be exported
as separate 3MF parts, or as potentially overlapping and invalid STL
polyhedra.

Strange women lyin' in ponds distributin' swords is no basis for a
system of government.  "Some, but not all", especially repeated, is no
basis for a solid and understandable design.

On 9/25/2026 6:58 PM, Jamie LL wrote: > > my thought was a new operator like your proposed part("name"), but > that is immune to implicit unions. so when it counts, we can ask for > explicit-union style behavior, in a backwards compatible way (actually > I was thinking of named sets of geometry, which has the potential for > producing hierarchical structures, which might be good or bad. but > naming the contents of the set individually as you propose also works > and keeps the namespace flat). > part(), as proposed, is in a sense immune to implicit union. Actually, it's mostly immune to explicit union too.  Not completely, because if you union two pieces that have the same part name, they *do* get unioned. To a first approximation, you could say that all pieces that are inside part("A") are in their own isolated universe, interacting with one another but not with pieces in other parts.  (And it is tempting to define it exactly that way, since it would provide a simple and uniform definition across all operations.  But I'm not sure it's the most useful definition.) I think you want that, because I think that if you have ten separated pieces that are all to be green, you want them to end up in one part in the resulting mesh file (typically 3MF) so that you only need to associate that one part with the green filament. But we are mostly agreeing.  Lazy Union in its current form is not the answer.  We need something new.  Exactly what... that's a good question.  part() is my stab, but I don't claim that its implications are fully thought-out. > we could also add list_parts() and $num_parts that allow processing of > parts specially. > I believe you'll find that there is still a chicken-and-egg problem.  You need to evaluate the child before you can determine whether it contains parts, or how many of them... and we don't have a way to evaluate a child without adding the result to the model.  If we had a way to evaluate a child and temporarily store the result, so that we could examine its structure, things would be different... but we don't. > It looks like your proposal implicitly unions parts with the same name. > Yes.  (Give or take some uncertainty as to how parts interact with intersection, difference, hull, minkowski, projection, et cetera.) >  Hierarchical naming of sets of parts would be a way around that, if > it is deemed desirable. > Mostly, don't call part() with the same name if you don't intend the results to be (eventually) unioned.  How you come up with unique names is your business.  The current generation of the proposal says that part() with no name will construct a unique name, so if you don't need to identify the part and you want it to be separated, you can use that and not have to come up with unique names on your own. >> translate(...) { cube(); sphere(); } either implicitly unions, or >> doesn't.  And existing practice is that it doesn't. >> Oh, oops.  My error.  "doesn't" should be "does". translate() implicitly unions, with or without lazy union. There's only a limited set of operators that don't implicitly union, even with LU enabled:  children(), echo(), assert(), let(), for(), if(). What I was saying was that getting rid of implicit union is intrinsically incompatible with standard OpenSCAD.  You can come up with schemes that explicitly don't union (like part()), but then you haven't gotten rid of implicit union. > same goes for: > > intersection() { >   a(){ >     for(x=[-1,1]) >         translate([x,0,0]*10) >             sphere(15); >   } >   cube(25, center=true); > } > > module a() { >   children(); > } > The results of the module call are wrapped in a group().  I don't know for sure whether they are unioned or not (I think they are), but they end up as a single child for the intersection.  Check the CSG tree: intersection() { group() { multmatrix([[1, 0, 0, -10], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) { sphere($fn = 0, $fe = 0, $fa = 12, $fs = 2, r = 15); } multmatrix([[1, 0, 0, 10], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) { sphere($fn = 0, $fe = 0, $fa = 12, $fs = 2, r = 15); } } cube(size = [25, 25, 25], center = true); } But note that LU kept the for() from generating its own group(). As best I can tell, the semantics of Lazy Union in its current form can be described as: Some, but not all, operations will not implicitly union their children.  Those un-unioned children will be considered to be separate children when processed by some, but not all, parents. Un-unioned children that survive to the top level will be exported as separate 3MF parts, or as potentially overlapping and invalid STL polyhedra. Strange women lyin' in ponds distributin' swords is no basis for a system of government.  "Some, but not all", especially repeated, is no basis for a solid and understandable design.