discuss@lists.openscad.org

OpenSCAD general discussion Mailing-list

View all threads

chamfering joints with hull()

JB
Jon Bondy
Fri, Dec 12, 2025 12:42 AM

Thank you!  I tossed that into my bag of tricks!

On 12/11/2025 7:00 PM, Rudolf via Discuss wrote:

I like the idea behind your approach and kind of rewrote it into a
more generic solution  by introducing a global variable $camfer and
camfer-enabled modules cube_(), cylinder_() and sphere_() that allow
to be scaled by $camfer.

$fn=100;
$camfer=1;

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

module camfer(x=1.2)
{
  for(i=[0:$children-1])
  children(i);

  for(i=[0:$children-1], j=[0:$children-1])
  if(i<j)
  hull()
  {
    intersection()
    {
      children(i);
      children(j, $camfer=x);
    }
    intersection()
    {
      children(i, $camfer=x);
      children(j);
    }
  }
}
module cube_(size, center) scale($camfer)cube(size, center);
module sphere_(r, d) scale($camfer)sphere(r=r, d=d);
module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r,
d=d, center=center);

camfer()
{
  translate(pos1) cube_(10, center=true);
  translate(pos2) rotate(a=[20,20,0]) cube_([6, 5, 5], center=true);
}

translate([20, 0, 0])
  camfer()
  {
    translate(pos1) sphere_(d=size1);
    translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder_(h=size1, d=size2, center=true);
    translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder_(h=size1, d=size2, center=true);
  }

Am 08.12.2025 um 00:03 schrieb Todd Allen via Discuss:

module centered_scale(center_point, scale_vector) {
    translate(center_point)
        scale(scale_vector)
            translate(-1*center_point)
                children(0);
}

// chamfered joining of objects
// unions objects with a hull of those objects limited to the
intersection of the objects after scaling
// adjust object scale factors to change the size and angle of chamfers
// all=true then joints between all children are chamfered
// all=false only the joints between the 1st child and the other
children are chamfered
module hulled_join(centered_scales, all=false) {
    union() {
        children();
        for(i=[0:1:all?$children-2:0])
            for(j=[i+1:1:$children-1])
                hull() intersection() {
                    centered_scale(centered_scales[i][0],
centered_scales[i][1]) children(i);
                    centered_scale(centered_scales[j][0],
centered_scales[j][1]) children(j);
                    union() {
                        children(i);
                        children(j);
                    }
                }
    }
}

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

//  EXAMPLE 1
translate([-size1,0,0])
hulled_join([
    [pos1, (1+2.0/size1)[1,1,1]],
    [pos2, (1+1.0/size2)
[1,1,1]],
    [pos2, (1+1.0/size2)*[1,1,1]]
], $fn=128)
{
    translate(pos1)
        sphere(d=size1);
    translate(pos2)
        rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1,
d=size2, center=true);
    translate(pos2)
        rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1,
d=size2, center=true);
}

//  EXAMPLE 2
translate([size1,0,0])
hulled_join([
    [pos1, (1+1.0/size1)[1,1,1]],
    [pos2, (1+2.0/size2)
[1,1,1]],
    [pos2, (1+2.0/size2)*[1,1,1]]
], all=true, $fn=128)
{
    translate(pos1)
    sphere(d=size1);
    translate(pos2)
    rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2,
center=true);
    translate(pos2)
    rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2,
center=true);
}

hulljoin.png

On Sat, Dec 6, 2025 at 4:23 PM Todd Allen speedebikes@gmail.com wrote:

 Here's an example of how I sometimes chamfer joints using
 hull().  I have found this useful but it would be more useful if
 a similar method could be applied between parent and child
 objects using BOSL2's positioning/alignment/attachment but I
 don't yet see a good way and would love it if someone could help
 me do it.

 module centered_scale(center_point, scale_vector) {
     translate(center_point)
         scale(scale_vector)
             translate(-1*center_point)
                 children(0);
 }

 // chamfered joining of objects
 // unions objects with a hull of those objects limited to the
 intersection of the objects after scaling
 // adjust object scale factors to change the size and angle of
 the chamfer
 module hulled_join(centered_scales) {
     union() {
         children();
         hull() intersection() {
             intersection_for(i=[0:1:$children-1])
                 centered_scale(centered_scales[i][0],
 centered_scales[i][1])
                     children(i);
             union()
                 children();
         }
     }
 }

 size1 = 10;
 size2 = 5;
 pos1 = [0,0,0];
 pos2 = [size1*0.5+size2*0.3,0,0];

 //  EXAMPLE 1
 hulled_join([
     [pos1, (1+1.5/size1)*[1,1,1]],
     [pos2, (1+1.0/size2)*[1,1,1]]
     ])
 {

OpenSCAD mailing list
To unsubscribe send an email todiscuss-leave@lists.openscad.org

--
This email has been checked for viruses by AVG antivirus software.
www.avg.com

Thank you!  I tossed that into my bag of tricks! On 12/11/2025 7:00 PM, Rudolf via Discuss wrote: > > I like the idea behind your approach and kind of rewrote it into a > more generic solution  by introducing a global variable $camfer and > camfer-enabled modules cube_(), cylinder_() and sphere_() that allow > to be scaled by $camfer. > > $fn=100; > $camfer=1; > > size1 = 10; > size2 = 5; > pos1 = [0,0,0]; > pos2 = [size1*0.5+size2*0.3,0,0]; > > > module camfer(x=1.2) > { >   for(i=[0:$children-1]) >   children(i); > >   for(i=[0:$children-1], j=[0:$children-1]) >   if(i<j) >   hull() >   { >     intersection() >     { >       children(i); >       children(j, $camfer=x); >     } >     intersection() >     { >       children(i, $camfer=x); >       children(j); >     } >   } > } > module cube_(size, center) scale($camfer)cube(size, center); > module sphere_(r, d) scale($camfer)sphere(r=r, d=d); > module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, > d=d, center=center); > > > camfer() > { >   translate(pos1) cube_(10, center=true); >   translate(pos2) rotate(a=[20,20,0]) cube_([6, 5, 5], center=true); > } > > translate([20, 0, 0]) >   camfer() >   { >     translate(pos1) sphere_(d=size1); >     translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) > cylinder_(h=size1, d=size2, center=true); >     translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) > cylinder_(h=size1, d=size2, center=true); >   } > > > > > Am 08.12.2025 um 00:03 schrieb Todd Allen via Discuss: >> module centered_scale(center_point, scale_vector) { >>     translate(center_point) >>         scale(scale_vector) >>             translate(-1*center_point) >>                 children(0); >> } >> >> // chamfered joining of objects >> // unions objects with a hull of those objects limited to the >> intersection of the objects after scaling >> // adjust object scale factors to change the size and angle of chamfers >> // all=true then joints between all children are chamfered >> // all=false only the joints between the 1st child and the other >> children are chamfered >> module hulled_join(centered_scales, all=false) { >>     union() { >>         children(); >>         for(i=[0:1:all?$children-2:0]) >>             for(j=[i+1:1:$children-1]) >>                 hull() intersection() { >>                     centered_scale(centered_scales[i][0], >> centered_scales[i][1]) children(i); >>                     centered_scale(centered_scales[j][0], >> centered_scales[j][1]) children(j); >>                     union() { >>                         children(i); >>                         children(j); >>                     } >>                 } >>     } >> } >> >> size1 = 10; >> size2 = 5; >> pos1 = [0,0,0]; >> pos2 = [size1*0.5+size2*0.3,0,0]; >> >> //  EXAMPLE 1 >> translate([-size1,0,0]) >> hulled_join([ >>     [pos1, (1+2.0/size1)*[1,1,1]], >>     [pos2, (1+1.0/size2)*[1,1,1]], >>     [pos2, (1+1.0/size2)*[1,1,1]] >> ], $fn=128) >> { >>     translate(pos1) >>         sphere(d=size1); >>     translate(pos2) >>         rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, >> d=size2, center=true); >>     translate(pos2) >>         rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, >> d=size2, center=true); >> } >> >> //  EXAMPLE 2 >> translate([size1,0,0]) >> hulled_join([ >>     [pos1, (1+1.0/size1)*[1,1,1]], >>     [pos2, (1+2.0/size2)*[1,1,1]], >>     [pos2, (1+2.0/size2)*[1,1,1]] >> ], all=true, $fn=128) >> { >>     translate(pos1) >>     sphere(d=size1); >>     translate(pos2) >>     rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2, >> center=true); >>     translate(pos2) >>     rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2, >> center=true); >> } >> >> hulljoin.png >> >> On Sat, Dec 6, 2025 at 4:23 PM Todd Allen <speedebikes@gmail.com> wrote: >> >> Here's an example of how I sometimes chamfer joints using >> hull().  I have found this useful but it would be more useful if >> a similar method could be applied between parent and child >> objects using BOSL2's positioning/alignment/attachment but I >> don't yet see a good way and would love it if someone could help >> me do it. >> >> module centered_scale(center_point, scale_vector) { >>     translate(center_point) >>         scale(scale_vector) >>             translate(-1*center_point) >>                 children(0); >> } >> >> // chamfered joining of objects >> // unions objects with a hull of those objects limited to the >> intersection of the objects after scaling >> // adjust object scale factors to change the size and angle of >> the chamfer >> module hulled_join(centered_scales) { >>     union() { >>         children(); >>         hull() intersection() { >>             intersection_for(i=[0:1:$children-1]) >>                 centered_scale(centered_scales[i][0], >> centered_scales[i][1]) >>                     children(i); >>             union() >>                 children(); >>         } >>     } >> } >> >> size1 = 10; >> size2 = 5; >> pos1 = [0,0,0]; >> pos2 = [size1*0.5+size2*0.3,0,0]; >> >> //  EXAMPLE 1 >> hulled_join([ >>     [pos1, (1+1.5/size1)*[1,1,1]], >>     [pos2, (1+1.0/size2)*[1,1,1]] >>     ]) >> { >> > > _______________________________________________ > OpenSCAD mailing list > To unsubscribe send an email todiscuss-leave@lists.openscad.org -- This email has been checked for viruses by AVG antivirus software. www.avg.com
R
Rudolf
Fri, Dec 12, 2025 11:11 AM

The "trick" is definitely a must-have. However, from this point the
fillet() operator just takes a snap of the fingers.

You tell the Fillet() operator the # iterations and the limits ... and
you get this: I am thrilled!

$fn=100;
$camfer=1;

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

module Fillet(x=1.5, y=1.5, iterations=10)
{
  for(i=[0:$children-1]) children(i);

  for(k=[0:iterations-1]) // fillet iteration
  {
    x_ = 1+ k*(x-1)/iterations;
    y_ = y - k*(y-1)/iterations;

    for(i=[0:$children-1], j=[0:$children-1]) // Chamfer
    if(i<j)
    hull()
    {
      intersection()
      {
        children(i);
        children(j, $camfer=x_);
      }
      intersection()
      {
        children(i, $camfer=y_);
        children(j);
      }
    }
  }
}

module cube_(size, center) scale($camfer)cube(size, center);
module sphere_(r, d) scale($camfer)sphere(r=r, d=d);
module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, d=d,
center=center);

Fillet()
  {
   translate([-15, 0, 0])translate(pos1) cube_(10, center=true);
   translate([-15, 0, 0])translate(pos2) rotate(a=[20,20,0]) cube_([10,
5, 5], center=true);
   translate(pos1) sphere_(d=size1);
   translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder_(h=size1, d=size2, center=false);
   translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder_(h=size1, d=size2, center=true);
  }

Am 12.12.2025 um 01:42 schrieb Jon Bondy:

Thank you!  I tossed that into my bag of tricks!

On 12/11/2025 7:00 PM, Rudolf via Discuss wrote:

I like the idea behind your approach and kind of rewrote it into a
more generic solution  by introducing a global variable $camfer and
camfer-enabled modules cube_(), cylinder_() and sphere_() that allow
to be scaled by $camfer.

$fn=100;
$camfer=1;

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

module camfer(x=1.2)
{
  for(i=[0:$children-1])
  children(i);

  for(i=[0:$children-1], j=[0:$children-1])
  if(i<j)
  hull()
  {
    intersection()
    {
      children(i);
      children(j, $camfer=x);
    }
    intersection()
    {
      children(i, $camfer=x);
      children(j);
    }
  }
}
module cube_(size, center) scale($camfer)cube(size, center);
module sphere_(r, d) scale($camfer)sphere(r=r, d=d);
module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r,
d=d, center=center);

camfer()
{
  translate(pos1) cube_(10, center=true);
  translate(pos2) rotate(a=[20,20,0]) cube_([6, 5, 5], center=true);
}

translate([20, 0, 0])
  camfer()
  {
    translate(pos1) sphere_(d=size1);
    translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder_(h=size1, d=size2, center=true);
    translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder_(h=size1, d=size2, center=true);
  }

Am 08.12.2025 um 00:03 schrieb Todd Allen via Discuss:

module centered_scale(center_point, scale_vector) {
    translate(center_point)
        scale(scale_vector)
            translate(-1*center_point)
                children(0);
}

// chamfered joining of objects
// unions objects with a hull of those objects limited to the
intersection of the objects after scaling
// adjust object scale factors to change the size and angle of chamfers
// all=true then joints between all children are chamfered
// all=false only the joints between the 1st child and the other
children are chamfered
module hulled_join(centered_scales, all=false) {
    union() {
        children();
        for(i=[0:1:all?$children-2:0])
            for(j=[i+1:1:$children-1])
                hull() intersection() {
                    centered_scale(centered_scales[i][0],
centered_scales[i][1]) children(i);
                    centered_scale(centered_scales[j][0],
centered_scales[j][1]) children(j);
                    union() {
                        children(i);
                        children(j);
                    }
                }
    }
}

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

//  EXAMPLE 1
translate([-size1,0,0])
hulled_join([
    [pos1, (1+2.0/size1)[1,1,1]],
    [pos2, (1+1.0/size2)
[1,1,1]],
    [pos2, (1+1.0/size2)*[1,1,1]]
], $fn=128)
{
    translate(pos1)
        sphere(d=size1);
    translate(pos2)
        rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1,
d=size2, center=true);
    translate(pos2)
        rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1,
d=size2, center=true);
}

//  EXAMPLE 2
translate([size1,0,0])
hulled_join([
    [pos1, (1+1.0/size1)[1,1,1]],
    [pos2, (1+2.0/size2)
[1,1,1]],
    [pos2, (1+2.0/size2)*[1,1,1]]
], all=true, $fn=128)
{
    translate(pos1)
    sphere(d=size1);
    translate(pos2)
    rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2,
center=true);
    translate(pos2)
    rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2,
center=true);
}

hulljoin.png

On Sat, Dec 6, 2025 at 4:23 PM Todd Allen speedebikes@gmail.com wrote:

 Here's an example of how I sometimes chamfer joints using
 hull().  I have found this useful but it would be more useful if
 a similar method could be applied between parent and child
 objects using BOSL2's positioning/alignment/attachment but I
 don't yet see a good way and would love it if someone could help
 me do it.

 module centered_scale(center_point, scale_vector) {
     translate(center_point)
         scale(scale_vector)
             translate(-1*center_point)
                 children(0);
 }

 // chamfered joining of objects
 // unions objects with a hull of those objects limited to the
 intersection of the objects after scaling
 // adjust object scale factors to change the size and angle of
 the chamfer
 module hulled_join(centered_scales) {
     union() {
         children();
         hull() intersection() {
             intersection_for(i=[0:1:$children-1])
                 centered_scale(centered_scales[i][0],
 centered_scales[i][1])
                     children(i);
             union()
                 children();
         }
     }
 }

 size1 = 10;
 size2 = 5;
 pos1 = [0,0,0];
 pos2 = [size1*0.5+size2*0.3,0,0];

 //  EXAMPLE 1
 hulled_join([
     [pos1, (1+1.5/size1)*[1,1,1]],
     [pos2, (1+1.0/size2)*[1,1,1]]
     ])
 {

OpenSCAD mailing list
To unsubscribe send an email todiscuss-leave@lists.openscad.org

The "trick" is definitely a must-have. However, from this point the fillet() operator just takes a snap of the fingers. You tell the Fillet() operator the # iterations and the limits ... and you get this: I am thrilled! $fn=100; $camfer=1; size1 = 10; size2 = 5; pos1 = [0,0,0]; pos2 = [size1*0.5+size2*0.3,0,0]; module Fillet(x=1.5, y=1.5, iterations=10) {   for(i=[0:$children-1]) children(i);   for(k=[0:iterations-1]) // fillet iteration   {     x_ = 1+ k*(x-1)/iterations;     y_ = y - k*(y-1)/iterations;     for(i=[0:$children-1], j=[0:$children-1]) // Chamfer     if(i<j)     hull()     {       intersection()       {         children(i);         children(j, $camfer=x_);       }       intersection()       {         children(i, $camfer=y_);         children(j);       }     }   } } module cube_(size, center) scale($camfer)cube(size, center); module sphere_(r, d) scale($camfer)sphere(r=r, d=d); module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, d=d, center=center); Fillet()   {    translate([-15, 0, 0])translate(pos1) cube_(10, center=true);    translate([-15, 0, 0])translate(pos2) rotate(a=[20,20,0]) cube_([10, 5, 5], center=true);    translate(pos1) sphere_(d=size1);    translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder_(h=size1, d=size2, center=false);    translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder_(h=size1, d=size2, center=true);   } Am 12.12.2025 um 01:42 schrieb Jon Bondy: > > Thank you!  I tossed that into my bag of tricks! > > > On 12/11/2025 7:00 PM, Rudolf via Discuss wrote: >> >> I like the idea behind your approach and kind of rewrote it into a >> more generic solution  by introducing a global variable $camfer and >> camfer-enabled modules cube_(), cylinder_() and sphere_() that allow >> to be scaled by $camfer. >> >> $fn=100; >> $camfer=1; >> >> size1 = 10; >> size2 = 5; >> pos1 = [0,0,0]; >> pos2 = [size1*0.5+size2*0.3,0,0]; >> >> >> module camfer(x=1.2) >> { >>   for(i=[0:$children-1]) >>   children(i); >> >>   for(i=[0:$children-1], j=[0:$children-1]) >>   if(i<j) >>   hull() >>   { >>     intersection() >>     { >>       children(i); >>       children(j, $camfer=x); >>     } >>     intersection() >>     { >>       children(i, $camfer=x); >>       children(j); >>     } >>   } >> } >> module cube_(size, center) scale($camfer)cube(size, center); >> module sphere_(r, d) scale($camfer)sphere(r=r, d=d); >> module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, >> d=d, center=center); >> >> >> camfer() >> { >>   translate(pos1) cube_(10, center=true); >>   translate(pos2) rotate(a=[20,20,0]) cube_([6, 5, 5], center=true); >> } >> >> translate([20, 0, 0]) >>   camfer() >>   { >>     translate(pos1) sphere_(d=size1); >>     translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) >> cylinder_(h=size1, d=size2, center=true); >>     translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) >> cylinder_(h=size1, d=size2, center=true); >>   } >> >> >> >> >> Am 08.12.2025 um 00:03 schrieb Todd Allen via Discuss: >>> module centered_scale(center_point, scale_vector) { >>>     translate(center_point) >>>         scale(scale_vector) >>>             translate(-1*center_point) >>>                 children(0); >>> } >>> >>> // chamfered joining of objects >>> // unions objects with a hull of those objects limited to the >>> intersection of the objects after scaling >>> // adjust object scale factors to change the size and angle of chamfers >>> // all=true then joints between all children are chamfered >>> // all=false only the joints between the 1st child and the other >>> children are chamfered >>> module hulled_join(centered_scales, all=false) { >>>     union() { >>>         children(); >>>         for(i=[0:1:all?$children-2:0]) >>>             for(j=[i+1:1:$children-1]) >>>                 hull() intersection() { >>>                     centered_scale(centered_scales[i][0], >>> centered_scales[i][1]) children(i); >>>                     centered_scale(centered_scales[j][0], >>> centered_scales[j][1]) children(j); >>>                     union() { >>>                         children(i); >>>                         children(j); >>>                     } >>>                 } >>>     } >>> } >>> >>> size1 = 10; >>> size2 = 5; >>> pos1 = [0,0,0]; >>> pos2 = [size1*0.5+size2*0.3,0,0]; >>> >>> //  EXAMPLE 1 >>> translate([-size1,0,0]) >>> hulled_join([ >>>     [pos1, (1+2.0/size1)*[1,1,1]], >>>     [pos2, (1+1.0/size2)*[1,1,1]], >>>     [pos2, (1+1.0/size2)*[1,1,1]] >>> ], $fn=128) >>> { >>>     translate(pos1) >>>         sphere(d=size1); >>>     translate(pos2) >>>         rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, >>> d=size2, center=true); >>>     translate(pos2) >>>         rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, >>> d=size2, center=true); >>> } >>> >>> //  EXAMPLE 2 >>> translate([size1,0,0]) >>> hulled_join([ >>>     [pos1, (1+1.0/size1)*[1,1,1]], >>>     [pos2, (1+2.0/size2)*[1,1,1]], >>>     [pos2, (1+2.0/size2)*[1,1,1]] >>> ], all=true, $fn=128) >>> { >>>     translate(pos1) >>>     sphere(d=size1); >>>     translate(pos2) >>>     rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2, >>> center=true); >>>     translate(pos2) >>>     rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2, >>> center=true); >>> } >>> >>> hulljoin.png >>> >>> On Sat, Dec 6, 2025 at 4:23 PM Todd Allen <speedebikes@gmail.com> wrote: >>> >>> Here's an example of how I sometimes chamfer joints using >>> hull().  I have found this useful but it would be more useful if >>> a similar method could be applied between parent and child >>> objects using BOSL2's positioning/alignment/attachment but I >>> don't yet see a good way and would love it if someone could help >>> me do it. >>> >>> module centered_scale(center_point, scale_vector) { >>>     translate(center_point) >>>         scale(scale_vector) >>>             translate(-1*center_point) >>>                 children(0); >>> } >>> >>> // chamfered joining of objects >>> // unions objects with a hull of those objects limited to the >>> intersection of the objects after scaling >>> // adjust object scale factors to change the size and angle of >>> the chamfer >>> module hulled_join(centered_scales) { >>>     union() { >>>         children(); >>>         hull() intersection() { >>>             intersection_for(i=[0:1:$children-1]) >>>                 centered_scale(centered_scales[i][0], >>> centered_scales[i][1]) >>>                     children(i); >>>             union() >>>                 children(); >>>         } >>>     } >>> } >>> >>> size1 = 10; >>> size2 = 5; >>> pos1 = [0,0,0]; >>> pos2 = [size1*0.5+size2*0.3,0,0]; >>> >>> //  EXAMPLE 1 >>> hulled_join([ >>>     [pos1, (1+1.5/size1)*[1,1,1]], >>>     [pos2, (1+1.0/size2)*[1,1,1]] >>>     ]) >>> { >>> >> >> _______________________________________________ >> OpenSCAD mailing list >> To unsubscribe send an email todiscuss-leave@lists.openscad.org > > <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient> > Virus-free.www.avg.com > <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient> > > > <#DAB4FAD8-2DD7-40BB-A1B8-4E2AA1F9FDF2>
R
Rudolf
Fri, Dec 12, 2025 1:12 PM

Some hints: As the approach is based on scale() and hull(), one should
be aware that

i) hull() enforces convexity and will not perform well (or at all) with
concave shapes. E.g. rings can be made to work, but need a primitive
definition that diametrically scales both diameters, and the second
shape mustn't cut it twice.

ii) The scaling center of a primitive is crucial for the mechanism to
achieve the desired result. The scale parameters x, y must be adopted to
the dimensional size of a primitive

But if used with care, the solution is really powerful.

Am 12.12.2025 um 12:11 schrieb Rudolf via Discuss:

The "trick" is definitely a must-have. However, from this point the
fillet() operator just takes a snap of the fingers.

You tell the Fillet() operator the # iterations and the limits ... and
you get this: I am thrilled!

$fn=100;
$camfer=1;

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

module Fillet(x=1.5, y=1.5, iterations=10)
{
  for(i=[0:$children-1]) children(i);

  for(k=[0:iterations-1]) // fillet iteration
  {
    x_ = 1+ k*(x-1)/iterations;
    y_ = y - k*(y-1)/iterations;

    for(i=[0:$children-1], j=[0:$children-1]) // Chamfer
    if(i<j)
    hull()
    {
      intersection()
      {
        children(i);
        children(j, $camfer=x_);
      }
      intersection()
      {
        children(i, $camfer=y_);
        children(j);
      }
    }
  }
}

module cube_(size, center) scale($camfer)cube(size, center);
module sphere_(r, d) scale($camfer)sphere(r=r, d=d);
module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r,
d=d, center=center);

Fillet()
  {
   translate([-15, 0, 0])translate(pos1) cube_(10, center=true);
   translate([-15, 0, 0])translate(pos2) rotate(a=[20,20,0])
cube_([10, 5, 5], center=true);
   translate(pos1) sphere_(d=size1);
   translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder_(h=size1, d=size2, center=false);
   translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder_(h=size1, d=size2, center=true);
  }

Some hints: As the approach is based on scale() and hull(), one should be aware that i) hull() enforces convexity and will not perform well (or at all) with concave shapes. E.g. rings can be made to work, but need a primitive definition that diametrically scales both diameters, and the second shape mustn't cut it twice. ii) The scaling center of a primitive is crucial for the mechanism to achieve the desired result. The scale parameters x, y must be adopted to the dimensional size of a primitive But if used with care, the solution is really powerful. Am 12.12.2025 um 12:11 schrieb Rudolf via Discuss: > > The "trick" is definitely a must-have. However, from this point the > fillet() operator just takes a snap of the fingers. > > You tell the Fillet() operator the # iterations and the limits ... and > you get this: I am thrilled! > > > $fn=100; > $camfer=1; > > size1 = 10; > size2 = 5; > pos1 = [0,0,0]; > pos2 = [size1*0.5+size2*0.3,0,0]; > > module Fillet(x=1.5, y=1.5, iterations=10) > { >   for(i=[0:$children-1]) children(i); > >   for(k=[0:iterations-1]) // fillet iteration >   { >     x_ = 1+ k*(x-1)/iterations; >     y_ = y - k*(y-1)/iterations; > >     for(i=[0:$children-1], j=[0:$children-1]) // Chamfer >     if(i<j) >     hull() >     { >       intersection() >       { >         children(i); >         children(j, $camfer=x_); >       } >       intersection() >       { >         children(i, $camfer=y_); >         children(j); >       } >     } >   } > } > > module cube_(size, center) scale($camfer)cube(size, center); > module sphere_(r, d) scale($camfer)sphere(r=r, d=d); > module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, > d=d, center=center); > > Fillet() >   { >    translate([-15, 0, 0])translate(pos1) cube_(10, center=true); >    translate([-15, 0, 0])translate(pos2) rotate(a=[20,20,0]) > cube_([10, 5, 5], center=true); >    translate(pos1) sphere_(d=size1); >    translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) > cylinder_(h=size1, d=size2, center=false); >    translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) > cylinder_(h=size1, d=size2, center=true); >   } >
TA
Todd Allen
Fri, Dec 12, 2025 6:16 PM

Rudolf, big thanks for your interest in this technique and your offered
changes!  I hope your enthusiasm is contagious and there will be more
contributions.

Your revision of the hull() and intersection() targets brings some clarity
and I think efficiency to the method.  I like your idea to use a $ var to
enable scaled children which can eliminate my passing of scaling factors as
parameters to hulled_join() for a cleaner interface.  I prefer scaling in a
single separate module instead of adding it to cube(), sphere(), etc. as
hulled_join() can be applied to children formed in any fashion including
polyhedron(), linear_extrude(),  rotate_extrude(), etc.  I think you lost
independent scaling factors for each child which gave control of the angle
of the join.  I liked your addition of rounding by iterative proportional
adjustment of scaling but reworked it to retain independent scale factors
for each child.  I also retained my “all” parameter enabling the
specification of forming joins between all children or just between the
first child and the other children.  I adapted this new approach of scaling
children of hulled_join() to work with BOSL2 attachables, improving that
interface too.
[image: hjtest2.png]
include <BOSL2/std.scad>
$fn = 128;

// OpenSCAD hulled_join() examples
size1 = 20;
size2 = 10;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

// round joints between sphere and other children
translate([0,1.5size1,0])
hulled_join(iterations=10) {
hj_scale(pos1, (1+2.0/size1)
[1,1,1])
translate(pos1) sphere(d=size1);
hj_scale(pos2, (1+1.0/size2)[1,1,1])
translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder(h=size1, d=size2, center=true);
hj_scale(pos2, (1+1.0/size2)
[1,1,1])
translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder(h=size1, d=size2, center=true);
}

// chamfer all the joints
hulled_join(all=true) {
hj_scale(pos1, (1+2.0/size1)[1,1,1])
translate(pos1) sphere(d=size1);
hj_scale(pos2, (1+1.0/size2)
[1,1,1])
translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder(h=size1, d=size2, center=true);
hj_scale(pos2, (1+1.0/size2)*[1,1,1])
translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder(h=size1, d=size2, center=true);
}

// BOSL2 hulled_join() example of:
//
https://github.com/BelfrySCAD/BOSL2/wiki/attachments.scad#function-desc_dir
right(40) back(30)
prismoid(20,10,h=15)
attach(RIGHT,BOT) cuboid([4,4,15])
position(TOP) cyl(d=12,h=5,orient=desc_dir(),anchor=BACK);

// with hulled_join() joints
module body(ao=false) {prismoid(20,10,h=15, anchor=ao?ao:BOT) children();}
module arm(ao=false) {cuboid([4,4,20], anchor=ao?ao:CTR) children();}
module hand(ao=false) {cyl(d=12,h=5,orient=desc_dir(), anchor=ao?ao:BACK)
children();}
right(40)
body() let(body_desc=parent())
attach(RIGHT,BOT) arm() let(arm_desc=parent())
position(TOP) hand() let(hand_desc=parent())
restore() hulled_join(3) {
// arm first to allow default case of only computing chamfers between
first child and the other children
hja_scale(1.6*[1,1,1], arm_desc) arm(CTR);
hja_scale(1.2*[1,1,1], body_desc) body(CTR);
hja_scale(1.2*[1,1,1], hand_desc) hand(CTR);
};

//
// this code could be in a library
//
$hj_scale=0;

// to control scaling a child of hulled_join()
module hj_scale(center_point, scale_vector) {
if ($hj_scale) {
sv = [
1+$hj_scale*(scale_vector.x-1),
1+$hj_scale*(scale_vector.y-1),
1+$hj_scale*(scale_vector.z-1)
];
// optimizable to a single translation?
translate(center_point) scale(sv) translate(-1*center_point)
children();
}
else children();
}

// to control scaling a BOSL2 attachable child of hulled_join()
module hja_scale(scale_vector, desc) {
if ($hj_scale) {
sv = [
1+$hj_scale*(scale_vector.x-1),
1+$hj_scale*(scale_vector.y-1),
1+$hj_scale*(scale_vector.z-1)
];
restore(transform_desc(scale(sv, cp=desc_point(desc,anchor=CTR)),
desc)) children();
}
else restore(desc) children();
}

// hulled joining of children objects
// unions objects with a hull of those objects limited to intersections of
the objects with scaled partners
// adjust scale_vector of hj_scale() or hja_scale() in each child to change
the size and angle of joints
// all=true, joints between all children are chamfered
// all=false, only the joints between the 1st child and the other children
are chamfered
module hulled_join(iterations=1, all=false) {
union() {
children();
for(si=[0:1:iterations-1]) {
for(i=[0:1:all?$children-2:0]) for(j=[i+1:1:$children-1])
hull() {
intersection() {
children(i, $hj_scale=0);
children(j, $hj_scale=(si+1)/iterations);
}
intersection() {
children(i, $hj_scale=(iterations-si)/iterations);
children(j, $hj_scale=0);
}
}
}
}
}

On Fri, Dec 12, 2025 at 5:11 AM Rudolf via Discuss <
discuss@lists.openscad.org> wrote:

The "trick" is definitely a must-have. However, from this point the
fillet() operator just takes a snap of the fingers.

You tell the Fillet() operator the # iterations and the limits ... and you
get this: I am thrilled!

$fn=100;
$camfer=1;

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

module Fillet(x=1.5, y=1.5, iterations=10)
{
for(i=[0:$children-1]) children(i);

for(k=[0:iterations-1]) // fillet iteration
{
x_ = 1+ k*(x-1)/iterations;
y_ = y - k*(y-1)/iterations;

 for(i=[0:$children-1], j=[0:$children-1]) // Chamfer
 if(i<j)
 hull()
 {
   intersection()
   {
     children(i);
     children(j, $camfer=x_);
   }
   intersection()
   {
     children(i, $camfer=y_);
     children(j);
   }
 }

}
}

module cube_(size, center) scale($camfer)cube(size, center);
module sphere_(r, d) scale($camfer)sphere(r=r, d=d);
module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, d=d,
center=center);

Fillet()
{
translate([-15, 0, 0])translate(pos1) cube_(10, center=true);
translate([-15, 0, 0])translate(pos2) rotate(a=[20,20,0]) cube_([10, 5,
5], center=true);
translate(pos1) sphere_(d=size1);
translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder_(h=size1,
d=size2, center=false);
translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder_(h=size1, d=size2, center=true);
}

Am 12.12.2025 um 01:42 schrieb Jon Bondy:

Thank you!  I tossed that into my bag of tricks!

On 12/11/2025 7:00 PM, Rudolf via Discuss wrote:

I like the idea behind your approach and kind of rewrote it into a more
generic solution  by introducing a global variable $camfer and
camfer-enabled modules cube_(), cylinder_() and sphere_() that allow to be
scaled by $camfer.

$fn=100;
$camfer=1;

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

module camfer(x=1.2)
{
for(i=[0:$children-1])
children(i);

for(i=[0:$children-1], j=[0:$children-1])
if(i<j)
hull()
{
intersection()
{
children(i);
children(j, $camfer=x);
}
intersection()
{
children(i, $camfer=x);
children(j);
}
}
}
module cube_(size, center) scale($camfer)cube(size, center);
module sphere_(r, d) scale($camfer)sphere(r=r, d=d);
module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, d=d,
center=center);

camfer()
{
translate(pos1) cube_(10, center=true);
translate(pos2) rotate(a=[20,20,0]) cube_([6, 5, 5], center=true);
}

translate([20, 0, 0])
camfer()
{
translate(pos1) sphere_(d=size1);
translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder_(h=size1, d=size2, center=true);
translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder_(h=size1, d=size2, center=true);
}

Am 08.12.2025 um 00:03 schrieb Todd Allen via Discuss:

module centered_scale(center_point, scale_vector) {
translate(center_point)
scale(scale_vector)
translate(-1*center_point)
children(0);
}

// chamfered joining of objects
// unions objects with a hull of those objects limited to the intersection
of the objects after scaling
// adjust object scale factors to change the size and angle of chamfers
// all=true then joints between all children are chamfered
// all=false only the joints between the 1st child and the other children
are chamfered
module hulled_join(centered_scales, all=false) {
union() {
children();
for(i=[0:1:all?$children-2:0])
for(j=[i+1:1:$children-1])
hull() intersection() {
centered_scale(centered_scales[i][0],
centered_scales[i][1]) children(i);
centered_scale(centered_scales[j][0],
centered_scales[j][1]) children(j);
union() {
children(i);
children(j);
}
}
}
}

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

//  EXAMPLE 1
translate([-size1,0,0])
hulled_join([
[pos1, (1+2.0/size1)[1,1,1]],
[pos2, (1+1.0/size2)
[1,1,1]],
[pos2, (1+1.0/size2)*[1,1,1]]
], $fn=128)
{
translate(pos1)
sphere(d=size1);
translate(pos2)
rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2,
center=true);
translate(pos2)
rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2,
center=true);
}

//  EXAMPLE 2
translate([size1,0,0])
hulled_join([
[pos1, (1+1.0/size1)[1,1,1]],
[pos2, (1+2.0/size2)
[1,1,1]],
[pos2, (1+2.0/size2)*[1,1,1]]
], all=true, $fn=128)
{
translate(pos1)
sphere(d=size1);
translate(pos2)
rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2,
center=true);
translate(pos2)
rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2,
center=true);
}

[image: hulljoin.png]

On Sat, Dec 6, 2025 at 4:23 PM Todd Allen speedebikes@gmail.com wrote:

Here's an example of how I sometimes chamfer joints using hull().  I have
found this useful but it would be more useful if a similar method could be
applied between parent and child objects using BOSL2's
positioning/alignment/attachment but I don't yet see a good way and would
love it if someone could help me do it.

module centered_scale(center_point, scale_vector) {
translate(center_point)
scale(scale_vector)
translate(-1*center_point)
children(0);
}

// chamfered joining of objects
// unions objects with a hull of those objects limited to the
intersection of the objects after scaling
// adjust object scale factors to change the size and angle of the chamfer
module hulled_join(centered_scales) {
union() {
children();
hull() intersection() {
intersection_for(i=[0:1:$children-1])
centered_scale(centered_scales[i][0],
centered_scales[i][1])
children(i);
union()
children();
}
}
}

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

//  EXAMPLE 1
hulled_join([
[pos1, (1+1.5/size1)[1,1,1]],
[pos2, (1+1.0/size2)
[1,1,1]]
])
{


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org

http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient
Virus-free.www.avg.com
http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient
<#m_-1384773676116772327_DAB4FAD8-2DD7-40BB-A1B8-4E2AA1F9FDF2>


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org

Rudolf, big thanks for your interest in this technique and your offered changes! I hope your enthusiasm is contagious and there will be more contributions. Your revision of the hull() and intersection() targets brings some clarity and I think efficiency to the method. I like your idea to use a $ var to enable scaled children which can eliminate my passing of scaling factors as parameters to hulled_join() for a cleaner interface. I prefer scaling in a single separate module instead of adding it to cube(), sphere(), etc. as hulled_join() can be applied to children formed in any fashion including polyhedron(), linear_extrude(), rotate_extrude(), etc. I think you lost independent scaling factors for each child which gave control of the angle of the join. I liked your addition of rounding by iterative proportional adjustment of scaling but reworked it to retain independent scale factors for each child. I also retained my “all” parameter enabling the specification of forming joins between all children or just between the first child and the other children. I adapted this new approach of scaling children of hulled_join() to work with BOSL2 attachables, improving that interface too. [image: hjtest2.png] include <BOSL2/std.scad> $fn = 128; // OpenSCAD hulled_join() examples size1 = 20; size2 = 10; pos1 = [0,0,0]; pos2 = [size1*0.5+size2*0.3,0,0]; // round joints between sphere and other children translate([0,1.5*size1,0]) hulled_join(iterations=10) { hj_scale(pos1, (1+2.0/size1)*[1,1,1]) translate(pos1) sphere(d=size1); hj_scale(pos2, (1+1.0/size2)*[1,1,1]) translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2, center=true); hj_scale(pos2, (1+1.0/size2)*[1,1,1]) translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2, center=true); } // chamfer all the joints hulled_join(all=true) { hj_scale(pos1, (1+2.0/size1)*[1,1,1]) translate(pos1) sphere(d=size1); hj_scale(pos2, (1+1.0/size2)*[1,1,1]) translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2, center=true); hj_scale(pos2, (1+1.0/size2)*[1,1,1]) translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2, center=true); } // BOSL2 hulled_join() example of: // https://github.com/BelfrySCAD/BOSL2/wiki/attachments.scad#function-desc_dir right(40) back(30) prismoid(20,10,h=15) attach(RIGHT,BOT) cuboid([4,4,15]) position(TOP) cyl(d=12,h=5,orient=desc_dir(),anchor=BACK); // with hulled_join() joints module body(ao=false) {prismoid(20,10,h=15, anchor=ao?ao:BOT) children();} module arm(ao=false) {cuboid([4,4,20], anchor=ao?ao:CTR) children();} module hand(ao=false) {cyl(d=12,h=5,orient=desc_dir(), anchor=ao?ao:BACK) children();} right(40) body() let(body_desc=parent()) attach(RIGHT,BOT) arm() let(arm_desc=parent()) position(TOP) hand() let(hand_desc=parent()) restore() hulled_join(3) { // arm first to allow default case of only computing chamfers between first child and the other children hja_scale(1.6*[1,1,1], arm_desc) arm(CTR); hja_scale(1.2*[1,1,1], body_desc) body(CTR); hja_scale(1.2*[1,1,1], hand_desc) hand(CTR); }; // // this code could be in a library // $hj_scale=0; // to control scaling a child of hulled_join() module hj_scale(center_point, scale_vector) { if ($hj_scale) { sv = [ 1+$hj_scale*(scale_vector.x-1), 1+$hj_scale*(scale_vector.y-1), 1+$hj_scale*(scale_vector.z-1) ]; // optimizable to a single translation? translate(center_point) scale(sv) translate(-1*center_point) children(); } else children(); } // to control scaling a BOSL2 attachable child of hulled_join() module hja_scale(scale_vector, desc) { if ($hj_scale) { sv = [ 1+$hj_scale*(scale_vector.x-1), 1+$hj_scale*(scale_vector.y-1), 1+$hj_scale*(scale_vector.z-1) ]; restore(transform_desc(scale(sv, cp=desc_point(desc,anchor=CTR)), desc)) children(); } else restore(desc) children(); } // hulled joining of children objects // unions objects with a hull of those objects limited to intersections of the objects with scaled partners // adjust scale_vector of hj_scale() or hja_scale() in each child to change the size and angle of joints // all=true, joints between all children are chamfered // all=false, only the joints between the 1st child and the other children are chamfered module hulled_join(iterations=1, all=false) { union() { children(); for(si=[0:1:iterations-1]) { for(i=[0:1:all?$children-2:0]) for(j=[i+1:1:$children-1]) hull() { intersection() { children(i, $hj_scale=0); children(j, $hj_scale=(si+1)/iterations); } intersection() { children(i, $hj_scale=(iterations-si)/iterations); children(j, $hj_scale=0); } } } } } On Fri, Dec 12, 2025 at 5:11 AM Rudolf via Discuss < discuss@lists.openscad.org> wrote: > The "trick" is definitely a must-have. However, from this point the > fillet() operator just takes a snap of the fingers. > > You tell the Fillet() operator the # iterations and the limits ... and you > get this: I am thrilled! > > > $fn=100; > $camfer=1; > > size1 = 10; > size2 = 5; > pos1 = [0,0,0]; > pos2 = [size1*0.5+size2*0.3,0,0]; > > module Fillet(x=1.5, y=1.5, iterations=10) > { > for(i=[0:$children-1]) children(i); > > for(k=[0:iterations-1]) // fillet iteration > { > x_ = 1+ k*(x-1)/iterations; > y_ = y - k*(y-1)/iterations; > > for(i=[0:$children-1], j=[0:$children-1]) // Chamfer > if(i<j) > hull() > { > intersection() > { > children(i); > children(j, $camfer=x_); > } > intersection() > { > children(i, $camfer=y_); > children(j); > } > } > } > } > > module cube_(size, center) scale($camfer)cube(size, center); > module sphere_(r, d) scale($camfer)sphere(r=r, d=d); > module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, d=d, > center=center); > > Fillet() > { > translate([-15, 0, 0])translate(pos1) cube_(10, center=true); > translate([-15, 0, 0])translate(pos2) rotate(a=[20,20,0]) cube_([10, 5, > 5], center=true); > translate(pos1) sphere_(d=size1); > translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder_(h=size1, > d=size2, center=false); > translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) > cylinder_(h=size1, d=size2, center=true); > } > > Am 12.12.2025 um 01:42 schrieb Jon Bondy: > > Thank you! I tossed that into my bag of tricks! > > > On 12/11/2025 7:00 PM, Rudolf via Discuss wrote: > > I like the idea behind your approach and kind of rewrote it into a more > generic solution by introducing a global variable $camfer and > camfer-enabled modules cube_(), cylinder_() and sphere_() that allow to be > scaled by $camfer. > > $fn=100; > $camfer=1; > > size1 = 10; > size2 = 5; > pos1 = [0,0,0]; > pos2 = [size1*0.5+size2*0.3,0,0]; > > > module camfer(x=1.2) > { > for(i=[0:$children-1]) > children(i); > > for(i=[0:$children-1], j=[0:$children-1]) > if(i<j) > hull() > { > intersection() > { > children(i); > children(j, $camfer=x); > } > intersection() > { > children(i, $camfer=x); > children(j); > } > } > } > module cube_(size, center) scale($camfer)cube(size, center); > module sphere_(r, d) scale($camfer)sphere(r=r, d=d); > module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, d=d, > center=center); > > > camfer() > { > translate(pos1) cube_(10, center=true); > translate(pos2) rotate(a=[20,20,0]) cube_([6, 5, 5], center=true); > } > > translate([20, 0, 0]) > camfer() > { > translate(pos1) sphere_(d=size1); > translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) > cylinder_(h=size1, d=size2, center=true); > translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) > cylinder_(h=size1, d=size2, center=true); > } > > > > > Am 08.12.2025 um 00:03 schrieb Todd Allen via Discuss: > > module centered_scale(center_point, scale_vector) { > translate(center_point) > scale(scale_vector) > translate(-1*center_point) > children(0); > } > > // chamfered joining of objects > // unions objects with a hull of those objects limited to the intersection > of the objects after scaling > // adjust object scale factors to change the size and angle of chamfers > // all=true then joints between all children are chamfered > // all=false only the joints between the 1st child and the other children > are chamfered > module hulled_join(centered_scales, all=false) { > union() { > children(); > for(i=[0:1:all?$children-2:0]) > for(j=[i+1:1:$children-1]) > hull() intersection() { > centered_scale(centered_scales[i][0], > centered_scales[i][1]) children(i); > centered_scale(centered_scales[j][0], > centered_scales[j][1]) children(j); > union() { > children(i); > children(j); > } > } > } > } > > size1 = 10; > size2 = 5; > pos1 = [0,0,0]; > pos2 = [size1*0.5+size2*0.3,0,0]; > > // EXAMPLE 1 > translate([-size1,0,0]) > hulled_join([ > [pos1, (1+2.0/size1)*[1,1,1]], > [pos2, (1+1.0/size2)*[1,1,1]], > [pos2, (1+1.0/size2)*[1,1,1]] > ], $fn=128) > { > translate(pos1) > sphere(d=size1); > translate(pos2) > rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2, > center=true); > translate(pos2) > rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2, > center=true); > } > > // EXAMPLE 2 > translate([size1,0,0]) > hulled_join([ > [pos1, (1+1.0/size1)*[1,1,1]], > [pos2, (1+2.0/size2)*[1,1,1]], > [pos2, (1+2.0/size2)*[1,1,1]] > ], all=true, $fn=128) > { > translate(pos1) > sphere(d=size1); > translate(pos2) > rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2, > center=true); > translate(pos2) > rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2, > center=true); > } > > [image: hulljoin.png] > > > On Sat, Dec 6, 2025 at 4:23 PM Todd Allen <speedebikes@gmail.com> wrote: > >> Here's an example of how I sometimes chamfer joints using hull(). I have >> found this useful but it would be more useful if a similar method could be >> applied between parent and child objects using BOSL2's >> positioning/alignment/attachment but I don't yet see a good way and would >> love it if someone could help me do it. >> >> module centered_scale(center_point, scale_vector) { >> translate(center_point) >> scale(scale_vector) >> translate(-1*center_point) >> children(0); >> } >> >> // chamfered joining of objects >> // unions objects with a hull of those objects limited to the >> intersection of the objects after scaling >> // adjust object scale factors to change the size and angle of the chamfer >> module hulled_join(centered_scales) { >> union() { >> children(); >> hull() intersection() { >> intersection_for(i=[0:1:$children-1]) >> centered_scale(centered_scales[i][0], >> centered_scales[i][1]) >> children(i); >> union() >> children(); >> } >> } >> } >> >> size1 = 10; >> size2 = 5; >> pos1 = [0,0,0]; >> pos2 = [size1*0.5+size2*0.3,0,0]; >> >> // EXAMPLE 1 >> hulled_join([ >> [pos1, (1+1.5/size1)*[1,1,1]], >> [pos2, (1+1.0/size2)*[1,1,1]] >> ]) >> { >> > > _______________________________________________ > OpenSCAD mailing list > To unsubscribe send an email to discuss-leave@lists.openscad.org > > > > <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient> > Virus-free.www.avg.com > <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient> > <#m_-1384773676116772327_DAB4FAD8-2DD7-40BB-A1B8-4E2AA1F9FDF2> > > _______________________________________________ > OpenSCAD mailing list > To unsubscribe send an email to discuss-leave@lists.openscad.org
TA
Todd Allen
Fri, Dec 12, 2025 9:43 PM

In the previous code I posted for hulled_join() the BOSL2 example would
union an additional unneeded copy of children().  Here's a fix, though one
will then need to pass children=true for non BOSL2 attachables cases.

module hulled_join(iterations=1, all=false, children=false) {
union() {
if (children) children();
for(si=[0:1:iterations-1]) {
for(i=[0:1:all?$children-2:0]) for(j=[i+1:1:$children-1])
hull() {
intersection() {
children(i, $hj_scale=0);
children(j, $hj_scale=(si+1)/iterations);
}
intersection() {
children(i, $hj_scale=(iterations-si)/iterations);
children(j, $hj_scale=0);
}
}
}
}
}

On Fri, Dec 12, 2025 at 12:16 PM Todd Allen speedebikes@gmail.com wrote:

Rudolf, big thanks for your interest in this technique and your offered
changes!  I hope your enthusiasm is contagious and there will be more
contributions.

Your revision of the hull() and intersection() targets brings some clarity
and I think efficiency to the method.  I like your idea to use a $ var to
enable scaled children which can eliminate my passing of scaling factors as
parameters to hulled_join() for a cleaner interface.  I prefer scaling in a
single separate module instead of adding it to cube(), sphere(), etc. as
hulled_join() can be applied to children formed in any fashion including
polyhedron(), linear_extrude(),  rotate_extrude(), etc.  I think you lost
independent scaling factors for each child which gave control of the angle
of the join.  I liked your addition of rounding by iterative proportional
adjustment of scaling but reworked it to retain independent scale factors
for each child.  I also retained my “all” parameter enabling the
specification of forming joins between all children or just between the
first child and the other children.  I adapted this new approach of scaling
children of hulled_join() to work with BOSL2 attachables, improving that
interface too.
[image: hjtest2.png]
include <BOSL2/std.scad>
$fn = 128;

// OpenSCAD hulled_join() examples
size1 = 20;
size2 = 10;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

// round joints between sphere and other children
translate([0,1.5size1,0])
hulled_join(iterations=10) {
hj_scale(pos1, (1+2.0/size1)
[1,1,1])
translate(pos1) sphere(d=size1);
hj_scale(pos2, (1+1.0/size2)[1,1,1])
translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder(h=size1, d=size2, center=true);
hj_scale(pos2, (1+1.0/size2)
[1,1,1])
translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder(h=size1, d=size2, center=true);
}

// chamfer all the joints
hulled_join(all=true) {
hj_scale(pos1, (1+2.0/size1)[1,1,1])
translate(pos1) sphere(d=size1);
hj_scale(pos2, (1+1.0/size2)
[1,1,1])
translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder(h=size1, d=size2, center=true);
hj_scale(pos2, (1+1.0/size2)*[1,1,1])
translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder(h=size1, d=size2, center=true);
}

// BOSL2 hulled_join() example of:
//
https://github.com/BelfrySCAD/BOSL2/wiki/attachments.scad#function-desc_dir
right(40) back(30)
prismoid(20,10,h=15)
attach(RIGHT,BOT) cuboid([4,4,15])
position(TOP) cyl(d=12,h=5,orient=desc_dir(),anchor=BACK);

// with hulled_join() joints
module body(ao=false) {prismoid(20,10,h=15, anchor=ao?ao:BOT) children();}
module arm(ao=false) {cuboid([4,4,20], anchor=ao?ao:CTR) children();}
module hand(ao=false) {cyl(d=12,h=5,orient=desc_dir(), anchor=ao?ao:BACK)
children();}
right(40)
body() let(body_desc=parent())
attach(RIGHT,BOT) arm() let(arm_desc=parent())
position(TOP) hand() let(hand_desc=parent())
restore() hulled_join(3) {
// arm first to allow default case of only computing chamfers between
first child and the other children
hja_scale(1.6*[1,1,1], arm_desc) arm(CTR);
hja_scale(1.2*[1,1,1], body_desc) body(CTR);
hja_scale(1.2*[1,1,1], hand_desc) hand(CTR);
};

//
// this code could be in a library
//
$hj_scale=0;

// to control scaling a child of hulled_join()
module hj_scale(center_point, scale_vector) {
if ($hj_scale) {
sv = [
1+$hj_scale*(scale_vector.x-1),
1+$hj_scale*(scale_vector.y-1),
1+$hj_scale*(scale_vector.z-1)
];
// optimizable to a single translation?
translate(center_point) scale(sv) translate(-1*center_point)
children();
}
else children();
}

// to control scaling a BOSL2 attachable child of hulled_join()
module hja_scale(scale_vector, desc) {
if ($hj_scale) {
sv = [
1+$hj_scale*(scale_vector.x-1),
1+$hj_scale*(scale_vector.y-1),
1+$hj_scale*(scale_vector.z-1)
];
restore(transform_desc(scale(sv, cp=desc_point(desc,anchor=CTR)),
desc)) children();
}
else restore(desc) children();
}

// hulled joining of children objects
// unions objects with a hull of those objects limited to intersections of
the objects with scaled partners
// adjust scale_vector of hj_scale() or hja_scale() in each child to
change the size and angle of joints
// all=true, joints between all children are chamfered
// all=false, only the joints between the 1st child and the other children
are chamfered
module hulled_join(iterations=1, all=false) {
union() {
children();
for(si=[0:1:iterations-1]) {
for(i=[0:1:all?$children-2:0]) for(j=[i+1:1:$children-1])
hull() {
intersection() {
children(i, $hj_scale=0);
children(j, $hj_scale=(si+1)/iterations);
}
intersection() {
children(i, $hj_scale=(iterations-si)/iterations);
children(j, $hj_scale=0);
}
}
}
}
}

On Fri, Dec 12, 2025 at 5:11 AM Rudolf via Discuss <
discuss@lists.openscad.org> wrote:

The "trick" is definitely a must-have. However, from this point the
fillet() operator just takes a snap of the fingers.

You tell the Fillet() operator the # iterations and the limits ... and
you get this: I am thrilled!

$fn=100;
$camfer=1;

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

module Fillet(x=1.5, y=1.5, iterations=10)
{
for(i=[0:$children-1]) children(i);

for(k=[0:iterations-1]) // fillet iteration
{
x_ = 1+ k*(x-1)/iterations;
y_ = y - k*(y-1)/iterations;

 for(i=[0:$children-1], j=[0:$children-1]) // Chamfer
 if(i<j)
 hull()
 {
   intersection()
   {
     children(i);
     children(j, $camfer=x_);
   }
   intersection()
   {
     children(i, $camfer=y_);
     children(j);
   }
 }

}
}

module cube_(size, center) scale($camfer)cube(size, center);
module sphere_(r, d) scale($camfer)sphere(r=r, d=d);
module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, d=d,
center=center);

Fillet()
{
translate([-15, 0, 0])translate(pos1) cube_(10, center=true);
translate([-15, 0, 0])translate(pos2) rotate(a=[20,20,0]) cube_([10,
5, 5], center=true);
translate(pos1) sphere_(d=size1);
translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder_(h=size1, d=size2, center=false);
translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder_(h=size1, d=size2, center=true);
}

Am 12.12.2025 um 01:42 schrieb Jon Bondy:

Thank you!  I tossed that into my bag of tricks!

On 12/11/2025 7:00 PM, Rudolf via Discuss wrote:

I like the idea behind your approach and kind of rewrote it into a more
generic solution  by introducing a global variable $camfer and
camfer-enabled modules cube_(), cylinder_() and sphere_() that allow to be
scaled by $camfer.

$fn=100;
$camfer=1;

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

module camfer(x=1.2)
{
for(i=[0:$children-1])
children(i);

for(i=[0:$children-1], j=[0:$children-1])
if(i<j)
hull()
{
intersection()
{
children(i);
children(j, $camfer=x);
}
intersection()
{
children(i, $camfer=x);
children(j);
}
}
}
module cube_(size, center) scale($camfer)cube(size, center);
module sphere_(r, d) scale($camfer)sphere(r=r, d=d);
module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, d=d,
center=center);

camfer()
{
translate(pos1) cube_(10, center=true);
translate(pos2) rotate(a=[20,20,0]) cube_([6, 5, 5], center=true);
}

translate([20, 0, 0])
camfer()
{
translate(pos1) sphere_(d=size1);
translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder_(h=size1, d=size2, center=true);
translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder_(h=size1, d=size2, center=true);
}

Am 08.12.2025 um 00:03 schrieb Todd Allen via Discuss:

module centered_scale(center_point, scale_vector) {
translate(center_point)
scale(scale_vector)
translate(-1*center_point)
children(0);
}

// chamfered joining of objects
// unions objects with a hull of those objects limited to the
intersection of the objects after scaling
// adjust object scale factors to change the size and angle of chamfers
// all=true then joints between all children are chamfered
// all=false only the joints between the 1st child and the other children
are chamfered
module hulled_join(centered_scales, all=false) {
union() {
children();
for(i=[0:1:all?$children-2:0])
for(j=[i+1:1:$children-1])
hull() intersection() {
centered_scale(centered_scales[i][0],
centered_scales[i][1]) children(i);
centered_scale(centered_scales[j][0],
centered_scales[j][1]) children(j);
union() {
children(i);
children(j);
}
}
}
}

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

//  EXAMPLE 1
translate([-size1,0,0])
hulled_join([
[pos1, (1+2.0/size1)[1,1,1]],
[pos2, (1+1.0/size2)
[1,1,1]],
[pos2, (1+1.0/size2)*[1,1,1]]
], $fn=128)
{
translate(pos1)
sphere(d=size1);
translate(pos2)
rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2,
center=true);
translate(pos2)
rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2,
center=true);
}

//  EXAMPLE 2
translate([size1,0,0])
hulled_join([
[pos1, (1+1.0/size1)[1,1,1]],
[pos2, (1+2.0/size2)
[1,1,1]],
[pos2, (1+2.0/size2)*[1,1,1]]
], all=true, $fn=128)
{
translate(pos1)
sphere(d=size1);
translate(pos2)
rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2,
center=true);
translate(pos2)
rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2,
center=true);
}

[image: hulljoin.png]

On Sat, Dec 6, 2025 at 4:23 PM Todd Allen speedebikes@gmail.com wrote:

Here's an example of how I sometimes chamfer joints using hull().  I
have found this useful but it would be more useful if a similar method
could be applied between parent and child objects using BOSL2's
positioning/alignment/attachment but I don't yet see a good way and would
love it if someone could help me do it.

module centered_scale(center_point, scale_vector) {
translate(center_point)
scale(scale_vector)
translate(-1*center_point)
children(0);
}

// chamfered joining of objects
// unions objects with a hull of those objects limited to the
intersection of the objects after scaling
// adjust object scale factors to change the size and angle of the
chamfer
module hulled_join(centered_scales) {
union() {
children();
hull() intersection() {
intersection_for(i=[0:1:$children-1])
centered_scale(centered_scales[i][0],
centered_scales[i][1])
children(i);
union()
children();
}
}
}

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

//  EXAMPLE 1
hulled_join([
[pos1, (1+1.5/size1)[1,1,1]],
[pos2, (1+1.0/size2)
[1,1,1]]
])
{


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org

http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient
Virus-free.www.avg.com
http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient
<#m_-3373310449440574214_m_-1384773676116772327_DAB4FAD8-2DD7-40BB-A1B8-4E2AA1F9FDF2>


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org

In the previous code I posted for hulled_join() the BOSL2 example would union an additional unneeded copy of children(). Here's a fix, though one will then need to pass children=true for non BOSL2 attachables cases. module hulled_join(iterations=1, all=false, children=false) { union() { if (children) children(); for(si=[0:1:iterations-1]) { for(i=[0:1:all?$children-2:0]) for(j=[i+1:1:$children-1]) hull() { intersection() { children(i, $hj_scale=0); children(j, $hj_scale=(si+1)/iterations); } intersection() { children(i, $hj_scale=(iterations-si)/iterations); children(j, $hj_scale=0); } } } } } On Fri, Dec 12, 2025 at 12:16 PM Todd Allen <speedebikes@gmail.com> wrote: > Rudolf, big thanks for your interest in this technique and your offered > changes! I hope your enthusiasm is contagious and there will be more > contributions. > > Your revision of the hull() and intersection() targets brings some clarity > and I think efficiency to the method. I like your idea to use a $ var to > enable scaled children which can eliminate my passing of scaling factors as > parameters to hulled_join() for a cleaner interface. I prefer scaling in a > single separate module instead of adding it to cube(), sphere(), etc. as > hulled_join() can be applied to children formed in any fashion including > polyhedron(), linear_extrude(), rotate_extrude(), etc. I think you lost > independent scaling factors for each child which gave control of the angle > of the join. I liked your addition of rounding by iterative proportional > adjustment of scaling but reworked it to retain independent scale factors > for each child. I also retained my “all” parameter enabling the > specification of forming joins between all children or just between the > first child and the other children. I adapted this new approach of scaling > children of hulled_join() to work with BOSL2 attachables, improving that > interface too. > [image: hjtest2.png] > include <BOSL2/std.scad> > $fn = 128; > > // OpenSCAD hulled_join() examples > size1 = 20; > size2 = 10; > pos1 = [0,0,0]; > pos2 = [size1*0.5+size2*0.3,0,0]; > > // round joints between sphere and other children > translate([0,1.5*size1,0]) > hulled_join(iterations=10) { > hj_scale(pos1, (1+2.0/size1)*[1,1,1]) > translate(pos1) sphere(d=size1); > hj_scale(pos2, (1+1.0/size2)*[1,1,1]) > translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) > cylinder(h=size1, d=size2, center=true); > hj_scale(pos2, (1+1.0/size2)*[1,1,1]) > translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) > cylinder(h=size1, d=size2, center=true); > } > > // chamfer all the joints > hulled_join(all=true) { > hj_scale(pos1, (1+2.0/size1)*[1,1,1]) > translate(pos1) sphere(d=size1); > hj_scale(pos2, (1+1.0/size2)*[1,1,1]) > translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) > cylinder(h=size1, d=size2, center=true); > hj_scale(pos2, (1+1.0/size2)*[1,1,1]) > translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) > cylinder(h=size1, d=size2, center=true); > } > > // BOSL2 hulled_join() example of: > // > https://github.com/BelfrySCAD/BOSL2/wiki/attachments.scad#function-desc_dir > right(40) back(30) > prismoid(20,10,h=15) > attach(RIGHT,BOT) cuboid([4,4,15]) > position(TOP) cyl(d=12,h=5,orient=desc_dir(),anchor=BACK); > > // with hulled_join() joints > module body(ao=false) {prismoid(20,10,h=15, anchor=ao?ao:BOT) children();} > module arm(ao=false) {cuboid([4,4,20], anchor=ao?ao:CTR) children();} > module hand(ao=false) {cyl(d=12,h=5,orient=desc_dir(), anchor=ao?ao:BACK) > children();} > right(40) > body() let(body_desc=parent()) > attach(RIGHT,BOT) arm() let(arm_desc=parent()) > position(TOP) hand() let(hand_desc=parent()) > restore() hulled_join(3) { > // arm first to allow default case of only computing chamfers between > first child and the other children > hja_scale(1.6*[1,1,1], arm_desc) arm(CTR); > hja_scale(1.2*[1,1,1], body_desc) body(CTR); > hja_scale(1.2*[1,1,1], hand_desc) hand(CTR); > }; > > > // > // this code could be in a library > // > $hj_scale=0; > > // to control scaling a child of hulled_join() > module hj_scale(center_point, scale_vector) { > if ($hj_scale) { > sv = [ > 1+$hj_scale*(scale_vector.x-1), > 1+$hj_scale*(scale_vector.y-1), > 1+$hj_scale*(scale_vector.z-1) > ]; > // optimizable to a single translation? > translate(center_point) scale(sv) translate(-1*center_point) > children(); > } > else children(); > } > > // to control scaling a BOSL2 attachable child of hulled_join() > module hja_scale(scale_vector, desc) { > if ($hj_scale) { > sv = [ > 1+$hj_scale*(scale_vector.x-1), > 1+$hj_scale*(scale_vector.y-1), > 1+$hj_scale*(scale_vector.z-1) > ]; > restore(transform_desc(scale(sv, cp=desc_point(desc,anchor=CTR)), > desc)) children(); > } > else restore(desc) children(); > } > > // hulled joining of children objects > // unions objects with a hull of those objects limited to intersections of > the objects with scaled partners > // adjust scale_vector of hj_scale() or hja_scale() in each child to > change the size and angle of joints > // all=true, joints between all children are chamfered > // all=false, only the joints between the 1st child and the other children > are chamfered > module hulled_join(iterations=1, all=false) { > union() { > children(); > for(si=[0:1:iterations-1]) { > for(i=[0:1:all?$children-2:0]) for(j=[i+1:1:$children-1]) > hull() { > intersection() { > children(i, $hj_scale=0); > children(j, $hj_scale=(si+1)/iterations); > } > intersection() { > children(i, $hj_scale=(iterations-si)/iterations); > children(j, $hj_scale=0); > } > } > } > } > } > > On Fri, Dec 12, 2025 at 5:11 AM Rudolf via Discuss < > discuss@lists.openscad.org> wrote: > >> The "trick" is definitely a must-have. However, from this point the >> fillet() operator just takes a snap of the fingers. >> >> You tell the Fillet() operator the # iterations and the limits ... and >> you get this: I am thrilled! >> >> >> $fn=100; >> $camfer=1; >> >> size1 = 10; >> size2 = 5; >> pos1 = [0,0,0]; >> pos2 = [size1*0.5+size2*0.3,0,0]; >> >> module Fillet(x=1.5, y=1.5, iterations=10) >> { >> for(i=[0:$children-1]) children(i); >> >> for(k=[0:iterations-1]) // fillet iteration >> { >> x_ = 1+ k*(x-1)/iterations; >> y_ = y - k*(y-1)/iterations; >> >> for(i=[0:$children-1], j=[0:$children-1]) // Chamfer >> if(i<j) >> hull() >> { >> intersection() >> { >> children(i); >> children(j, $camfer=x_); >> } >> intersection() >> { >> children(i, $camfer=y_); >> children(j); >> } >> } >> } >> } >> >> module cube_(size, center) scale($camfer)cube(size, center); >> module sphere_(r, d) scale($camfer)sphere(r=r, d=d); >> module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, d=d, >> center=center); >> >> Fillet() >> { >> translate([-15, 0, 0])translate(pos1) cube_(10, center=true); >> translate([-15, 0, 0])translate(pos2) rotate(a=[20,20,0]) cube_([10, >> 5, 5], center=true); >> translate(pos1) sphere_(d=size1); >> translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) >> cylinder_(h=size1, d=size2, center=false); >> translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) >> cylinder_(h=size1, d=size2, center=true); >> } >> >> Am 12.12.2025 um 01:42 schrieb Jon Bondy: >> >> Thank you! I tossed that into my bag of tricks! >> >> >> On 12/11/2025 7:00 PM, Rudolf via Discuss wrote: >> >> I like the idea behind your approach and kind of rewrote it into a more >> generic solution by introducing a global variable $camfer and >> camfer-enabled modules cube_(), cylinder_() and sphere_() that allow to be >> scaled by $camfer. >> >> $fn=100; >> $camfer=1; >> >> size1 = 10; >> size2 = 5; >> pos1 = [0,0,0]; >> pos2 = [size1*0.5+size2*0.3,0,0]; >> >> >> module camfer(x=1.2) >> { >> for(i=[0:$children-1]) >> children(i); >> >> for(i=[0:$children-1], j=[0:$children-1]) >> if(i<j) >> hull() >> { >> intersection() >> { >> children(i); >> children(j, $camfer=x); >> } >> intersection() >> { >> children(i, $camfer=x); >> children(j); >> } >> } >> } >> module cube_(size, center) scale($camfer)cube(size, center); >> module sphere_(r, d) scale($camfer)sphere(r=r, d=d); >> module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, d=d, >> center=center); >> >> >> camfer() >> { >> translate(pos1) cube_(10, center=true); >> translate(pos2) rotate(a=[20,20,0]) cube_([6, 5, 5], center=true); >> } >> >> translate([20, 0, 0]) >> camfer() >> { >> translate(pos1) sphere_(d=size1); >> translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) >> cylinder_(h=size1, d=size2, center=true); >> translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) >> cylinder_(h=size1, d=size2, center=true); >> } >> >> >> >> >> Am 08.12.2025 um 00:03 schrieb Todd Allen via Discuss: >> >> module centered_scale(center_point, scale_vector) { >> translate(center_point) >> scale(scale_vector) >> translate(-1*center_point) >> children(0); >> } >> >> // chamfered joining of objects >> // unions objects with a hull of those objects limited to the >> intersection of the objects after scaling >> // adjust object scale factors to change the size and angle of chamfers >> // all=true then joints between all children are chamfered >> // all=false only the joints between the 1st child and the other children >> are chamfered >> module hulled_join(centered_scales, all=false) { >> union() { >> children(); >> for(i=[0:1:all?$children-2:0]) >> for(j=[i+1:1:$children-1]) >> hull() intersection() { >> centered_scale(centered_scales[i][0], >> centered_scales[i][1]) children(i); >> centered_scale(centered_scales[j][0], >> centered_scales[j][1]) children(j); >> union() { >> children(i); >> children(j); >> } >> } >> } >> } >> >> size1 = 10; >> size2 = 5; >> pos1 = [0,0,0]; >> pos2 = [size1*0.5+size2*0.3,0,0]; >> >> // EXAMPLE 1 >> translate([-size1,0,0]) >> hulled_join([ >> [pos1, (1+2.0/size1)*[1,1,1]], >> [pos2, (1+1.0/size2)*[1,1,1]], >> [pos2, (1+1.0/size2)*[1,1,1]] >> ], $fn=128) >> { >> translate(pos1) >> sphere(d=size1); >> translate(pos2) >> rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2, >> center=true); >> translate(pos2) >> rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2, >> center=true); >> } >> >> // EXAMPLE 2 >> translate([size1,0,0]) >> hulled_join([ >> [pos1, (1+1.0/size1)*[1,1,1]], >> [pos2, (1+2.0/size2)*[1,1,1]], >> [pos2, (1+2.0/size2)*[1,1,1]] >> ], all=true, $fn=128) >> { >> translate(pos1) >> sphere(d=size1); >> translate(pos2) >> rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2, >> center=true); >> translate(pos2) >> rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2, >> center=true); >> } >> >> [image: hulljoin.png] >> >> >> On Sat, Dec 6, 2025 at 4:23 PM Todd Allen <speedebikes@gmail.com> wrote: >> >>> Here's an example of how I sometimes chamfer joints using hull(). I >>> have found this useful but it would be more useful if a similar method >>> could be applied between parent and child objects using BOSL2's >>> positioning/alignment/attachment but I don't yet see a good way and would >>> love it if someone could help me do it. >>> >>> module centered_scale(center_point, scale_vector) { >>> translate(center_point) >>> scale(scale_vector) >>> translate(-1*center_point) >>> children(0); >>> } >>> >>> // chamfered joining of objects >>> // unions objects with a hull of those objects limited to the >>> intersection of the objects after scaling >>> // adjust object scale factors to change the size and angle of the >>> chamfer >>> module hulled_join(centered_scales) { >>> union() { >>> children(); >>> hull() intersection() { >>> intersection_for(i=[0:1:$children-1]) >>> centered_scale(centered_scales[i][0], >>> centered_scales[i][1]) >>> children(i); >>> union() >>> children(); >>> } >>> } >>> } >>> >>> size1 = 10; >>> size2 = 5; >>> pos1 = [0,0,0]; >>> pos2 = [size1*0.5+size2*0.3,0,0]; >>> >>> // EXAMPLE 1 >>> hulled_join([ >>> [pos1, (1+1.5/size1)*[1,1,1]], >>> [pos2, (1+1.0/size2)*[1,1,1]] >>> ]) >>> { >>> >> >> _______________________________________________ >> OpenSCAD mailing list >> To unsubscribe send an email to discuss-leave@lists.openscad.org >> >> >> >> <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient> >> Virus-free.www.avg.com >> <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient> >> <#m_-3373310449440574214_m_-1384773676116772327_DAB4FAD8-2DD7-40BB-A1B8-4E2AA1F9FDF2> >> >> _______________________________________________ >> OpenSCAD mailing list >> To unsubscribe send an email to discuss-leave@lists.openscad.org > >
R
Rudolf
Sat, Dec 13, 2025 1:40 AM

Todd,
the revision I made fits well into my libraries in which I redefine the
OpenSCAD primitives (and a lot more primitives) anyway.

Fillet() offers only two limit parameters which it applies to all
operand pairings given. That's right. But if one wants to define limit
parameters in a pairwise (or subset wise) fashion, syntax is his friend:
He can use a nested call pattern and gain maximal control:

Fillet(x0, y0, iter0)
{
  elem0();
  Fillet(x1, y1, iter1)
  {
    elem1();
    elem2();
  }
}

So, the lack of control argument doesn't hold. I think the main
limitation of the approach is "concavity". However it definitely
provides a valuable solution for a wide range of situations, even
scaling a primitive and finding the right limit parameters can be
tricky. What I love is that the approach handles overlapping fillets
like a charm. I already used it twice for prints I wouldn't have added
fillets today.

Am 12.12.2025 um 22:43 schrieb Todd Allen via Discuss:

In the previous code I posted for hulled_join() the BOSL2 example
would union an additional unneeded copy of children().  Here's a fix,
though one will then need to pass children=true for non BOSL2
attachables cases.

module hulled_join(iterations=1, all=false, children=false) {
    union() {
        if (children) children();
        for(si=[0:1:iterations-1]) {
            for(i=[0:1:all?$children-2:0]) for(j=[i+1:1:$children-1])
hull() {
                intersection() {
                    children(i, $hj_scale=0);
                    children(j, $hj_scale=(si+1)/iterations);
                }
                intersection() {
                    children(i, $hj_scale=(iterations-si)/iterations);
                    children(j, $hj_scale=0);
                }
            }
        }
    }
}

On Fri, Dec 12, 2025 at 12:16 PM Todd Allen speedebikes@gmail.com wrote:

 Rudolf, big thanks for your interest in this technique and your
 offered changes!  I hope your enthusiasm is contagious and there
 will be more contributions.

 Your revision of the hull() and intersection() targets brings some
 clarity and I think efficiency to the method.  I like your idea to
 use a $ var to enable scaled children which can eliminate my
 passing of scaling factors as parameters to hulled_join() for a
 cleaner interface.  I prefer scaling in a single separate module
 instead of adding it to cube(), sphere(), etc. as hulled_join()
 can be applied to children formed in any fashion including
 polyhedron(), linear_extrude(),  rotate_extrude(), etc.   I think
 you lost independent scaling factors for each child which gave
 control of the angle of the join.  I liked your addition of
 rounding by iterative proportional adjustment of scaling but
 reworked it to retain independent scale factors for each child.  I
 also retained my “all” parameter enabling the specification of
 forming joins between all children or just between the first child
 and the other children.  I adapted this new approach of scaling
 children of hulled_join() to work with BOSL2 attachables,
 improving that interface too.
 hjtest2.png
 include <BOSL2/std.scad>
 $fn = 128;

 // OpenSCAD hulled_join() examples
 size1 = 20;
 size2 = 10;
 pos1 = [0,0,0];
 pos2 = [size1*0.5+size2*0.3,0,0];

 // round joints between sphere and other children
 translate([0,1.5*size1,0])
 hulled_join(iterations=10) {
     hj_scale(pos1, (1+2.0/size1)*[1,1,1])
         translate(pos1) sphere(d=size1);
     hj_scale(pos2, (1+1.0/size2)*[1,1,1])
         translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
 cylinder(h=size1, d=size2, center=true);
     hj_scale(pos2, (1+1.0/size2)*[1,1,1])
         translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
 cylinder(h=size1, d=size2, center=true);
 }

 // chamfer all the joints
 hulled_join(all=true) {
     hj_scale(pos1, (1+2.0/size1)*[1,1,1])
         translate(pos1) sphere(d=size1);
     hj_scale(pos2, (1+1.0/size2)*[1,1,1])
         translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
 cylinder(h=size1, d=size2, center=true);
     hj_scale(pos2, (1+1.0/size2)*[1,1,1])
         translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
 cylinder(h=size1, d=size2, center=true);
 }

 // BOSL2 hulled_join() example of:
 //
 https://github.com/BelfrySCAD/BOSL2/wiki/attachments.scad#function-desc_dir
 right(40) back(30)
 prismoid(20,10,h=15)
 attach(RIGHT,BOT) cuboid([4,4,15])
 position(TOP) cyl(d=12,h=5,orient=desc_dir(),anchor=BACK);

 // with hulled_join() joints
 module body(ao=false) {prismoid(20,10,h=15, anchor=ao?ao:BOT)
 children();}
 module arm(ao=false) {cuboid([4,4,20], anchor=ao?ao:CTR) children();}
 module hand(ao=false) {cyl(d=12,h=5,orient=desc_dir(),
 anchor=ao?ao:BACK) children();}
 right(40)
 body() let(body_desc=parent())
 attach(RIGHT,BOT) arm() let(arm_desc=parent())
 position(TOP) hand() let(hand_desc=parent())
 restore() hulled_join(3) {
     // arm first to allow default case of only computing chamfers
 between first child and the other children
     hja_scale(1.6*[1,1,1], arm_desc) arm(CTR);
     hja_scale(1.2*[1,1,1], body_desc) body(CTR);
     hja_scale(1.2*[1,1,1], hand_desc) hand(CTR);
 };


 //
 // this code could be in a library
 //
 $hj_scale=0;

 // to control scaling a child of hulled_join()
 module hj_scale(center_point, scale_vector) {
     if ($hj_scale) {
         sv = [
             1+$hj_scale*(scale_vector.x-1),
             1+$hj_scale*(scale_vector.y-1),
             1+$hj_scale*(scale_vector.z-1)
         ];
         // optimizable to a single translation?
         translate(center_point) scale(sv)
 translate(-1*center_point) children();
     }
     else children();
 }

 // to control scaling a BOSL2 attachable child of hulled_join()
 module hja_scale(scale_vector, desc) {
     if ($hj_scale) {
         sv = [
             1+$hj_scale*(scale_vector.x-1),
             1+$hj_scale*(scale_vector.y-1),
             1+$hj_scale*(scale_vector.z-1)
         ];
         restore(transform_desc(scale(sv,
 cp=desc_point(desc,anchor=CTR)), desc)) children();
     }
     else restore(desc) children();
 }

 // hulled joining of children objects
 // unions objects with a hull of those objects limited to
 intersections of the objects with scaled partners
 // adjust scale_vector of hj_scale() or hja_scale() in each child
 to change the size and angle of joints
 // all=true, joints between all children are chamfered
 // all=false, only the joints between the 1st child and the other
 children are chamfered
 module hulled_join(iterations=1, all=false) {
     union() {
         children();
         for(si=[0:1:iterations-1]) {
             for(i=[0:1:all?$children-2:0])
 for(j=[i+1:1:$children-1]) hull() {
                 intersection() {
                     children(i, $hj_scale=0);
                     children(j, $hj_scale=(si+1)/iterations);
                 }
                 intersection() {
                     children(i, $hj_scale=(iterations-si)/iterations);
                     children(j, $hj_scale=0);
                 }
             }
         }
     }
 }

 On Fri, Dec 12, 2025 at 5:11 AM Rudolf via Discuss
 <discuss@lists.openscad.org> wrote:

     The "trick" is definitely a must-have. However, from this
     point the fillet() operator just takes a snap of the fingers.

     You tell the Fillet() operator the # iterations and the limits
     ... and you get this: I am thrilled!


     $fn=100;
     $camfer=1;

     size1 = 10;
     size2 = 5;
     pos1 = [0,0,0];
     pos2 = [size1*0.5+size2*0.3,0,0];

     module Fillet(x=1.5, y=1.5, iterations=10)
     {
       for(i=[0:$children-1]) children(i);

       for(k=[0:iterations-1]) // fillet iteration
       {
         x_ = 1+ k*(x-1)/iterations;
         y_ = y - k*(y-1)/iterations;

         for(i=[0:$children-1], j=[0:$children-1]) // Chamfer
         if(i<j)
         hull()
         {
           intersection()
           {
             children(i);
             children(j, $camfer=x_);
           }
           intersection()
           {
             children(i, $camfer=y_);
             children(j);
           }
         }
       }
     }

     module cube_(size, center) scale($camfer)cube(size, center);
     module sphere_(r, d) scale($camfer)sphere(r=r, d=d);
     module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h,
     r=r, d=d, center=center);

     Fillet()
       {
        translate([-15, 0, 0])translate(pos1) cube_(10, center=true);
        translate([-15, 0, 0])translate(pos2) rotate(a=[20,20,0])
     cube_([10, 5, 5], center=true);
        translate(pos1) sphere_(d=size1);
        translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
     cylinder_(h=size1, d=size2, center=false);
        translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
     cylinder_(h=size1, d=size2, center=true);
       }

     Am 12.12.2025 um 01:42 schrieb Jon Bondy:
     Thank you!  I tossed that into my bag of tricks!


     On 12/11/2025 7:00 PM, Rudolf via Discuss wrote:
     I like the idea behind your approach and kind of rewrote it
     into a more generic solution  by introducing a global
     variable $camfer and camfer-enabled modules cube_(),
     cylinder_() and sphere_() that allow to be scaled by $camfer.

     $fn=100;
     $camfer=1;

     size1 = 10;
     size2 = 5;
     pos1 = [0,0,0];
     pos2 = [size1*0.5+size2*0.3,0,0];


     module camfer(x=1.2)
     {
       for(i=[0:$children-1])
       children(i);

       for(i=[0:$children-1], j=[0:$children-1])
       if(i<j)
       hull()
       {
         intersection()
         {
           children(i);
           children(j, $camfer=x);
         }
         intersection()
         {
           children(i, $camfer=x);
           children(j);
         }
       }
     }
     module cube_(size, center) scale($camfer)cube(size, center);
     module sphere_(r, d) scale($camfer)sphere(r=r, d=d);
     module cylinder_(h, r, d, center)
     scale($camfer)cylinder(h=h, r=r, d=d, center=center);


     camfer()
     {
       translate(pos1) cube_(10, center=true);
       translate(pos2) rotate(a=[20,20,0]) cube_([6, 5, 5],
     center=true);
     }

     translate([20, 0, 0])
       camfer()
       {
         translate(pos1) sphere_(d=size1);
         translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
     cylinder_(h=size1, d=size2, center=true);
         translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
     cylinder_(h=size1, d=size2, center=true);
       }




     Am 08.12.2025 um 00:03 schrieb Todd Allen via Discuss:
     module centered_scale(center_point, scale_vector) {
         translate(center_point)
             scale(scale_vector)
                 translate(-1*center_point)
                     children(0);
     }

     // chamfered joining of objects
     // unions objects with a hull of those objects limited to
     the intersection of the objects after scaling
     // adjust object scale factors to change the size and angle
     of chamfers
     // all=true then joints between all children are chamfered
     // all=false only the joints between the 1st child and the
     other children are chamfered
     module hulled_join(centered_scales, all=false) {
         union() {
             children();
             for(i=[0:1:all?$children-2:0])
                 for(j=[i+1:1:$children-1])
                     hull() intersection() {
     centered_scale(centered_scales[i][0],
     centered_scales[i][1]) children(i);
     centered_scale(centered_scales[j][0],
     centered_scales[j][1]) children(j);
                         union() {
                             children(i);
                             children(j);
                         }
                     }
         }
     }

     size1 = 10;
     size2 = 5;
     pos1 = [0,0,0];
     pos2 = [size1*0.5+size2*0.3,0,0];

     //  EXAMPLE 1
     translate([-size1,0,0])
     hulled_join([
         [pos1, (1+2.0/size1)*[1,1,1]],
         [pos2, (1+1.0/size2)*[1,1,1]],
         [pos2, (1+1.0/size2)*[1,1,1]]
     ], $fn=128)
     {
         translate(pos1)
             sphere(d=size1);
         translate(pos2)
             rotate(a=[90,20,0]) scale([1,0.5,1])
     cylinder(h=size1, d=size2, center=true);
         translate(pos2)
             rotate(a=[90,20,90]) scale([0.5,1,1])
     cylinder(h=size1, d=size2, center=true);
     }

     //  EXAMPLE 2
     translate([size1,0,0])
     hulled_join([
         [pos1, (1+1.0/size1)*[1,1,1]],
         [pos2, (1+2.0/size2)*[1,1,1]],
         [pos2, (1+2.0/size2)*[1,1,1]]
     ], all=true, $fn=128)
     {
         translate(pos1)
         sphere(d=size1);
         translate(pos2)
         rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1,
     d=size2, center=true);
         translate(pos2)
         rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1,
     d=size2, center=true);
     }

     hulljoin.png

     On Sat, Dec 6, 2025 at 4:23 PM Todd Allen
     <speedebikes@gmail.com> wrote:

         Here's an example of how I sometimes
         chamfer joints using hull().  I have found this useful
         but it would be more useful if a similar method could
         be applied between parent and child objects using
         BOSL2's positioning/alignment/attachment but I don't
         yet see a good way and would love it if someone could
         help me do it.

         module centered_scale(center_point, scale_vector) {
             translate(center_point)
                 scale(scale_vector)
                     translate(-1*center_point)
                         children(0);
         }

         // chamfered joining of objects
         // unions objects with a hull of those objects limited
         to the intersection of the objects after scaling
         // adjust object scale factors to change the size and
         angle of the chamfer
         module hulled_join(centered_scales) {
             union() {
                 children();
                 hull() intersection() {
         intersection_for(i=[0:1:$children-1])
         centered_scale(centered_scales[i][0],
         centered_scales[i][1])
                             children(i);
                     union()
                         children();
                 }
             }
         }

         size1 = 10;
         size2 = 5;
         pos1 = [0,0,0];
         pos2 = [size1*0.5+size2*0.3,0,0];

         //  EXAMPLE 1
         hulled_join([
             [pos1, (1+1.5/size1)*[1,1,1]],
             [pos2, (1+1.0/size2)*[1,1,1]]
             ])
         {
     _______________________________________________
     OpenSCAD mailing list
     To unsubscribe send an email todiscuss-leave@lists.openscad.org
     <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient>
     	Virus-free.www.avg.com
     <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient>


     <#m_-3373310449440574214_m_-1384773676116772327_DAB4FAD8-2DD7-40BB-A1B8-4E2AA1F9FDF2>
     _______________________________________________
     OpenSCAD mailing list
     To unsubscribe send an email to discuss-leave@lists.openscad.org

OpenSCAD mailing list
To unsubscribe send an email todiscuss-leave@lists.openscad.org

Todd, the revision I made fits well into my libraries in which I redefine the OpenSCAD primitives (and a lot more primitives) anyway. Fillet() offers only two limit parameters which it applies to all operand pairings given. That's right. But if one wants to define limit parameters in a pairwise (or subset wise) fashion, syntax is his friend: He can use a nested call pattern and gain maximal control: Fillet(x0, y0, iter0) {   elem0();   Fillet(x1, y1, iter1)   {     elem1();     elem2();   } } So, the lack of control argument doesn't hold. I think the main limitation of the approach is "concavity". However it definitely provides a valuable solution for a wide range of situations, even scaling a primitive and finding the right limit parameters can be tricky. What I love is that the approach handles overlapping fillets like a charm. I already used it twice for prints I wouldn't have added fillets today. Am 12.12.2025 um 22:43 schrieb Todd Allen via Discuss: > In the previous code I posted for hulled_join() the BOSL2 example > would union an additional unneeded copy of children().  Here's a fix, > though one will then need to pass children=true for non BOSL2 > attachables cases. > > module hulled_join(iterations=1, all=false, children=false) { >     union() { >         if (children) children(); >         for(si=[0:1:iterations-1]) { >             for(i=[0:1:all?$children-2:0]) for(j=[i+1:1:$children-1]) > hull() { >                 intersection() { >                     children(i, $hj_scale=0); >                     children(j, $hj_scale=(si+1)/iterations); >                 } >                 intersection() { >                     children(i, $hj_scale=(iterations-si)/iterations); >                     children(j, $hj_scale=0); >                 } >             } >         } >     } > } > > On Fri, Dec 12, 2025 at 12:16 PM Todd Allen <speedebikes@gmail.com> wrote: > > Rudolf, big thanks for your interest in this technique and your > offered changes!  I hope your enthusiasm is contagious and there > will be more contributions. > > Your revision of the hull() and intersection() targets brings some > clarity and I think efficiency to the method.  I like your idea to > use a $ var to enable scaled children which can eliminate my > passing of scaling factors as parameters to hulled_join() for a > cleaner interface.  I prefer scaling in a single separate module > instead of adding it to cube(), sphere(), etc. as hulled_join() > can be applied to children formed in any fashion including > polyhedron(), linear_extrude(),  rotate_extrude(), etc.   I think > you lost independent scaling factors for each child which gave > control of the angle of the join.  I liked your addition of > rounding by iterative proportional adjustment of scaling but > reworked it to retain independent scale factors for each child.  I > also retained my “all” parameter enabling the specification of > forming joins between all children or just between the first child > and the other children.  I adapted this new approach of scaling > children of hulled_join() to work with BOSL2 attachables, > improving that interface too. > hjtest2.png > include <BOSL2/std.scad> > $fn = 128; > > // OpenSCAD hulled_join() examples > size1 = 20; > size2 = 10; > pos1 = [0,0,0]; > pos2 = [size1*0.5+size2*0.3,0,0]; > > // round joints between sphere and other children > translate([0,1.5*size1,0]) > hulled_join(iterations=10) { >     hj_scale(pos1, (1+2.0/size1)*[1,1,1]) >         translate(pos1) sphere(d=size1); >     hj_scale(pos2, (1+1.0/size2)*[1,1,1]) >         translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) > cylinder(h=size1, d=size2, center=true); >     hj_scale(pos2, (1+1.0/size2)*[1,1,1]) >         translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) > cylinder(h=size1, d=size2, center=true); > } > > // chamfer all the joints > hulled_join(all=true) { >     hj_scale(pos1, (1+2.0/size1)*[1,1,1]) >         translate(pos1) sphere(d=size1); >     hj_scale(pos2, (1+1.0/size2)*[1,1,1]) >         translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) > cylinder(h=size1, d=size2, center=true); >     hj_scale(pos2, (1+1.0/size2)*[1,1,1]) >         translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) > cylinder(h=size1, d=size2, center=true); > } > > // BOSL2 hulled_join() example of: > // > https://github.com/BelfrySCAD/BOSL2/wiki/attachments.scad#function-desc_dir > right(40) back(30) > prismoid(20,10,h=15) > attach(RIGHT,BOT) cuboid([4,4,15]) > position(TOP) cyl(d=12,h=5,orient=desc_dir(),anchor=BACK); > > // with hulled_join() joints > module body(ao=false) {prismoid(20,10,h=15, anchor=ao?ao:BOT) > children();} > module arm(ao=false) {cuboid([4,4,20], anchor=ao?ao:CTR) children();} > module hand(ao=false) {cyl(d=12,h=5,orient=desc_dir(), > anchor=ao?ao:BACK) children();} > right(40) > body() let(body_desc=parent()) > attach(RIGHT,BOT) arm() let(arm_desc=parent()) > position(TOP) hand() let(hand_desc=parent()) > restore() hulled_join(3) { >     // arm first to allow default case of only computing chamfers > between first child and the other children >     hja_scale(1.6*[1,1,1], arm_desc) arm(CTR); >     hja_scale(1.2*[1,1,1], body_desc) body(CTR); >     hja_scale(1.2*[1,1,1], hand_desc) hand(CTR); > }; > > > // > // this code could be in a library > // > $hj_scale=0; > > // to control scaling a child of hulled_join() > module hj_scale(center_point, scale_vector) { >     if ($hj_scale) { >         sv = [ >             1+$hj_scale*(scale_vector.x-1), >             1+$hj_scale*(scale_vector.y-1), >             1+$hj_scale*(scale_vector.z-1) >         ]; >         // optimizable to a single translation? >         translate(center_point) scale(sv) > translate(-1*center_point) children(); >     } >     else children(); > } > > // to control scaling a BOSL2 attachable child of hulled_join() > module hja_scale(scale_vector, desc) { >     if ($hj_scale) { >         sv = [ >             1+$hj_scale*(scale_vector.x-1), >             1+$hj_scale*(scale_vector.y-1), >             1+$hj_scale*(scale_vector.z-1) >         ]; >         restore(transform_desc(scale(sv, > cp=desc_point(desc,anchor=CTR)), desc)) children(); >     } >     else restore(desc) children(); > } > > // hulled joining of children objects > // unions objects with a hull of those objects limited to > intersections of the objects with scaled partners > // adjust scale_vector of hj_scale() or hja_scale() in each child > to change the size and angle of joints > // all=true, joints between all children are chamfered > // all=false, only the joints between the 1st child and the other > children are chamfered > module hulled_join(iterations=1, all=false) { >     union() { >         children(); >         for(si=[0:1:iterations-1]) { >             for(i=[0:1:all?$children-2:0]) > for(j=[i+1:1:$children-1]) hull() { >                 intersection() { >                     children(i, $hj_scale=0); >                     children(j, $hj_scale=(si+1)/iterations); >                 } >                 intersection() { >                     children(i, $hj_scale=(iterations-si)/iterations); >                     children(j, $hj_scale=0); >                 } >             } >         } >     } > } > > On Fri, Dec 12, 2025 at 5:11 AM Rudolf via Discuss > <discuss@lists.openscad.org> wrote: > > The "trick" is definitely a must-have. However, from this > point the fillet() operator just takes a snap of the fingers. > > You tell the Fillet() operator the # iterations and the limits > ... and you get this: I am thrilled! > > > $fn=100; > $camfer=1; > > size1 = 10; > size2 = 5; > pos1 = [0,0,0]; > pos2 = [size1*0.5+size2*0.3,0,0]; > > module Fillet(x=1.5, y=1.5, iterations=10) > { >   for(i=[0:$children-1]) children(i); > >   for(k=[0:iterations-1]) // fillet iteration >   { >     x_ = 1+ k*(x-1)/iterations; >     y_ = y - k*(y-1)/iterations; > >     for(i=[0:$children-1], j=[0:$children-1]) // Chamfer >     if(i<j) >     hull() >     { >       intersection() >       { >         children(i); >         children(j, $camfer=x_); >       } >       intersection() >       { >         children(i, $camfer=y_); >         children(j); >       } >     } >   } > } > > module cube_(size, center) scale($camfer)cube(size, center); > module sphere_(r, d) scale($camfer)sphere(r=r, d=d); > module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, > r=r, d=d, center=center); > > Fillet() >   { >    translate([-15, 0, 0])translate(pos1) cube_(10, center=true); >    translate([-15, 0, 0])translate(pos2) rotate(a=[20,20,0]) > cube_([10, 5, 5], center=true); >    translate(pos1) sphere_(d=size1); >    translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) > cylinder_(h=size1, d=size2, center=false); >    translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) > cylinder_(h=size1, d=size2, center=true); >   } > > Am 12.12.2025 um 01:42 schrieb Jon Bondy: >> >> Thank you!  I tossed that into my bag of tricks! >> >> >> On 12/11/2025 7:00 PM, Rudolf via Discuss wrote: >>> >>> I like the idea behind your approach and kind of rewrote it >>> into a more generic solution  by introducing a global >>> variable $camfer and camfer-enabled modules cube_(), >>> cylinder_() and sphere_() that allow to be scaled by $camfer. >>> >>> $fn=100; >>> $camfer=1; >>> >>> size1 = 10; >>> size2 = 5; >>> pos1 = [0,0,0]; >>> pos2 = [size1*0.5+size2*0.3,0,0]; >>> >>> >>> module camfer(x=1.2) >>> { >>>   for(i=[0:$children-1]) >>>   children(i); >>> >>>   for(i=[0:$children-1], j=[0:$children-1]) >>>   if(i<j) >>>   hull() >>>   { >>>     intersection() >>>     { >>>       children(i); >>>       children(j, $camfer=x); >>>     } >>>     intersection() >>>     { >>>       children(i, $camfer=x); >>>       children(j); >>>     } >>>   } >>> } >>> module cube_(size, center) scale($camfer)cube(size, center); >>> module sphere_(r, d) scale($camfer)sphere(r=r, d=d); >>> module cylinder_(h, r, d, center) >>> scale($camfer)cylinder(h=h, r=r, d=d, center=center); >>> >>> >>> camfer() >>> { >>>   translate(pos1) cube_(10, center=true); >>>   translate(pos2) rotate(a=[20,20,0]) cube_([6, 5, 5], >>> center=true); >>> } >>> >>> translate([20, 0, 0]) >>>   camfer() >>>   { >>>     translate(pos1) sphere_(d=size1); >>>     translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) >>> cylinder_(h=size1, d=size2, center=true); >>>     translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) >>> cylinder_(h=size1, d=size2, center=true); >>>   } >>> >>> >>> >>> >>> Am 08.12.2025 um 00:03 schrieb Todd Allen via Discuss: >>>> module centered_scale(center_point, scale_vector) { >>>>     translate(center_point) >>>>         scale(scale_vector) >>>>             translate(-1*center_point) >>>>                 children(0); >>>> } >>>> >>>> // chamfered joining of objects >>>> // unions objects with a hull of those objects limited to >>>> the intersection of the objects after scaling >>>> // adjust object scale factors to change the size and angle >>>> of chamfers >>>> // all=true then joints between all children are chamfered >>>> // all=false only the joints between the 1st child and the >>>> other children are chamfered >>>> module hulled_join(centered_scales, all=false) { >>>>     union() { >>>>         children(); >>>>         for(i=[0:1:all?$children-2:0]) >>>>             for(j=[i+1:1:$children-1]) >>>>                 hull() intersection() { >>>> centered_scale(centered_scales[i][0], >>>> centered_scales[i][1]) children(i); >>>> centered_scale(centered_scales[j][0], >>>> centered_scales[j][1]) children(j); >>>>                     union() { >>>>                         children(i); >>>>                         children(j); >>>>                     } >>>>                 } >>>>     } >>>> } >>>> >>>> size1 = 10; >>>> size2 = 5; >>>> pos1 = [0,0,0]; >>>> pos2 = [size1*0.5+size2*0.3,0,0]; >>>> >>>> //  EXAMPLE 1 >>>> translate([-size1,0,0]) >>>> hulled_join([ >>>>     [pos1, (1+2.0/size1)*[1,1,1]], >>>>     [pos2, (1+1.0/size2)*[1,1,1]], >>>>     [pos2, (1+1.0/size2)*[1,1,1]] >>>> ], $fn=128) >>>> { >>>>     translate(pos1) >>>>         sphere(d=size1); >>>>     translate(pos2) >>>>         rotate(a=[90,20,0]) scale([1,0.5,1]) >>>> cylinder(h=size1, d=size2, center=true); >>>>     translate(pos2) >>>>         rotate(a=[90,20,90]) scale([0.5,1,1]) >>>> cylinder(h=size1, d=size2, center=true); >>>> } >>>> >>>> //  EXAMPLE 2 >>>> translate([size1,0,0]) >>>> hulled_join([ >>>>     [pos1, (1+1.0/size1)*[1,1,1]], >>>>     [pos2, (1+2.0/size2)*[1,1,1]], >>>>     [pos2, (1+2.0/size2)*[1,1,1]] >>>> ], all=true, $fn=128) >>>> { >>>>     translate(pos1) >>>>     sphere(d=size1); >>>>     translate(pos2) >>>>     rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, >>>> d=size2, center=true); >>>>     translate(pos2) >>>>     rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, >>>> d=size2, center=true); >>>> } >>>> >>>> hulljoin.png >>>> >>>> On Sat, Dec 6, 2025 at 4:23 PM Todd Allen >>>> <speedebikes@gmail.com> wrote: >>>> >>>> Here's an example of how I sometimes >>>> chamfer joints using hull().  I have found this useful >>>> but it would be more useful if a similar method could >>>> be applied between parent and child objects using >>>> BOSL2's positioning/alignment/attachment but I don't >>>> yet see a good way and would love it if someone could >>>> help me do it. >>>> >>>> module centered_scale(center_point, scale_vector) { >>>>     translate(center_point) >>>>         scale(scale_vector) >>>>             translate(-1*center_point) >>>>                 children(0); >>>> } >>>> >>>> // chamfered joining of objects >>>> // unions objects with a hull of those objects limited >>>> to the intersection of the objects after scaling >>>> // adjust object scale factors to change the size and >>>> angle of the chamfer >>>> module hulled_join(centered_scales) { >>>>     union() { >>>>         children(); >>>>         hull() intersection() { >>>> intersection_for(i=[0:1:$children-1]) >>>> centered_scale(centered_scales[i][0], >>>> centered_scales[i][1]) >>>>                     children(i); >>>>             union() >>>>                 children(); >>>>         } >>>>     } >>>> } >>>> >>>> size1 = 10; >>>> size2 = 5; >>>> pos1 = [0,0,0]; >>>> pos2 = [size1*0.5+size2*0.3,0,0]; >>>> >>>> //  EXAMPLE 1 >>>> hulled_join([ >>>>     [pos1, (1+1.5/size1)*[1,1,1]], >>>>     [pos2, (1+1.0/size2)*[1,1,1]] >>>>     ]) >>>> { >>>> >>> >>> _______________________________________________ >>> OpenSCAD mailing list >>> To unsubscribe send an email todiscuss-leave@lists.openscad.org >> >> <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient> >> Virus-free.www.avg.com >> <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient> >> >> >> <#m_-3373310449440574214_m_-1384773676116772327_DAB4FAD8-2DD7-40BB-A1B8-4E2AA1F9FDF2> > _______________________________________________ > OpenSCAD mailing list > To unsubscribe send an email to discuss-leave@lists.openscad.org > > > _______________________________________________ > OpenSCAD mailing list > To unsubscribe send an email todiscuss-leave@lists.openscad.org
TA
Todd Allen
Sat, Dec 13, 2025 4:01 PM

if one wants to define limit parameters in a pairwise (or subset wise)

fashion, syntax is his friend: He can use a nested call pattern and gain
maximal control
Nesting calls is semantically different and increases computation.

So, the lack of control argument doesn't hold. … finding the right limit

parameters can be tricky.
When there was a scaling vector associated with each child, if say the
child was a cylinder one could make a join that would extend 1 mm from the
radius and 2 mm from the ends.  Or if it was a cube of non uniform
dimensions one could make the join extend uniformly in width, depth and
height.  You named your scalars X & Y despite them having nothing to do
with a coordinate system nor even being orthogonal controls and made it
tricky to find the right limits.

I think the main limitation of the approach is "concavity"

Which the name hulled_join() conveyed.  It’s a bigger problem when you
name the operation fillet() which also confuses that the operation is
useful for chamfers.

On Fri, Dec 12, 2025 at 7:41 PM Rudolf via Discuss <
discuss@lists.openscad.org> wrote:

Todd,
the revision I made fits well into my libraries in which I redefine the
OpenSCAD primitives (and a lot more primitives) anyway.

Fillet() offers only two limit parameters which it applies to all operand
pairings given. That's right. But if one wants to define limit parameters
in a pairwise (or subset wise) fashion, syntax is his friend: He can use a
nested call pattern and gain maximal control:

Fillet(x0, y0, iter0)
{
elem0();
Fillet(x1, y1, iter1)
{
elem1();
elem2();
}
}

So, the lack of control argument doesn't hold. I think the main limitation
of the approach is "concavity". However it definitely provides a valuable
solution for a wide range of situations, even scaling a primitive and
finding the right limit parameters can be tricky. What I love is that the
approach handles overlapping fillets like a charm. I already used it twice
for prints I wouldn't have added fillets today.

Am 12.12.2025 um 22:43 schrieb Todd Allen via Discuss:

In the previous code I posted for hulled_join() the BOSL2 example would
union an additional unneeded copy of children().  Here's a fix, though one
will then need to pass children=true for non BOSL2 attachables cases.

module hulled_join(iterations=1, all=false, children=false) {
union() {
if (children) children();
for(si=[0:1:iterations-1]) {
for(i=[0:1:all?$children-2:0]) for(j=[i+1:1:$children-1])
hull() {
intersection() {
children(i, $hj_scale=0);
children(j, $hj_scale=(si+1)/iterations);
}
intersection() {
children(i, $hj_scale=(iterations-si)/iterations);
children(j, $hj_scale=0);
}
}
}
}
}

On Fri, Dec 12, 2025 at 12:16 PM Todd Allen speedebikes@gmail.com wrote:

Rudolf, big thanks for your interest in this technique and your offered
changes!  I hope your enthusiasm is contagious and there will be more
contributions.

Your revision of the hull() and intersection() targets brings some
clarity and I think efficiency to the method.  I like your idea to use a $
var to enable scaled children which can eliminate my passing of scaling
factors as parameters to hulled_join() for a cleaner interface.  I prefer
scaling in a single separate module instead of adding it to cube(),
sphere(), etc. as hulled_join() can be applied to children formed in any
fashion including polyhedron(), linear_extrude(),  rotate_extrude(), etc.
I think you lost independent scaling factors for each child which gave
control of the angle of the join.  I liked your addition of rounding by
iterative proportional adjustment of scaling but reworked it to retain
independent scale factors for each child.  I also retained my “all”
parameter enabling the specification of forming joins between all children
or just between the first child and the other children.  I adapted this new
approach of scaling children of hulled_join() to work with BOSL2
attachables, improving that interface too.
[image: hjtest2.png]
include <BOSL2/std.scad>
$fn = 128;

// OpenSCAD hulled_join() examples
size1 = 20;
size2 = 10;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

// round joints between sphere and other children
translate([0,1.5size1,0])
hulled_join(iterations=10) {
hj_scale(pos1, (1+2.0/size1)
[1,1,1])
translate(pos1) sphere(d=size1);
hj_scale(pos2, (1+1.0/size2)[1,1,1])
translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder(h=size1, d=size2, center=true);
hj_scale(pos2, (1+1.0/size2)
[1,1,1])
translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder(h=size1, d=size2, center=true);
}

// chamfer all the joints
hulled_join(all=true) {
hj_scale(pos1, (1+2.0/size1)[1,1,1])
translate(pos1) sphere(d=size1);
hj_scale(pos2, (1+1.0/size2)
[1,1,1])
translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder(h=size1, d=size2, center=true);
hj_scale(pos2, (1+1.0/size2)*[1,1,1])
translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder(h=size1, d=size2, center=true);
}

// BOSL2 hulled_join() example of:
//
https://github.com/BelfrySCAD/BOSL2/wiki/attachments.scad#function-desc_dir
right(40) back(30)
prismoid(20,10,h=15)
attach(RIGHT,BOT) cuboid([4,4,15])
position(TOP) cyl(d=12,h=5,orient=desc_dir(),anchor=BACK);

// with hulled_join() joints
module body(ao=false) {prismoid(20,10,h=15, anchor=ao?ao:BOT) children();}
module arm(ao=false) {cuboid([4,4,20], anchor=ao?ao:CTR) children();}
module hand(ao=false) {cyl(d=12,h=5,orient=desc_dir(), anchor=ao?ao:BACK)
children();}
right(40)
body() let(body_desc=parent())
attach(RIGHT,BOT) arm() let(arm_desc=parent())
position(TOP) hand() let(hand_desc=parent())
restore() hulled_join(3) {
// arm first to allow default case of only computing chamfers between
first child and the other children
hja_scale(1.6*[1,1,1], arm_desc) arm(CTR);
hja_scale(1.2*[1,1,1], body_desc) body(CTR);
hja_scale(1.2*[1,1,1], hand_desc) hand(CTR);
};

//
// this code could be in a library
//
$hj_scale=0;

// to control scaling a child of hulled_join()
module hj_scale(center_point, scale_vector) {
if ($hj_scale) {
sv = [
1+$hj_scale*(scale_vector.x-1),
1+$hj_scale*(scale_vector.y-1),
1+$hj_scale*(scale_vector.z-1)
];
// optimizable to a single translation?
translate(center_point) scale(sv) translate(-1*center_point)
children();
}
else children();
}

// to control scaling a BOSL2 attachable child of hulled_join()
module hja_scale(scale_vector, desc) {
if ($hj_scale) {
sv = [
1+$hj_scale*(scale_vector.x-1),
1+$hj_scale*(scale_vector.y-1),
1+$hj_scale*(scale_vector.z-1)
];
restore(transform_desc(scale(sv, cp=desc_point(desc,anchor=CTR)),
desc)) children();
}
else restore(desc) children();
}

// hulled joining of children objects
// unions objects with a hull of those objects limited to intersections
of the objects with scaled partners
// adjust scale_vector of hj_scale() or hja_scale() in each child to
change the size and angle of joints
// all=true, joints between all children are chamfered
// all=false, only the joints between the 1st child and the other
children are chamfered
module hulled_join(iterations=1, all=false) {
union() {
children();
for(si=[0:1:iterations-1]) {
for(i=[0:1:all?$children-2:0]) for(j=[i+1:1:$children-1])
hull() {
intersection() {
children(i, $hj_scale=0);
children(j, $hj_scale=(si+1)/iterations);
}
intersection() {
children(i, $hj_scale=(iterations-si)/iterations);
children(j, $hj_scale=0);
}
}
}
}
}

On Fri, Dec 12, 2025 at 5:11 AM Rudolf via Discuss <
discuss@lists.openscad.org> wrote:

The "trick" is definitely a must-have. However, from this point the
fillet() operator just takes a snap of the fingers.

You tell the Fillet() operator the # iterations and the limits ... and
you get this: I am thrilled!

$fn=100;
$camfer=1;

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

module Fillet(x=1.5, y=1.5, iterations=10)
{
for(i=[0:$children-1]) children(i);

for(k=[0:iterations-1]) // fillet iteration
{
x_ = 1+ k*(x-1)/iterations;
y_ = y - k*(y-1)/iterations;

 for(i=[0:$children-1], j=[0:$children-1]) // Chamfer
 if(i<j)
 hull()
 {
   intersection()
   {
     children(i);
     children(j, $camfer=x_);
   }
   intersection()
   {
     children(i, $camfer=y_);
     children(j);
   }
 }

}
}

module cube_(size, center) scale($camfer)cube(size, center);
module sphere_(r, d) scale($camfer)sphere(r=r, d=d);
module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, d=d,
center=center);

Fillet()
{
translate([-15, 0, 0])translate(pos1) cube_(10, center=true);
translate([-15, 0, 0])translate(pos2) rotate(a=[20,20,0]) cube_([10,
5, 5], center=true);
translate(pos1) sphere_(d=size1);
translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder_(h=size1, d=size2, center=false);
translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder_(h=size1, d=size2, center=true);
}

Am 12.12.2025 um 01:42 schrieb Jon Bondy:

Thank you!  I tossed that into my bag of tricks!

On 12/11/2025 7:00 PM, Rudolf via Discuss wrote:

I like the idea behind your approach and kind of rewrote it into a more
generic solution  by introducing a global variable $camfer and
camfer-enabled modules cube_(), cylinder_() and sphere_() that allow to be
scaled by $camfer.

$fn=100;
$camfer=1;

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

module camfer(x=1.2)
{
for(i=[0:$children-1])
children(i);

for(i=[0:$children-1], j=[0:$children-1])
if(i<j)
hull()
{
intersection()
{
children(i);
children(j, $camfer=x);
}
intersection()
{
children(i, $camfer=x);
children(j);
}
}
}
module cube_(size, center) scale($camfer)cube(size, center);
module sphere_(r, d) scale($camfer)sphere(r=r, d=d);
module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, d=d,
center=center);

camfer()
{
translate(pos1) cube_(10, center=true);
translate(pos2) rotate(a=[20,20,0]) cube_([6, 5, 5], center=true);
}

translate([20, 0, 0])
camfer()
{
translate(pos1) sphere_(d=size1);
translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
cylinder_(h=size1, d=size2, center=true);
translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
cylinder_(h=size1, d=size2, center=true);
}

Am 08.12.2025 um 00:03 schrieb Todd Allen via Discuss:

module centered_scale(center_point, scale_vector) {
translate(center_point)
scale(scale_vector)
translate(-1*center_point)
children(0);
}

// chamfered joining of objects
// unions objects with a hull of those objects limited to the
intersection of the objects after scaling
// adjust object scale factors to change the size and angle of chamfers
// all=true then joints between all children are chamfered
// all=false only the joints between the 1st child and the other
children are chamfered
module hulled_join(centered_scales, all=false) {
union() {
children();
for(i=[0:1:all?$children-2:0])
for(j=[i+1:1:$children-1])
hull() intersection() {
centered_scale(centered_scales[i][0],
centered_scales[i][1]) children(i);
centered_scale(centered_scales[j][0],
centered_scales[j][1]) children(j);
union() {
children(i);
children(j);
}
}
}
}

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

//  EXAMPLE 1
translate([-size1,0,0])
hulled_join([
[pos1, (1+2.0/size1)[1,1,1]],
[pos2, (1+1.0/size2)
[1,1,1]],
[pos2, (1+1.0/size2)*[1,1,1]]
], $fn=128)
{
translate(pos1)
sphere(d=size1);
translate(pos2)
rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2,
center=true);
translate(pos2)
rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2,
center=true);
}

//  EXAMPLE 2
translate([size1,0,0])
hulled_join([
[pos1, (1+1.0/size1)[1,1,1]],
[pos2, (1+2.0/size2)
[1,1,1]],
[pos2, (1+2.0/size2)*[1,1,1]]
], all=true, $fn=128)
{
translate(pos1)
sphere(d=size1);
translate(pos2)
rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2,
center=true);
translate(pos2)
rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2,
center=true);
}

[image: hulljoin.png]

On Sat, Dec 6, 2025 at 4:23 PM Todd Allen speedebikes@gmail.com wrote:

Here's an example of how I sometimes chamfer joints using hull().  I
have found this useful but it would be more useful if a similar method
could be applied between parent and child objects using BOSL2's
positioning/alignment/attachment but I don't yet see a good way and would
love it if someone could help me do it.

module centered_scale(center_point, scale_vector) {
translate(center_point)
scale(scale_vector)
translate(-1*center_point)
children(0);
}

// chamfered joining of objects
// unions objects with a hull of those objects limited to the
intersection of the objects after scaling
// adjust object scale factors to change the size and angle of the
chamfer
module hulled_join(centered_scales) {
union() {
children();
hull() intersection() {
intersection_for(i=[0:1:$children-1])
centered_scale(centered_scales[i][0],
centered_scales[i][1])
children(i);
union()
children();
}
}
}

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

//  EXAMPLE 1
hulled_join([
[pos1, (1+1.5/size1)[1,1,1]],
[pos2, (1+1.0/size2)
[1,1,1]]
])
{


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org

http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient
Virus-free.www.avg.com
http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient
<#m_5948437202312854252_m_-3373310449440574214_m_-1384773676116772327_DAB4FAD8-2DD7-40BB-A1B8-4E2AA1F9FDF2>


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org


OpenSCAD mailing list
To unsubscribe send an email to discuss-leave@lists.openscad.org

>if one wants to define limit parameters in a pairwise (or subset wise) fashion, syntax is his friend: He can use a nested call pattern and gain maximal control Nesting calls is semantically different and increases computation. >So, the lack of control argument doesn't hold. … finding the right limit parameters can be tricky. When there was a scaling vector associated with each child, if say the child was a cylinder one could make a join that would extend 1 mm from the radius and 2 mm from the ends. Or if it was a cube of non uniform dimensions one could make the join extend uniformly in width, depth and height. You named your scalars X & Y despite them having nothing to do with a coordinate system nor even being orthogonal controls and made it tricky to find the right limits. >I think the main limitation of the approach is "concavity" Which the name hulled_join() conveyed. It’s a bigger problem when you name the operation fillet() which also confuses that the operation is useful for chamfers. On Fri, Dec 12, 2025 at 7:41 PM Rudolf via Discuss < discuss@lists.openscad.org> wrote: > Todd, > the revision I made fits well into my libraries in which I redefine the > OpenSCAD primitives (and a lot more primitives) anyway. > > Fillet() offers only two limit parameters which it applies to all operand > pairings given. That's right. But if one wants to define limit parameters > in a pairwise (or subset wise) fashion, syntax is his friend: He can use a > nested call pattern and gain maximal control: > > Fillet(x0, y0, iter0) > { > elem0(); > Fillet(x1, y1, iter1) > { > elem1(); > elem2(); > } > } > > So, the lack of control argument doesn't hold. I think the main limitation > of the approach is "concavity". However it definitely provides a valuable > solution for a wide range of situations, even scaling a primitive and > finding the right limit parameters can be tricky. What I love is that the > approach handles overlapping fillets like a charm. I already used it twice > for prints I wouldn't have added fillets today. > > > Am 12.12.2025 um 22:43 schrieb Todd Allen via Discuss: > > In the previous code I posted for hulled_join() the BOSL2 example would > union an additional unneeded copy of children(). Here's a fix, though one > will then need to pass children=true for non BOSL2 attachables cases. > > module hulled_join(iterations=1, all=false, children=false) { > union() { > if (children) children(); > for(si=[0:1:iterations-1]) { > for(i=[0:1:all?$children-2:0]) for(j=[i+1:1:$children-1]) > hull() { > intersection() { > children(i, $hj_scale=0); > children(j, $hj_scale=(si+1)/iterations); > } > intersection() { > children(i, $hj_scale=(iterations-si)/iterations); > children(j, $hj_scale=0); > } > } > } > } > } > > On Fri, Dec 12, 2025 at 12:16 PM Todd Allen <speedebikes@gmail.com> wrote: > >> Rudolf, big thanks for your interest in this technique and your offered >> changes! I hope your enthusiasm is contagious and there will be more >> contributions. >> >> Your revision of the hull() and intersection() targets brings some >> clarity and I think efficiency to the method. I like your idea to use a $ >> var to enable scaled children which can eliminate my passing of scaling >> factors as parameters to hulled_join() for a cleaner interface. I prefer >> scaling in a single separate module instead of adding it to cube(), >> sphere(), etc. as hulled_join() can be applied to children formed in any >> fashion including polyhedron(), linear_extrude(), rotate_extrude(), etc. >> I think you lost independent scaling factors for each child which gave >> control of the angle of the join. I liked your addition of rounding by >> iterative proportional adjustment of scaling but reworked it to retain >> independent scale factors for each child. I also retained my “all” >> parameter enabling the specification of forming joins between all children >> or just between the first child and the other children. I adapted this new >> approach of scaling children of hulled_join() to work with BOSL2 >> attachables, improving that interface too. >> [image: hjtest2.png] >> include <BOSL2/std.scad> >> $fn = 128; >> >> // OpenSCAD hulled_join() examples >> size1 = 20; >> size2 = 10; >> pos1 = [0,0,0]; >> pos2 = [size1*0.5+size2*0.3,0,0]; >> >> // round joints between sphere and other children >> translate([0,1.5*size1,0]) >> hulled_join(iterations=10) { >> hj_scale(pos1, (1+2.0/size1)*[1,1,1]) >> translate(pos1) sphere(d=size1); >> hj_scale(pos2, (1+1.0/size2)*[1,1,1]) >> translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) >> cylinder(h=size1, d=size2, center=true); >> hj_scale(pos2, (1+1.0/size2)*[1,1,1]) >> translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) >> cylinder(h=size1, d=size2, center=true); >> } >> >> // chamfer all the joints >> hulled_join(all=true) { >> hj_scale(pos1, (1+2.0/size1)*[1,1,1]) >> translate(pos1) sphere(d=size1); >> hj_scale(pos2, (1+1.0/size2)*[1,1,1]) >> translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) >> cylinder(h=size1, d=size2, center=true); >> hj_scale(pos2, (1+1.0/size2)*[1,1,1]) >> translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) >> cylinder(h=size1, d=size2, center=true); >> } >> >> // BOSL2 hulled_join() example of: >> // >> https://github.com/BelfrySCAD/BOSL2/wiki/attachments.scad#function-desc_dir >> right(40) back(30) >> prismoid(20,10,h=15) >> attach(RIGHT,BOT) cuboid([4,4,15]) >> position(TOP) cyl(d=12,h=5,orient=desc_dir(),anchor=BACK); >> >> // with hulled_join() joints >> module body(ao=false) {prismoid(20,10,h=15, anchor=ao?ao:BOT) children();} >> module arm(ao=false) {cuboid([4,4,20], anchor=ao?ao:CTR) children();} >> module hand(ao=false) {cyl(d=12,h=5,orient=desc_dir(), anchor=ao?ao:BACK) >> children();} >> right(40) >> body() let(body_desc=parent()) >> attach(RIGHT,BOT) arm() let(arm_desc=parent()) >> position(TOP) hand() let(hand_desc=parent()) >> restore() hulled_join(3) { >> // arm first to allow default case of only computing chamfers between >> first child and the other children >> hja_scale(1.6*[1,1,1], arm_desc) arm(CTR); >> hja_scale(1.2*[1,1,1], body_desc) body(CTR); >> hja_scale(1.2*[1,1,1], hand_desc) hand(CTR); >> }; >> >> >> // >> // this code could be in a library >> // >> $hj_scale=0; >> >> // to control scaling a child of hulled_join() >> module hj_scale(center_point, scale_vector) { >> if ($hj_scale) { >> sv = [ >> 1+$hj_scale*(scale_vector.x-1), >> 1+$hj_scale*(scale_vector.y-1), >> 1+$hj_scale*(scale_vector.z-1) >> ]; >> // optimizable to a single translation? >> translate(center_point) scale(sv) translate(-1*center_point) >> children(); >> } >> else children(); >> } >> >> // to control scaling a BOSL2 attachable child of hulled_join() >> module hja_scale(scale_vector, desc) { >> if ($hj_scale) { >> sv = [ >> 1+$hj_scale*(scale_vector.x-1), >> 1+$hj_scale*(scale_vector.y-1), >> 1+$hj_scale*(scale_vector.z-1) >> ]; >> restore(transform_desc(scale(sv, cp=desc_point(desc,anchor=CTR)), >> desc)) children(); >> } >> else restore(desc) children(); >> } >> >> // hulled joining of children objects >> // unions objects with a hull of those objects limited to intersections >> of the objects with scaled partners >> // adjust scale_vector of hj_scale() or hja_scale() in each child to >> change the size and angle of joints >> // all=true, joints between all children are chamfered >> // all=false, only the joints between the 1st child and the other >> children are chamfered >> module hulled_join(iterations=1, all=false) { >> union() { >> children(); >> for(si=[0:1:iterations-1]) { >> for(i=[0:1:all?$children-2:0]) for(j=[i+1:1:$children-1]) >> hull() { >> intersection() { >> children(i, $hj_scale=0); >> children(j, $hj_scale=(si+1)/iterations); >> } >> intersection() { >> children(i, $hj_scale=(iterations-si)/iterations); >> children(j, $hj_scale=0); >> } >> } >> } >> } >> } >> >> On Fri, Dec 12, 2025 at 5:11 AM Rudolf via Discuss < >> discuss@lists.openscad.org> wrote: >> >>> The "trick" is definitely a must-have. However, from this point the >>> fillet() operator just takes a snap of the fingers. >>> >>> You tell the Fillet() operator the # iterations and the limits ... and >>> you get this: I am thrilled! >>> >>> >>> $fn=100; >>> $camfer=1; >>> >>> size1 = 10; >>> size2 = 5; >>> pos1 = [0,0,0]; >>> pos2 = [size1*0.5+size2*0.3,0,0]; >>> >>> module Fillet(x=1.5, y=1.5, iterations=10) >>> { >>> for(i=[0:$children-1]) children(i); >>> >>> for(k=[0:iterations-1]) // fillet iteration >>> { >>> x_ = 1+ k*(x-1)/iterations; >>> y_ = y - k*(y-1)/iterations; >>> >>> for(i=[0:$children-1], j=[0:$children-1]) // Chamfer >>> if(i<j) >>> hull() >>> { >>> intersection() >>> { >>> children(i); >>> children(j, $camfer=x_); >>> } >>> intersection() >>> { >>> children(i, $camfer=y_); >>> children(j); >>> } >>> } >>> } >>> } >>> >>> module cube_(size, center) scale($camfer)cube(size, center); >>> module sphere_(r, d) scale($camfer)sphere(r=r, d=d); >>> module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, d=d, >>> center=center); >>> >>> Fillet() >>> { >>> translate([-15, 0, 0])translate(pos1) cube_(10, center=true); >>> translate([-15, 0, 0])translate(pos2) rotate(a=[20,20,0]) cube_([10, >>> 5, 5], center=true); >>> translate(pos1) sphere_(d=size1); >>> translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) >>> cylinder_(h=size1, d=size2, center=false); >>> translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) >>> cylinder_(h=size1, d=size2, center=true); >>> } >>> >>> Am 12.12.2025 um 01:42 schrieb Jon Bondy: >>> >>> Thank you! I tossed that into my bag of tricks! >>> >>> >>> On 12/11/2025 7:00 PM, Rudolf via Discuss wrote: >>> >>> I like the idea behind your approach and kind of rewrote it into a more >>> generic solution by introducing a global variable $camfer and >>> camfer-enabled modules cube_(), cylinder_() and sphere_() that allow to be >>> scaled by $camfer. >>> >>> $fn=100; >>> $camfer=1; >>> >>> size1 = 10; >>> size2 = 5; >>> pos1 = [0,0,0]; >>> pos2 = [size1*0.5+size2*0.3,0,0]; >>> >>> >>> module camfer(x=1.2) >>> { >>> for(i=[0:$children-1]) >>> children(i); >>> >>> for(i=[0:$children-1], j=[0:$children-1]) >>> if(i<j) >>> hull() >>> { >>> intersection() >>> { >>> children(i); >>> children(j, $camfer=x); >>> } >>> intersection() >>> { >>> children(i, $camfer=x); >>> children(j); >>> } >>> } >>> } >>> module cube_(size, center) scale($camfer)cube(size, center); >>> module sphere_(r, d) scale($camfer)sphere(r=r, d=d); >>> module cylinder_(h, r, d, center) scale($camfer)cylinder(h=h, r=r, d=d, >>> center=center); >>> >>> >>> camfer() >>> { >>> translate(pos1) cube_(10, center=true); >>> translate(pos2) rotate(a=[20,20,0]) cube_([6, 5, 5], center=true); >>> } >>> >>> translate([20, 0, 0]) >>> camfer() >>> { >>> translate(pos1) sphere_(d=size1); >>> translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) >>> cylinder_(h=size1, d=size2, center=true); >>> translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) >>> cylinder_(h=size1, d=size2, center=true); >>> } >>> >>> >>> >>> >>> Am 08.12.2025 um 00:03 schrieb Todd Allen via Discuss: >>> >>> module centered_scale(center_point, scale_vector) { >>> translate(center_point) >>> scale(scale_vector) >>> translate(-1*center_point) >>> children(0); >>> } >>> >>> // chamfered joining of objects >>> // unions objects with a hull of those objects limited to the >>> intersection of the objects after scaling >>> // adjust object scale factors to change the size and angle of chamfers >>> // all=true then joints between all children are chamfered >>> // all=false only the joints between the 1st child and the other >>> children are chamfered >>> module hulled_join(centered_scales, all=false) { >>> union() { >>> children(); >>> for(i=[0:1:all?$children-2:0]) >>> for(j=[i+1:1:$children-1]) >>> hull() intersection() { >>> centered_scale(centered_scales[i][0], >>> centered_scales[i][1]) children(i); >>> centered_scale(centered_scales[j][0], >>> centered_scales[j][1]) children(j); >>> union() { >>> children(i); >>> children(j); >>> } >>> } >>> } >>> } >>> >>> size1 = 10; >>> size2 = 5; >>> pos1 = [0,0,0]; >>> pos2 = [size1*0.5+size2*0.3,0,0]; >>> >>> // EXAMPLE 1 >>> translate([-size1,0,0]) >>> hulled_join([ >>> [pos1, (1+2.0/size1)*[1,1,1]], >>> [pos2, (1+1.0/size2)*[1,1,1]], >>> [pos2, (1+1.0/size2)*[1,1,1]] >>> ], $fn=128) >>> { >>> translate(pos1) >>> sphere(d=size1); >>> translate(pos2) >>> rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2, >>> center=true); >>> translate(pos2) >>> rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2, >>> center=true); >>> } >>> >>> // EXAMPLE 2 >>> translate([size1,0,0]) >>> hulled_join([ >>> [pos1, (1+1.0/size1)*[1,1,1]], >>> [pos2, (1+2.0/size2)*[1,1,1]], >>> [pos2, (1+2.0/size2)*[1,1,1]] >>> ], all=true, $fn=128) >>> { >>> translate(pos1) >>> sphere(d=size1); >>> translate(pos2) >>> rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2, >>> center=true); >>> translate(pos2) >>> rotate(a=[90,20,90]) scale([0.5,1,1]) cylinder(h=size1, d=size2, >>> center=true); >>> } >>> >>> [image: hulljoin.png] >>> >>> >>> On Sat, Dec 6, 2025 at 4:23 PM Todd Allen <speedebikes@gmail.com> wrote: >>> >>>> Here's an example of how I sometimes chamfer joints using hull(). I >>>> have found this useful but it would be more useful if a similar method >>>> could be applied between parent and child objects using BOSL2's >>>> positioning/alignment/attachment but I don't yet see a good way and would >>>> love it if someone could help me do it. >>>> >>>> module centered_scale(center_point, scale_vector) { >>>> translate(center_point) >>>> scale(scale_vector) >>>> translate(-1*center_point) >>>> children(0); >>>> } >>>> >>>> // chamfered joining of objects >>>> // unions objects with a hull of those objects limited to the >>>> intersection of the objects after scaling >>>> // adjust object scale factors to change the size and angle of the >>>> chamfer >>>> module hulled_join(centered_scales) { >>>> union() { >>>> children(); >>>> hull() intersection() { >>>> intersection_for(i=[0:1:$children-1]) >>>> centered_scale(centered_scales[i][0], >>>> centered_scales[i][1]) >>>> children(i); >>>> union() >>>> children(); >>>> } >>>> } >>>> } >>>> >>>> size1 = 10; >>>> size2 = 5; >>>> pos1 = [0,0,0]; >>>> pos2 = [size1*0.5+size2*0.3,0,0]; >>>> >>>> // EXAMPLE 1 >>>> hulled_join([ >>>> [pos1, (1+1.5/size1)*[1,1,1]], >>>> [pos2, (1+1.0/size2)*[1,1,1]] >>>> ]) >>>> { >>>> >>> >>> _______________________________________________ >>> OpenSCAD mailing list >>> To unsubscribe send an email to discuss-leave@lists.openscad.org >>> >>> >>> >>> <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient> >>> Virus-free.www.avg.com >>> <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient> >>> <#m_5948437202312854252_m_-3373310449440574214_m_-1384773676116772327_DAB4FAD8-2DD7-40BB-A1B8-4E2AA1F9FDF2> >>> >>> _______________________________________________ >>> OpenSCAD mailing list >>> To unsubscribe send an email to discuss-leave@lists.openscad.org >> >> > _______________________________________________ > OpenSCAD mailing list > To unsubscribe send an email to discuss-leave@lists.openscad.org > > _______________________________________________ > OpenSCAD mailing list > To unsubscribe send an email to discuss-leave@lists.openscad.org
R
Rudolf
Sat, Dec 13, 2025 11:52 PM

Am 13.12.2025 um 17:01 schrieb Todd Allen:

Nesting calls is semantically different and increases computation.

I am not convinced of that.

So, the lack of control argument doesn't hold. … finding the right

limit parameters can be tricky.
When there was a scaling vector associated with each child, if say the
child was a cylinder one could make a join that would extend 1 mm from
the radius and 2 mm from the ends.  Or if it was a cube of non uniform
dimensions one could make the join extend uniformly in width, depth
and height.  You named your scalars X & Y despite them having nothing
to do with a coordinate system nor even being orthogonal controls and
made it tricky to find the right limits.

I admit, my naming is not the best, but, hey,  I posted my unpolished
test code. In production code I usually write something like this:

module Fillet(scale=[1.5, 1.5], iterations=30)

{
  scale = is_list(scale)?scale:[scale, scale];
  assert(iterations > 0 && iterations<=30, "enforcing 0 < iterations
<=30");
  ...
  for(k=[0:iterations])                  // fillet iteration

I think the main limitation of the approach is "concavity"

Which the name hulled_join() conveyed.   It’s a bigger problem when
you name the operation fillet() which also confuses that the operation
is useful for chamfers.

(Very) Well-behaved concave objects do work, as the following code shows
(right object). And: a chamfer is just a special case of a fillet.
iterations=1 produces a chamfer, iterations=10 or so a fillet (left
object).

$fn=100;
$chamfer=1;

module Fillet(scale=[1.5, 1.5], iterations=30)
{
  scale = is_list(scale)?scale:[scale, scale];
  assert(iterations > 0 && iterations<=30, "enforcing 0 < iterations
<=30");

  for(i=[0:$children-1]) children(i);      // all children
  for(k=[0:iterations])                  // fillet iteration
  {
    x_ = 1+k*(scale[0]-1)/(iterations+1);
    y_ = scale[1]-k*(scale[1]-1)/(iterations+1);

    for(i=[0:$children-1], j=[0:$children-1]) // chamfers
    if(i<j) hull()
    {
      intersection() {children(i); children(j, $chamfer=x_); }
      intersection() {children(i, $chamfer=y_); children(j); }
    }
  }
}

module cube_(size, center) scale($chamfer)cube(size, center);
module sphere_(r, d) scale($chamfer)sphere(r=r, d=d);
module cylinder_(h, r, d, center) scale($chamfer)cylinder(h=h, r=r, d=d,
center=center);

translate([0, -14, 0])Fillet([1.5, 1.2], 1)  // nested call
{
  Fillet([1.5, 1.2], 10)
  {
    sphere_(d = 10);
    translate([3, 0, 0])cylinder_(d = 3, h=10, true);
  }
  translate([3, 0, 0])rotate([90, 0, 0]) cylinder_(d = 3, h=10, true);
}

Fillet([2, 4], 2) // concave shape, very well-behaved case.
{
  concave_shape_();  // concave shape must be first child
  translate([0, 0, 2])rotate([0,90,0])cylinder_(r=1.5, h=10,
center=false);
  translate([0, 0, -3])rotate([0,90,0])cylinder_(r=1.5, h=10,
center=false);
}

module concave_shape_()
scale($chamfer)
{
  difference()
  {
    cube([6, 12, 12], center=true);
    translate([10, 0, 0])sphere(9);
  }
}

On Fri, Dec 12, 2025 at 7:41 PM Rudolf via Discuss
discuss@lists.openscad.org wrote:

 Todd,
 the revision I made fits well into my libraries in which I
 redefine the OpenSCAD primitives (and a lot more primitives) anyway.

 Fillet() offers only two limit parameters which it applies to all
 operand pairings given. That's right. But if one wants to define
 limit parameters in a pairwise (or subset wise) fashion, syntax is
 his friend: He can use a nested call pattern and gain maximal
 control:

 Fillet(x0, y0, iter0)
 {
   elem0();
   Fillet(x1, y1, iter1)
   {
     elem1();
     elem2();
   }
 }

 So, the lack of control argument doesn't hold. I think the main
 limitation of the approach is "concavity". However it definitely
 provides a valuable solution for a wide range of situations, even
 scaling a primitive and finding the right limit parameters can be
 tricky. What I love is that the approach handles overlapping
 fillets like a charm. I already used it twice for prints I
 wouldn't have added fillets today.

 Am 12.12.2025 um 22:43 schrieb Todd Allen via Discuss:
 In the previous code I posted for hulled_join() the BOSL2 example
 would union an additional unneeded copy of children().  Here's a
 fix, though one will then need to pass children=true for non
 BOSL2 attachables cases.

 module hulled_join(iterations=1, all=false, children=false) {
     union() {
         if (children) children();
         for(si=[0:1:iterations-1]) {
             for(i=[0:1:all?$children-2:0])
 for(j=[i+1:1:$children-1]) hull() {
                 intersection() {
                     children(i, $hj_scale=0);
                     children(j, $hj_scale=(si+1)/iterations);
                 }
                 intersection() {
                     children(i,
 $hj_scale=(iterations-si)/iterations);
                     children(j, $hj_scale=0);
                 }
             }
         }
     }
 }

 On Fri, Dec 12, 2025 at 12:16 PM Todd Allen
 <speedebikes@gmail.com> wrote:

     Rudolf, big thanks for your interest in this technique and
     your offered changes!  I hope your enthusiasm is contagious
     and there will be more contributions.

     Your revision of the hull() and intersection() targets brings
     some clarity and I think efficiency to the method.  I like
     your idea to use a $ var to enable scaled children which can
     eliminate my passing of scaling factors as parameters to
     hulled_join() for a cleaner interface.  I prefer scaling in a
     single separate module instead of adding it to cube(),
     sphere(), etc. as hulled_join() can be applied to children
     formed in any fashion including polyhedron(),
     linear_extrude(),  rotate_extrude(), etc.   I think you lost
     independent scaling factors for each child which gave control
     of the angle of the join.  I liked your addition of rounding
     by iterative proportional adjustment of scaling but reworked
     it to retain independent scale factors for each child.  I
     also retained my “all” parameter enabling the specification
     of forming joins between all children or just between the
     first child and the other children.  I adapted this new
     approach of scaling children of hulled_join() to work with
     BOSL2 attachables, improving that interface too.
     hjtest2.png
     include <BOSL2/std.scad>
     $fn = 128;

     // OpenSCAD hulled_join() examples
     size1 = 20;
     size2 = 10;
     pos1 = [0,0,0];
     pos2 = [size1*0.5+size2*0.3,0,0];

     // round joints between sphere and other children
     translate([0,1.5*size1,0])
     hulled_join(iterations=10) {
         hj_scale(pos1, (1+2.0/size1)*[1,1,1])
             translate(pos1) sphere(d=size1);
         hj_scale(pos2, (1+1.0/size2)*[1,1,1])
             translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
     cylinder(h=size1, d=size2, center=true);
         hj_scale(pos2, (1+1.0/size2)*[1,1,1])
             translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
     cylinder(h=size1, d=size2, center=true);
     }

     // chamfer all the joints
     hulled_join(all=true) {
         hj_scale(pos1, (1+2.0/size1)*[1,1,1])
             translate(pos1) sphere(d=size1);
         hj_scale(pos2, (1+1.0/size2)*[1,1,1])
             translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
     cylinder(h=size1, d=size2, center=true);
         hj_scale(pos2, (1+1.0/size2)*[1,1,1])
             translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
     cylinder(h=size1, d=size2, center=true);
     }

     // BOSL2 hulled_join() example of:
     //
     https://github.com/BelfrySCAD/BOSL2/wiki/attachments.scad#function-desc_dir
     right(40) back(30)
     prismoid(20,10,h=15)
     attach(RIGHT,BOT) cuboid([4,4,15])
     position(TOP) cyl(d=12,h=5,orient=desc_dir(),anchor=BACK);

     // with hulled_join() joints
     module body(ao=false) {prismoid(20,10,h=15, anchor=ao?ao:BOT)
     children();}
     module arm(ao=false) {cuboid([4,4,20], anchor=ao?ao:CTR)
     children();}
     module hand(ao=false) {cyl(d=12,h=5,orient=desc_dir(),
     anchor=ao?ao:BACK) children();}
     right(40)
     body() let(body_desc=parent())
     attach(RIGHT,BOT) arm() let(arm_desc=parent())
     position(TOP) hand() let(hand_desc=parent())
     restore() hulled_join(3) {
         // arm first to allow default case of only computing
     chamfers between first child and the other children
         hja_scale(1.6*[1,1,1], arm_desc) arm(CTR);
         hja_scale(1.2*[1,1,1], body_desc) body(CTR);
         hja_scale(1.2*[1,1,1], hand_desc) hand(CTR);
     };


     //
     // this code could be in a library
     //
     $hj_scale=0;

     // to control scaling a child of hulled_join()
     module hj_scale(center_point, scale_vector) {
         if ($hj_scale) {
             sv = [
                 1+$hj_scale*(scale_vector.x-1),
                 1+$hj_scale*(scale_vector.y-1),
                 1+$hj_scale*(scale_vector.z-1)
             ];
             // optimizable to a single translation?
             translate(center_point) scale(sv)
     translate(-1*center_point) children();
         }
         else children();
     }

     // to control scaling a BOSL2 attachable child of hulled_join()
     module hja_scale(scale_vector, desc) {
         if ($hj_scale) {
             sv = [
                 1+$hj_scale*(scale_vector.x-1),
                 1+$hj_scale*(scale_vector.y-1),
                 1+$hj_scale*(scale_vector.z-1)
             ];
             restore(transform_desc(scale(sv,
     cp=desc_point(desc,anchor=CTR)), desc)) children();
         }
         else restore(desc) children();
     }

     // hulled joining of children objects
     // unions objects with a hull of those objects limited to
     intersections of the objects with scaled partners
     // adjust scale_vector of hj_scale() or hja_scale() in each
     child to change the size and angle of joints
     // all=true, joints between all children are chamfered
     // all=false, only the joints between the 1st child and the
     other children are chamfered
     module hulled_join(iterations=1, all=false) {
         union() {
             children();
             for(si=[0:1:iterations-1]) {
                 for(i=[0:1:all?$children-2:0])
     for(j=[i+1:1:$children-1]) hull() {
                     intersection() {
                         children(i, $hj_scale=0);
                         children(j, $hj_scale=(si+1)/iterations);
                     }
                     intersection() {
                         children(i,
     $hj_scale=(iterations-si)/iterations);
                         children(j, $hj_scale=0);
                     }
                 }
             }
         }
     }

     On Fri, Dec 12, 2025 at 5:11 AM Rudolf via Discuss
     <discuss@lists.openscad.org> wrote:

         The "trick" is definitely a must-have. However, from this
         point the fillet() operator just takes a snap of the
         fingers.

         You tell the Fillet() operator the # iterations and the
         limits ... and you get this: I am thrilled!


         $fn=100;
         $camfer=1;

         size1 = 10;
         size2 = 5;
         pos1 = [0,0,0];
         pos2 = [size1*0.5+size2*0.3,0,0];

         module Fillet(x=1.5, y=1.5, iterations=10)
         {
           for(i=[0:$children-1]) children(i);

           for(k=[0:iterations-1]) // fillet iteration
           {
             x_ = 1+ k*(x-1)/iterations;
             y_ = y - k*(y-1)/iterations;

         for(i=[0:$children-1], j=[0:$children-1]) // Chamfer
             if(i<j)
             hull()
             {
               intersection()
               {
                 children(i);
                 children(j, $camfer=x_);
               }
               intersection()
               {
                 children(i, $camfer=y_);
                 children(j);
               }
             }
           }
         }

         module cube_(size, center) scale($camfer)cube(size, center);
         module sphere_(r, d) scale($camfer)sphere(r=r, d=d);
         module cylinder_(h, r, d, center)
         scale($camfer)cylinder(h=h, r=r, d=d, center=center);

         Fillet()
           {
            translate([-15, 0, 0])translate(pos1) cube_(10,
         center=true);
            translate([-15, 0, 0])translate(pos2)
         rotate(a=[20,20,0]) cube_([10, 5, 5], center=true);
            translate(pos1) sphere_(d=size1);
            translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1])
         cylinder_(h=size1, d=size2, center=false);
            translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1])
         cylinder_(h=size1, d=size2, center=true);
           }

         Am 12.12.2025 um 01:42 schrieb Jon Bondy:
         Thank you!  I tossed that into my bag of tricks!


         On 12/11/2025 7:00 PM, Rudolf via Discuss wrote:
         I like the idea behind your approach and kind of
         rewrote it into a more generic solution  by introducing
         a global variable $camfer and camfer-enabled modules
         cube_(), cylinder_() and sphere_() that allow to be
         scaled by $camfer.

         $fn=100;
         $camfer=1;

         size1 = 10;
         size2 = 5;
         pos1 = [0,0,0];
         pos2 = [size1*0.5+size2*0.3,0,0];


         module camfer(x=1.2)
         {
           for(i=[0:$children-1])
           children(i);

           for(i=[0:$children-1], j=[0:$children-1])
           if(i<j)
           hull()
           {
             intersection()
             {
               children(i);
               children(j, $camfer=x);
             }
             intersection()
             {
               children(i, $camfer=x);
               children(j);
             }
           }
         }
         module cube_(size, center) scale($camfer)cube(size,
         center);
         module sphere_(r, d) scale($camfer)sphere(r=r, d=d);
         module cylinder_(h, r, d, center)
         scale($camfer)cylinder(h=h, r=r, d=d, center=center);


         camfer()
         {
           translate(pos1) cube_(10, center=true);
           translate(pos2) rotate(a=[20,20,0]) cube_([6, 5, 5],
         center=true);
         }

         translate([20, 0, 0])
           camfer()
           {
             translate(pos1) sphere_(d=size1);
             translate(pos2) rotate(a=[90,20,0])
         scale([1,0.5,1]) cylinder_(h=size1, d=size2, center=true);
             translate(pos2) rotate(a=[90,20,90])
         scale([0.5,1,1]) cylinder_(h=size1, d=size2, center=true);
           }




         Am 08.12.2025 um 00:03 schrieb Todd Allen via Discuss:
         module centered_scale(center_point, scale_vector) {
             translate(center_point)
                 scale(scale_vector)
                     translate(-1*center_point)
                         children(0);
         }

         // chamfered joining of objects
         // unions objects with a hull of those objects limited
         to the intersection of the objects after scaling
         // adjust object scale factors to change the size and
         angle of chamfers
         // all=true then joints between all children are chamfered
         // all=false only the joints between the 1st child and
         the other children are chamfered
         module hulled_join(centered_scales, all=false) {
             union() {
                 children();
                 for(i=[0:1:all?$children-2:0])
                     for(j=[i+1:1:$children-1])
                         hull() intersection() {
         centered_scale(centered_scales[i][0],
         centered_scales[i][1]) children(i);
         centered_scale(centered_scales[j][0],
         centered_scales[j][1]) children(j);
                             union() {
                                 children(i);
                                 children(j);
                             }
                         }
             }
         }

         size1 = 10;
         size2 = 5;
         pos1 = [0,0,0];
         pos2 = [size1*0.5+size2*0.3,0,0];

         //  EXAMPLE 1
         translate([-size1,0,0])
         hulled_join([
             [pos1, (1+2.0/size1)*[1,1,1]],
             [pos2, (1+1.0/size2)*[1,1,1]],
             [pos2, (1+1.0/size2)*[1,1,1]]
         ], $fn=128)
         {
             translate(pos1)
                 sphere(d=size1);
             translate(pos2)
                 rotate(a=[90,20,0]) scale([1,0.5,1])
         cylinder(h=size1, d=size2, center=true);
             translate(pos2)
                 rotate(a=[90,20,90]) scale([0.5,1,1])
         cylinder(h=size1, d=size2, center=true);
         }

         //  EXAMPLE 2
         translate([size1,0,0])
         hulled_join([
             [pos1, (1+1.0/size1)*[1,1,1]],
             [pos2, (1+2.0/size2)*[1,1,1]],
             [pos2, (1+2.0/size2)*[1,1,1]]
         ], all=true, $fn=128)
         {
             translate(pos1)
             sphere(d=size1);
             translate(pos2)
             rotate(a=[90,20,0]) scale([1,0.5,1])
         cylinder(h=size1, d=size2, center=true);
             translate(pos2)
             rotate(a=[90,20,90]) scale([0.5,1,1])
         cylinder(h=size1, d=size2, center=true);
         }

         hulljoin.png

         On Sat, Dec 6, 2025 at 4:23 PM Todd Allen
         <speedebikes@gmail.com> wrote:

             Here's an example of how I sometimes
             chamfer joints using hull().  I have found this
             useful but it would be more useful if a similar
             method could be applied between parent and child
             objects using BOSL2's
             positioning/alignment/attachment but I don't yet
             see a good way and would love it if someone could
             help me do it.

             module centered_scale(center_point, scale_vector) {
                 translate(center_point)
                     scale(scale_vector)
             translate(-1*center_point)
                             children(0);
             }

             // chamfered joining of objects
             // unions objects with a hull of those objects
             limited to the intersection of the objects after
             scaling
             // adjust object scale factors to change the size
             and angle of the chamfer
             module hulled_join(centered_scales) {
                 union() {
                     children();
                     hull() intersection() {
             intersection_for(i=[0:1:$children-1])
             centered_scale(centered_scales[i][0],
             centered_scales[i][1])
                                 children(i);
                         union()
                             children();
                     }
                 }
             }

             size1 = 10;
             size2 = 5;
             pos1 = [0,0,0];
             pos2 = [size1*0.5+size2*0.3,0,0];

             //  EXAMPLE 1
             hulled_join([
                 [pos1, (1+1.5/size1)*[1,1,1]],
                 [pos2, (1+1.0/size2)*[1,1,1]]
                 ])
             {
         _______________________________________________
         OpenSCAD mailing list
         To unsubscribe send an email todiscuss-leave@lists.openscad.org
         <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient>
         	Virus-free.www.avg.com
         <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient>


         <#m_5948437202312854252_m_-3373310449440574214_m_-1384773676116772327_DAB4FAD8-2DD7-40BB-A1B8-4E2AA1F9FDF2>
         _______________________________________________
         OpenSCAD mailing list
         To unsubscribe send an email to
         discuss-leave@lists.openscad.org


 _______________________________________________
 OpenSCAD mailing list
 To unsubscribe send an email todiscuss-leave@lists.openscad.org
 _______________________________________________
 OpenSCAD mailing list
 To unsubscribe send an email to discuss-leave@lists.openscad.org
Am 13.12.2025 um 17:01 schrieb Todd Allen: > Nesting calls is semantically different and increases computation. I am not convinced of that. > >So, the lack of control argument doesn't hold. … finding the right > limit parameters can be tricky. > When there was a scaling vector associated with each child, if say the > child was a cylinder one could make a join that would extend 1 mm from > the radius and 2 mm from the ends.  Or if it was a cube of non uniform > dimensions one could make the join extend uniformly in width, depth > and height.  You named your scalars X & Y despite them having nothing > to do with a coordinate system nor even being orthogonal controls and > made it tricky to find the right limits. I admit, my naming is not the best, but, hey,  I posted my unpolished test code. In production code I usually write something like this: module Fillet(scale=[1.5, 1.5], iterations=30) {   scale = is_list(scale)?scale:[scale, scale];   assert(iterations > 0 && iterations<=30, "enforcing 0 < iterations <=30");   ...   for(k=[0:iterations])                  // fillet iteration > > >I think the main limitation of the approach is "concavity" > Which the name hulled_join() conveyed.   It’s a bigger problem when > you name the operation fillet() which also confuses that the operation > is useful for chamfers. (Very) Well-behaved concave objects do work, as the following code shows (right object). And: a chamfer is just a special case of a fillet. iterations=1 produces a chamfer, iterations=10 or so a fillet (left object). $fn=100; $chamfer=1; module Fillet(scale=[1.5, 1.5], iterations=30) {   scale = is_list(scale)?scale:[scale, scale];   assert(iterations > 0 && iterations<=30, "enforcing 0 < iterations <=30");   for(i=[0:$children-1]) children(i);      // all children   for(k=[0:iterations])                  // fillet iteration   {     x_ = 1+k*(scale[0]-1)/(iterations+1);     y_ = scale[1]-k*(scale[1]-1)/(iterations+1);     for(i=[0:$children-1], j=[0:$children-1]) // chamfers     if(i<j) hull()     {       intersection() {children(i); children(j, $chamfer=x_); }       intersection() {children(i, $chamfer=y_); children(j); }     }   } } module cube_(size, center) scale($chamfer)cube(size, center); module sphere_(r, d) scale($chamfer)sphere(r=r, d=d); module cylinder_(h, r, d, center) scale($chamfer)cylinder(h=h, r=r, d=d, center=center); translate([0, -14, 0])Fillet([1.5, 1.2], 1)  // nested call {   Fillet([1.5, 1.2], 10)   {     sphere_(d = 10);     translate([3, 0, 0])cylinder_(d = 3, h=10, true);   }   translate([3, 0, 0])rotate([90, 0, 0]) cylinder_(d = 3, h=10, true); } Fillet([2, 4], 2) // concave shape, very well-behaved case. {   concave_shape_();  // concave shape must be first child   translate([0, 0, 2])rotate([0,90,0])cylinder_(r=1.5, h=10, center=false);   translate([0, 0, -3])rotate([0,90,0])cylinder_(r=1.5, h=10, center=false); } module concave_shape_() scale($chamfer) {   difference()   {     cube([6, 12, 12], center=true);     translate([10, 0, 0])sphere(9);   } } > > > > On Fri, Dec 12, 2025 at 7:41 PM Rudolf via Discuss > <discuss@lists.openscad.org> wrote: > > Todd, > the revision I made fits well into my libraries in which I > redefine the OpenSCAD primitives (and a lot more primitives) anyway. > > Fillet() offers only two limit parameters which it applies to all > operand pairings given. That's right. But if one wants to define > limit parameters in a pairwise (or subset wise) fashion, syntax is > his friend: He can use a nested call pattern and gain maximal > control: > > Fillet(x0, y0, iter0) > { >   elem0(); >   Fillet(x1, y1, iter1) >   { >     elem1(); >     elem2(); >   } > } > > So, the lack of control argument doesn't hold. I think the main > limitation of the approach is "concavity". However it definitely > provides a valuable solution for a wide range of situations, even > scaling a primitive and finding the right limit parameters can be > tricky. What I love is that the approach handles overlapping > fillets like a charm. I already used it twice for prints I > wouldn't have added fillets today. > > Am 12.12.2025 um 22:43 schrieb Todd Allen via Discuss: >> In the previous code I posted for hulled_join() the BOSL2 example >> would union an additional unneeded copy of children().  Here's a >> fix, though one will then need to pass children=true for non >> BOSL2 attachables cases. >> >> module hulled_join(iterations=1, all=false, children=false) { >>     union() { >>         if (children) children(); >>         for(si=[0:1:iterations-1]) { >>             for(i=[0:1:all?$children-2:0]) >> for(j=[i+1:1:$children-1]) hull() { >>                 intersection() { >>                     children(i, $hj_scale=0); >>                     children(j, $hj_scale=(si+1)/iterations); >>                 } >>                 intersection() { >>                     children(i, >> $hj_scale=(iterations-si)/iterations); >>                     children(j, $hj_scale=0); >>                 } >>             } >>         } >>     } >> } >> >> On Fri, Dec 12, 2025 at 12:16 PM Todd Allen >> <speedebikes@gmail.com> wrote: >> >> Rudolf, big thanks for your interest in this technique and >> your offered changes!  I hope your enthusiasm is contagious >> and there will be more contributions. >> >> Your revision of the hull() and intersection() targets brings >> some clarity and I think efficiency to the method.  I like >> your idea to use a $ var to enable scaled children which can >> eliminate my passing of scaling factors as parameters to >> hulled_join() for a cleaner interface.  I prefer scaling in a >> single separate module instead of adding it to cube(), >> sphere(), etc. as hulled_join() can be applied to children >> formed in any fashion including polyhedron(), >> linear_extrude(),  rotate_extrude(), etc.   I think you lost >> independent scaling factors for each child which gave control >> of the angle of the join.  I liked your addition of rounding >> by iterative proportional adjustment of scaling but reworked >> it to retain independent scale factors for each child.  I >> also retained my “all” parameter enabling the specification >> of forming joins between all children or just between the >> first child and the other children.  I adapted this new >> approach of scaling children of hulled_join() to work with >> BOSL2 attachables, improving that interface too. >> hjtest2.png >> include <BOSL2/std.scad> >> $fn = 128; >> >> // OpenSCAD hulled_join() examples >> size1 = 20; >> size2 = 10; >> pos1 = [0,0,0]; >> pos2 = [size1*0.5+size2*0.3,0,0]; >> >> // round joints between sphere and other children >> translate([0,1.5*size1,0]) >> hulled_join(iterations=10) { >>     hj_scale(pos1, (1+2.0/size1)*[1,1,1]) >>         translate(pos1) sphere(d=size1); >>     hj_scale(pos2, (1+1.0/size2)*[1,1,1]) >>         translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) >> cylinder(h=size1, d=size2, center=true); >>     hj_scale(pos2, (1+1.0/size2)*[1,1,1]) >>         translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) >> cylinder(h=size1, d=size2, center=true); >> } >> >> // chamfer all the joints >> hulled_join(all=true) { >>     hj_scale(pos1, (1+2.0/size1)*[1,1,1]) >>         translate(pos1) sphere(d=size1); >>     hj_scale(pos2, (1+1.0/size2)*[1,1,1]) >>         translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) >> cylinder(h=size1, d=size2, center=true); >>     hj_scale(pos2, (1+1.0/size2)*[1,1,1]) >>         translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) >> cylinder(h=size1, d=size2, center=true); >> } >> >> // BOSL2 hulled_join() example of: >> // >> https://github.com/BelfrySCAD/BOSL2/wiki/attachments.scad#function-desc_dir >> right(40) back(30) >> prismoid(20,10,h=15) >> attach(RIGHT,BOT) cuboid([4,4,15]) >> position(TOP) cyl(d=12,h=5,orient=desc_dir(),anchor=BACK); >> >> // with hulled_join() joints >> module body(ao=false) {prismoid(20,10,h=15, anchor=ao?ao:BOT) >> children();} >> module arm(ao=false) {cuboid([4,4,20], anchor=ao?ao:CTR) >> children();} >> module hand(ao=false) {cyl(d=12,h=5,orient=desc_dir(), >> anchor=ao?ao:BACK) children();} >> right(40) >> body() let(body_desc=parent()) >> attach(RIGHT,BOT) arm() let(arm_desc=parent()) >> position(TOP) hand() let(hand_desc=parent()) >> restore() hulled_join(3) { >>     // arm first to allow default case of only computing >> chamfers between first child and the other children >>     hja_scale(1.6*[1,1,1], arm_desc) arm(CTR); >>     hja_scale(1.2*[1,1,1], body_desc) body(CTR); >>     hja_scale(1.2*[1,1,1], hand_desc) hand(CTR); >> }; >> >> >> // >> // this code could be in a library >> // >> $hj_scale=0; >> >> // to control scaling a child of hulled_join() >> module hj_scale(center_point, scale_vector) { >>     if ($hj_scale) { >>         sv = [ >>             1+$hj_scale*(scale_vector.x-1), >>             1+$hj_scale*(scale_vector.y-1), >>             1+$hj_scale*(scale_vector.z-1) >>         ]; >>         // optimizable to a single translation? >>         translate(center_point) scale(sv) >> translate(-1*center_point) children(); >>     } >>     else children(); >> } >> >> // to control scaling a BOSL2 attachable child of hulled_join() >> module hja_scale(scale_vector, desc) { >>     if ($hj_scale) { >>         sv = [ >>             1+$hj_scale*(scale_vector.x-1), >>             1+$hj_scale*(scale_vector.y-1), >>             1+$hj_scale*(scale_vector.z-1) >>         ]; >>         restore(transform_desc(scale(sv, >> cp=desc_point(desc,anchor=CTR)), desc)) children(); >>     } >>     else restore(desc) children(); >> } >> >> // hulled joining of children objects >> // unions objects with a hull of those objects limited to >> intersections of the objects with scaled partners >> // adjust scale_vector of hj_scale() or hja_scale() in each >> child to change the size and angle of joints >> // all=true, joints between all children are chamfered >> // all=false, only the joints between the 1st child and the >> other children are chamfered >> module hulled_join(iterations=1, all=false) { >>     union() { >>         children(); >>         for(si=[0:1:iterations-1]) { >>             for(i=[0:1:all?$children-2:0]) >> for(j=[i+1:1:$children-1]) hull() { >>                 intersection() { >>                     children(i, $hj_scale=0); >>                     children(j, $hj_scale=(si+1)/iterations); >>                 } >>                 intersection() { >>                     children(i, >> $hj_scale=(iterations-si)/iterations); >>                     children(j, $hj_scale=0); >>                 } >>             } >>         } >>     } >> } >> >> On Fri, Dec 12, 2025 at 5:11 AM Rudolf via Discuss >> <discuss@lists.openscad.org> wrote: >> >> The "trick" is definitely a must-have. However, from this >> point the fillet() operator just takes a snap of the >> fingers. >> >> You tell the Fillet() operator the # iterations and the >> limits ... and you get this: I am thrilled! >> >> >> $fn=100; >> $camfer=1; >> >> size1 = 10; >> size2 = 5; >> pos1 = [0,0,0]; >> pos2 = [size1*0.5+size2*0.3,0,0]; >> >> module Fillet(x=1.5, y=1.5, iterations=10) >> { >>   for(i=[0:$children-1]) children(i); >> >>   for(k=[0:iterations-1]) // fillet iteration >>   { >>     x_ = 1+ k*(x-1)/iterations; >>     y_ = y - k*(y-1)/iterations; >> >> for(i=[0:$children-1], j=[0:$children-1]) // Chamfer >>     if(i<j) >>     hull() >>     { >>       intersection() >>       { >>         children(i); >>         children(j, $camfer=x_); >>       } >>       intersection() >>       { >>         children(i, $camfer=y_); >>         children(j); >>       } >>     } >>   } >> } >> >> module cube_(size, center) scale($camfer)cube(size, center); >> module sphere_(r, d) scale($camfer)sphere(r=r, d=d); >> module cylinder_(h, r, d, center) >> scale($camfer)cylinder(h=h, r=r, d=d, center=center); >> >> Fillet() >>   { >>    translate([-15, 0, 0])translate(pos1) cube_(10, >> center=true); >>    translate([-15, 0, 0])translate(pos2) >> rotate(a=[20,20,0]) cube_([10, 5, 5], center=true); >>    translate(pos1) sphere_(d=size1); >>    translate(pos2) rotate(a=[90,20,0]) scale([1,0.5,1]) >> cylinder_(h=size1, d=size2, center=false); >>    translate(pos2) rotate(a=[90,20,90]) scale([0.5,1,1]) >> cylinder_(h=size1, d=size2, center=true); >>   } >> >> Am 12.12.2025 um 01:42 schrieb Jon Bondy: >>> >>> Thank you!  I tossed that into my bag of tricks! >>> >>> >>> On 12/11/2025 7:00 PM, Rudolf via Discuss wrote: >>>> >>>> I like the idea behind your approach and kind of >>>> rewrote it into a more generic solution  by introducing >>>> a global variable $camfer and camfer-enabled modules >>>> cube_(), cylinder_() and sphere_() that allow to be >>>> scaled by $camfer. >>>> >>>> $fn=100; >>>> $camfer=1; >>>> >>>> size1 = 10; >>>> size2 = 5; >>>> pos1 = [0,0,0]; >>>> pos2 = [size1*0.5+size2*0.3,0,0]; >>>> >>>> >>>> module camfer(x=1.2) >>>> { >>>>   for(i=[0:$children-1]) >>>>   children(i); >>>> >>>>   for(i=[0:$children-1], j=[0:$children-1]) >>>>   if(i<j) >>>>   hull() >>>>   { >>>>     intersection() >>>>     { >>>>       children(i); >>>>       children(j, $camfer=x); >>>>     } >>>>     intersection() >>>>     { >>>>       children(i, $camfer=x); >>>>       children(j); >>>>     } >>>>   } >>>> } >>>> module cube_(size, center) scale($camfer)cube(size, >>>> center); >>>> module sphere_(r, d) scale($camfer)sphere(r=r, d=d); >>>> module cylinder_(h, r, d, center) >>>> scale($camfer)cylinder(h=h, r=r, d=d, center=center); >>>> >>>> >>>> camfer() >>>> { >>>>   translate(pos1) cube_(10, center=true); >>>>   translate(pos2) rotate(a=[20,20,0]) cube_([6, 5, 5], >>>> center=true); >>>> } >>>> >>>> translate([20, 0, 0]) >>>>   camfer() >>>>   { >>>>     translate(pos1) sphere_(d=size1); >>>>     translate(pos2) rotate(a=[90,20,0]) >>>> scale([1,0.5,1]) cylinder_(h=size1, d=size2, center=true); >>>>     translate(pos2) rotate(a=[90,20,90]) >>>> scale([0.5,1,1]) cylinder_(h=size1, d=size2, center=true); >>>>   } >>>> >>>> >>>> >>>> >>>> Am 08.12.2025 um 00:03 schrieb Todd Allen via Discuss: >>>>> module centered_scale(center_point, scale_vector) { >>>>>     translate(center_point) >>>>>         scale(scale_vector) >>>>>             translate(-1*center_point) >>>>>                 children(0); >>>>> } >>>>> >>>>> // chamfered joining of objects >>>>> // unions objects with a hull of those objects limited >>>>> to the intersection of the objects after scaling >>>>> // adjust object scale factors to change the size and >>>>> angle of chamfers >>>>> // all=true then joints between all children are chamfered >>>>> // all=false only the joints between the 1st child and >>>>> the other children are chamfered >>>>> module hulled_join(centered_scales, all=false) { >>>>>     union() { >>>>>         children(); >>>>>         for(i=[0:1:all?$children-2:0]) >>>>>             for(j=[i+1:1:$children-1]) >>>>>                 hull() intersection() { >>>>> centered_scale(centered_scales[i][0], >>>>> centered_scales[i][1]) children(i); >>>>> centered_scale(centered_scales[j][0], >>>>> centered_scales[j][1]) children(j); >>>>>                     union() { >>>>>                         children(i); >>>>>                         children(j); >>>>>                     } >>>>>                 } >>>>>     } >>>>> } >>>>> >>>>> size1 = 10; >>>>> size2 = 5; >>>>> pos1 = [0,0,0]; >>>>> pos2 = [size1*0.5+size2*0.3,0,0]; >>>>> >>>>> //  EXAMPLE 1 >>>>> translate([-size1,0,0]) >>>>> hulled_join([ >>>>>     [pos1, (1+2.0/size1)*[1,1,1]], >>>>>     [pos2, (1+1.0/size2)*[1,1,1]], >>>>>     [pos2, (1+1.0/size2)*[1,1,1]] >>>>> ], $fn=128) >>>>> { >>>>>     translate(pos1) >>>>>         sphere(d=size1); >>>>>     translate(pos2) >>>>>         rotate(a=[90,20,0]) scale([1,0.5,1]) >>>>> cylinder(h=size1, d=size2, center=true); >>>>>     translate(pos2) >>>>>         rotate(a=[90,20,90]) scale([0.5,1,1]) >>>>> cylinder(h=size1, d=size2, center=true); >>>>> } >>>>> >>>>> //  EXAMPLE 2 >>>>> translate([size1,0,0]) >>>>> hulled_join([ >>>>>     [pos1, (1+1.0/size1)*[1,1,1]], >>>>>     [pos2, (1+2.0/size2)*[1,1,1]], >>>>>     [pos2, (1+2.0/size2)*[1,1,1]] >>>>> ], all=true, $fn=128) >>>>> { >>>>>     translate(pos1) >>>>>     sphere(d=size1); >>>>>     translate(pos2) >>>>>     rotate(a=[90,20,0]) scale([1,0.5,1]) >>>>> cylinder(h=size1, d=size2, center=true); >>>>>     translate(pos2) >>>>>     rotate(a=[90,20,90]) scale([0.5,1,1]) >>>>> cylinder(h=size1, d=size2, center=true); >>>>> } >>>>> >>>>> hulljoin.png >>>>> >>>>> On Sat, Dec 6, 2025 at 4:23 PM Todd Allen >>>>> <speedebikes@gmail.com> wrote: >>>>> >>>>> Here's an example of how I sometimes >>>>> chamfer joints using hull().  I have found this >>>>> useful but it would be more useful if a similar >>>>> method could be applied between parent and child >>>>> objects using BOSL2's >>>>> positioning/alignment/attachment but I don't yet >>>>> see a good way and would love it if someone could >>>>> help me do it. >>>>> >>>>> module centered_scale(center_point, scale_vector) { >>>>>     translate(center_point) >>>>>         scale(scale_vector) >>>>> translate(-1*center_point) >>>>>                 children(0); >>>>> } >>>>> >>>>> // chamfered joining of objects >>>>> // unions objects with a hull of those objects >>>>> limited to the intersection of the objects after >>>>> scaling >>>>> // adjust object scale factors to change the size >>>>> and angle of the chamfer >>>>> module hulled_join(centered_scales) { >>>>>     union() { >>>>>         children(); >>>>>         hull() intersection() { >>>>> intersection_for(i=[0:1:$children-1]) >>>>> centered_scale(centered_scales[i][0], >>>>> centered_scales[i][1]) >>>>>                     children(i); >>>>>             union() >>>>>                 children(); >>>>>         } >>>>>     } >>>>> } >>>>> >>>>> size1 = 10; >>>>> size2 = 5; >>>>> pos1 = [0,0,0]; >>>>> pos2 = [size1*0.5+size2*0.3,0,0]; >>>>> >>>>> //  EXAMPLE 1 >>>>> hulled_join([ >>>>>     [pos1, (1+1.5/size1)*[1,1,1]], >>>>>     [pos2, (1+1.0/size2)*[1,1,1]] >>>>>     ]) >>>>> { >>>>> >>>> >>>> _______________________________________________ >>>> OpenSCAD mailing list >>>> To unsubscribe send an email todiscuss-leave@lists.openscad.org >>> >>> <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient> >>> Virus-free.www.avg.com >>> <http://www.avg.com/email-signature?utm_medium=email&utm_source=link&utm_campaign=sig-email&utm_content=emailclient> >>> >>> >>> <#m_5948437202312854252_m_-3373310449440574214_m_-1384773676116772327_DAB4FAD8-2DD7-40BB-A1B8-4E2AA1F9FDF2> >> _______________________________________________ >> OpenSCAD mailing list >> To unsubscribe send an email to >> discuss-leave@lists.openscad.org >> >> >> _______________________________________________ >> OpenSCAD mailing list >> To unsubscribe send an email todiscuss-leave@lists.openscad.org > _______________________________________________ > OpenSCAD mailing list > To unsubscribe send an email to discuss-leave@lists.openscad.org >
TA
Todd Allen
Wed, Dec 24, 2025 6:44 PM

Here is a revised hull_join.scad that works better with BOSL2 along with
some examples both without and with BOSL2 of how it can be used.  It’s
better than I expected but will appreciate any thoughts for further
improvements.

corner.scad:  At back right an even chamfer, in the middle a second join
with different scaling adds a shaped chamfer for an inner gusset, at the
front left iterating on the second join for a rounded gusset.
[image: corner.png]

stool.scad:  Separate hull_join() operations for the leg to seat joint and
the leg to legbrace joint allow different scale parameters to be used for
different joint shapes.  Although the legbrace is concave by selecting a
center point of scaling at the center of the intersection between the leg
and legbrace a scale vector is calculated to produce a precisely sized and
shaped joint.
[image: stool.png]

pod.scad:  A BOSL2 example
[image: pod.png]

siblings.scad:  A BOSL2 example using parent() and restore() to join
siblings in a BOSL2 attachment hierarchy.  It demonstrates making uniform
joins of specified size across dimensions in which those objects are not
uniform.
[image: siblings.png]

// ******************************  hull_join.scad


/*
Hulled joining of children objects
Hulls child pairs limited to intersections of each child with scaled partner

Each child object needs hj_scale(scale_vector, center_point=[0,0,0])
or hjt_scale() as a parent to control its scaling for the hull_join()
operation.

Scale vector and optional center point of scaling passed to
hj_scale() or hjt_scale() of each child control the size and angle of
joints.

When used with BOSL2:
After an object producing module invocation to which we want to apply
hull_join()
use let(child_t=transform()) to capture transformations produced by
parameters such
as anchor, spin and orient.  When reproducing this object as a child of
hull_join()
suppress transformation generating parameters as they will be applied in
hjt_scale()
by passing it the captured transformation matrix as shown in the examples.

pairs = specifies which pairs of children to join
"chain" : [0,1], [1,2], [2,3], ... [n-2, n-1]
"loop" : [0,1], [1,2], [2,3], ... [n-2, n-1], [n-1, 0]
"first" : [0,1], [0,2], [0,3], ... [0, n-1]
"all" : all pairs
*/
$hj_scale=0;
function get_pairs_chain(n) = [for(i=[0:n-2]) [i,i+1]];
function get_pairs_loop(n) = [for(i=[0:n-2]) [i,i+1], [n-1,0]];
function get_pairs_first(n) = [for(i=[1:n-1]) [0,i]];
function get_pairs_all(n) = [for(i=[0:n-2]) for(j=[i+1:n-1]) [i,j]];
module hull_join(iterations=1, pairs="chain", children=false) {
assert($children > 1, "hull_join() requires 2 or more children");
union() {
if(children)
children();
pairlist =
pairs=="chain" ? get_pairs_chain($children) :
pairs=="loop" ? get_pairs_loop($children) :
pairs=="first" ? get_pairs_first($children) :
pairs=="all" ? get_pairs_all($children) :
assert(false,"hulled_join(): invalid pairs parameter.");
for(si=[0:1:iterations-1]) for(pair = pairlist) hull() {
intersection() {
children(pair[0], $hj_scale=0);
children(pair[1], $hj_scale=(si+1)/iterations);
}
intersection() {
children(pair[0], $hj_scale=(iterations-si)/iterations);
children(pair[1], $hj_scale=0);
}
}
}
}

// returns a scale vector to resize something of a given size by an offset
// assumes scaling about a center point, thus 2.0offset to offset in each
direction
function hj_get_scale_for_offset(size, offset) = [
1.0 + 2.0
offset.x/size.x,
1.0 + 2.0offset.y/size.y,
1.0 + 2.0
offset.z/size.z
];

// controlled scaling about a center point of a child of hulled_join()
module hj_scale(scale_vector, center_point=[0,0,0]) {
if ($hj_scale) {
sv = [
1+$hj_scale*(scale_vector.x-1),
1+$hj_scale*(scale_vector.y-1),
1+$hj_scale*(scale_vector.z-1)
];
if(center_point.x || center_point.y || center_point.z)
// optimizable to a single translation?
translate(center_point) scale(sv) translate(-1*center_point)
children();
else
scale(sv) children();
}
else children();
}

function transform() = $transform;

// controlled scaling about a center point of a BOSL2 child of hulled_join()
module hjt_scale(transform, scale_vector, center_point=[0,0,0]) {
if ($hj_scale) {
s = [
1+$hj_scale*(scale_vector.x-1),
1+$hj_scale*(scale_vector.y-1),
1+$hj_scale*(scale_vector.z-1)
];
scaled = (center_point.x || center_point.y || center_point.z) ?
transform * move(center_point) * scale(s) * move(-center_point)
:
transform * scale(s);
multmatrix(scaled) children();
}
else multmatrix(transform) children();
}

// ******************************  corner.scad


include <hull_join.scad>

radius = 2;
length = 20;
rod_cp = [0, 0, length*0.5];
a1 = 30;
a2 = 90;
module rod() {cylinder(h=length, r=radius, $fn=32);}
module yrot(angle) {rotate(a=angle, v=[0,1,0]) children();}

translate([8,8,0])
// join with an even chamfer
hull_join(children=true) {
yrot(a1) hj_scale([1.5, 1.5, 2.0], rod_cp) rod();
yrot(a2) hj_scale([1.5, 1.5, 2.0], rod_cp) rod();
}

union() {
// join with an even chamfer
hull_join(children=true) {
yrot(a1) hj_scale([1.5, 1.5, 2.0], rod_cp) rod();
yrot(a2) hj_scale([1.5, 1.5, 2.0], rod_cp) rod();
}
// add a shaped inner gusset
hull_join() {
yrot(a1) hj_scale([2.25, 1.5, 1.0], rod_cp) rod();
yrot(a2) hj_scale([4.0 ,0.6, 1.0], rod_cp) rod();
}
}

translate([-8,-8,0]) union() {
// join with an even chamfer
hull_join(children=true) {
yrot(a1) hj_scale([1.5, 1.5, 2.0], rod_cp) rod();
yrot(a2) hj_scale([1.5, 1.5, 2.0], rod_cp) rod();
}
// add a shaped inner gusset
hull_join(32) {
yrot(a1) hj_scale([2.25, 1.5, 1.0], rod_cp) rod();
yrot(a2) hj_scale([4.0 ,0.6, 1.0], rod_cp) rod();
}
}

// ******************************  stool.scad


include <hull_join.scad>

$fn = 128;
eps = 0.001;

seat_size = [20,20,2];
seat_cp = [0,0,seat_size.z*0.5]; // seat center point
seat_offset = [0,0,1.0]; // distance join will extend from seat
module seat() {
rotate_extrude() union() {
square([(seat_size.x-seat_size.z)*0.5+eps, seat_size.z]);
translate([(seat_size.x-seat_size.z)0.5,seat_size.z0.5])
circle(d=seat_size.z);
}
}

leg_size = [2,2,26];
// joins will only be formed around diameter of leg, not around the ends
// thus the z component of offsets is 0 and we don't need to set the center
point of scaling in z
leg_to_seat_offset = [1.5,1.5,0];
leg_to_brace_offset = [0.75,0.75,0];
module leg() {
cylinder(h=leg_size.z, d1=leg_size.x, d2=leg_size.x*0.7);
}

module place_leg() {
translate([seat_size.x0.3,0,seat_size.z0.5]) rotate(a=10, v=[0,1,0])
children();
}

legbrace_size = [18.5,18.5,0.75];
legbrace_join_size = [legbrace_size.z, leg_size.y, legbrace_size.z2.0]; //
size of intersection of legbrace and leg
legbrace_offset = [0.10,0.10,0.125];
legbrace_cp = [legbrace_size.x
0.5,0,0]; // center of intersection of
legbrace and leg
module legbrace() {
rotate_extrude()
translate([legbrace_size.x0.5,0]) rotate(-10) scale([1,2.0])
circle(d=legbrace_size.z);
}
module place_legbrace() {
translate([0,0,seat_size.z
0.5+leg_size.z*0.7]) children();
}

module placed_leg() {
place_leg() leg();
// join seat & leg
hull_join(iterations=2) {
hj_scale(hj_get_scale_for_offset(seat_size, seat_offset), seat_cp)
seat();
place_leg() hj_scale(hj_get_scale_for_offset(leg_size,
leg_to_seat_offset)) leg();
}
// join leg & legbrace
hull_join(iterations=10) {
place_leg() hj_scale(hj_get_scale_for_offset(leg_size,
leg_to_brace_offset)) leg();
place_legbrace()
hj_scale(hj_get_scale_for_offset(legbrace_join_size, legbrace_offset),
legbrace_cp) legbrace();
}
}

module legs() {
for(zrot=[0,120,240])
rotate(a=zrot, v=[0,0,1]) placed_leg();
}

union() {
seat();
place_legbrace() legbrace();
legs();
}

// ******************************  pod.scad  ******************************
include <BOSL2/std.scad>
include <hull_join.scad>

eps = 0.001;

wall = 1.5;

base_size = [10,10,8.5];
base_offset = 8*[1,1,0];
// Make wrapper modules for objects used in a hierarchy with BOSL2 features
for positioning and orientation.
// After each object to be used in hull_join() capture its transformation
matrix.
// Then restore() the coordinate system and inside hull_join() reproduce
each object as
// a child of hjt_scale() which applies scaling before applying the
captured matrix.
// The (transform=false) parameter is added to wrapper modules when needed
to enable suppressing features such as anchor, spin and orientation which
modify the transformation matrix.  We pass transform=true in the first call
of the wrapper before capturing the transformation matrix.  Then in
hull_join() hjt_scale() can operate on an untransformed version of the
object before applying the captured matrix.
module base(transform=false) {zcyl(d1=base_size.x+5, d2=base_size.x-5,
h=base_size.z, chamfer=0.5, anchor=transform?BOT:CTR, $fn=3) children();}
module basex() {tag("podx") zcyl(d1=base_size.x+5-3wall,
d2=base_size.x-5-3
wall, h=base_size.z+2*eps, chamfer=-0.5, $fn=3)
children();}

body_size = [10,10,10];
body_offset = 3*[1,1,0];
module body() {zcyl(d1=body_size.x+2, d2=body_size.x-2, h=body_size.z,
chamfer=0.5, $fn=6) children();}
module bodyx() {tag("podx") zcyl(d1=body_size.x+2-2wall,
d2=body_size.x-2-2
wall, h=body_size.z+0.6, chamfer1=-0.5, $fn=6)
children();}

diff(remove="podx")
base(true) let(base_t=transform()) {
basex();
zrot_copies([0, 120, 240]) up(0.45) attach(RIGHT,LEFT,overlap=1) body()
let(body_t=transform()) {
bodyx();
restore() hull_join(iterations=32) {
hjt_scale(base_t, hj_get_scale_for_offset(base_size,
base_offset)) base();
hjt_scale(body_t, hj_get_scale_for_offset(body_size,
body_offset)) body();
}
}
}

// ******************************  siblings.scad


include <BOSL2/std.scad>
include <hull_join.scad>

$fs = 0.05;

join = 0.33;
base_size = [10,15,1];
base_offset = join*[1,1,1];
module base(transform=false) {cuboid(base_size, rounding=0.5,
anchor=transform?BACK+BOT:CTR) children();}

body_size = [1,10,20];
body_offset = join*[1,1,1];
module body(angle=0) {cuboid(body_size, rounding=0.5, spin=angle)
children();}
module place_body(angle) zrot(angle) children();

xrot(-35) base(true) let(base_desc=parent(), base_t=transform()) {
// if both bodies were direct children of base,
// then as siblings body1_t & body2_t would be in different scopes
unable to be together in hull_join()
// restore(base_desc) allows 2nd body to effectively have base as its
parent while being a child of 1st body
attach(FRONT+TOP+LEFT,RIGHT,overlap=5) body(-45)
let(body1_t=transform())
restore(base_desc)
attach(FRONT+TOP+RIGHT,LEFT,overlap=5) body(45) let(body2_t=transform())
restore()
hull_join(iterations=8, pairs="all") {
hjt_scale(base_t, hj_get_scale_for_offset(base_size, base_offset))
base();
hjt_scale(body1_t, hj_get_scale_for_offset(body_size, body_offset))
body();
hjt_scale(body2_t, hj_get_scale_for_offset(body_size, body_offset))
body();
}
}

On Sat, Dec 6, 2025 at 4:23 PM Todd Allen speedebikes@gmail.com wrote:

Here's an example of how I sometimes chamfer joints using hull().  I have
found this useful but it would be more useful if a similar method could be
applied between parent and child objects using BOSL2's
positioning/alignment/attachment but I don't yet see a good way and would
love it if someone could help me do it.

module centered_scale(center_point, scale_vector) {
translate(center_point)
scale(scale_vector)
translate(-1*center_point)
children(0);
}

// chamfered joining of objects
// unions objects with a hull of those objects limited to the intersection
of the objects after scaling
// adjust object scale factors to change the size and angle of the chamfer
module hulled_join(centered_scales) {
union() {
children();
hull() intersection() {
intersection_for(i=[0:1:$children-1])
centered_scale(centered_scales[i][0],
centered_scales[i][1])
children(i);
union()
children();
}
}
}

size1 = 10;
size2 = 5;
pos1 = [0,0,0];
pos2 = [size10.5+size20.3,0,0];

//  EXAMPLE 1
hulled_join([
[pos1, (1+1.5/size1)[1,1,1]],
[pos2, (1+1.0/size2)
[1,1,1]]
])
{
translate(pos1)
cube(10, center=true);
translate(pos2)
rotate(a=[20,20,0]) cube(5, center=true);
}

// EXAMPLE 2
translate([size12,0,0])
hulled_join([
[pos1, (1+1.0/size1)
[1,1,1]],
[pos2, (1+1.3/size2)*[1,1,1]]
], $fn=128)
{
translate(pos1)
sphere(d=size1);
translate(pos2)
rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2,
center=true);
}

Here is a revised hull_join.scad that works better with BOSL2 along with some examples both without and with BOSL2 of how it can be used. It’s better than I expected but will appreciate any thoughts for further improvements. corner.scad: At back right an even chamfer, in the middle a second join with different scaling adds a shaped chamfer for an inner gusset, at the front left iterating on the second join for a rounded gusset. [image: corner.png] stool.scad: Separate hull_join() operations for the leg to seat joint and the leg to legbrace joint allow different scale parameters to be used for different joint shapes. Although the legbrace is concave by selecting a center point of scaling at the center of the intersection between the leg and legbrace a scale vector is calculated to produce a precisely sized and shaped joint. [image: stool.png] pod.scad: A BOSL2 example [image: pod.png] siblings.scad: A BOSL2 example using parent() and restore() to join siblings in a BOSL2 attachment hierarchy. It demonstrates making uniform joins of specified size across dimensions in which those objects are not uniform. [image: siblings.png] // ****************************** hull_join.scad ****************************** /* Hulled joining of children objects Hulls child pairs limited to intersections of each child with scaled partner Each child object needs hj_scale(scale_vector, center_point=[0,0,0]) or hjt_scale() as a parent to control its scaling for the hull_join() operation. Scale vector and optional center point of scaling passed to hj_scale() or hjt_scale() of each child control the size and angle of joints. When used with BOSL2: After an object producing module invocation to which we want to apply hull_join() use let(child_t=transform()) to capture transformations produced by parameters such as anchor, spin and orient. When reproducing this object as a child of hull_join() suppress transformation generating parameters as they will be applied in hjt_scale() by passing it the captured transformation matrix as shown in the examples. pairs = specifies which pairs of children to join "chain" : [0,1], [1,2], [2,3], ... [n-2, n-1] "loop" : [0,1], [1,2], [2,3], ... [n-2, n-1], [n-1, 0] "first" : [0,1], [0,2], [0,3], ... [0, n-1] "all" : all pairs */ $hj_scale=0; function get_pairs_chain(n) = [for(i=[0:n-2]) [i,i+1]]; function get_pairs_loop(n) = [for(i=[0:n-2]) [i,i+1], [n-1,0]]; function get_pairs_first(n) = [for(i=[1:n-1]) [0,i]]; function get_pairs_all(n) = [for(i=[0:n-2]) for(j=[i+1:n-1]) [i,j]]; module hull_join(iterations=1, pairs="chain", children=false) { assert($children > 1, "hull_join() requires 2 or more children"); union() { if(children) children(); pairlist = pairs=="chain" ? get_pairs_chain($children) : pairs=="loop" ? get_pairs_loop($children) : pairs=="first" ? get_pairs_first($children) : pairs=="all" ? get_pairs_all($children) : assert(false,"hulled_join(): invalid pairs parameter."); for(si=[0:1:iterations-1]) for(pair = pairlist) hull() { intersection() { children(pair[0], $hj_scale=0); children(pair[1], $hj_scale=(si+1)/iterations); } intersection() { children(pair[0], $hj_scale=(iterations-si)/iterations); children(pair[1], $hj_scale=0); } } } } // returns a scale vector to resize something of a given size by an offset // assumes scaling about a center point, thus 2.0*offset to offset in each direction function hj_get_scale_for_offset(size, offset) = [ 1.0 + 2.0*offset.x/size.x, 1.0 + 2.0*offset.y/size.y, 1.0 + 2.0*offset.z/size.z ]; // controlled scaling about a center point of a child of hulled_join() module hj_scale(scale_vector, center_point=[0,0,0]) { if ($hj_scale) { sv = [ 1+$hj_scale*(scale_vector.x-1), 1+$hj_scale*(scale_vector.y-1), 1+$hj_scale*(scale_vector.z-1) ]; if(center_point.x || center_point.y || center_point.z) // optimizable to a single translation? translate(center_point) scale(sv) translate(-1*center_point) children(); else scale(sv) children(); } else children(); } function transform() = $transform; // controlled scaling about a center point of a BOSL2 child of hulled_join() module hjt_scale(transform, scale_vector, center_point=[0,0,0]) { if ($hj_scale) { s = [ 1+$hj_scale*(scale_vector.x-1), 1+$hj_scale*(scale_vector.y-1), 1+$hj_scale*(scale_vector.z-1) ]; scaled = (center_point.x || center_point.y || center_point.z) ? transform * move(center_point) * scale(s) * move(-center_point) : transform * scale(s); multmatrix(scaled) children(); } else multmatrix(transform) children(); } // ****************************** corner.scad ****************************** include <hull_join.scad> radius = 2; length = 20; rod_cp = [0, 0, length*0.5]; a1 = 30; a2 = 90; module rod() {cylinder(h=length, r=radius, $fn=32);} module yrot(angle) {rotate(a=angle, v=[0,1,0]) children();} translate([8,8,0]) // join with an even chamfer hull_join(children=true) { yrot(a1) hj_scale([1.5, 1.5, 2.0], rod_cp) rod(); yrot(a2) hj_scale([1.5, 1.5, 2.0], rod_cp) rod(); } union() { // join with an even chamfer hull_join(children=true) { yrot(a1) hj_scale([1.5, 1.5, 2.0], rod_cp) rod(); yrot(a2) hj_scale([1.5, 1.5, 2.0], rod_cp) rod(); } // add a shaped inner gusset hull_join() { yrot(a1) hj_scale([2.25, 1.5, 1.0], rod_cp) rod(); yrot(a2) hj_scale([4.0 ,0.6, 1.0], rod_cp) rod(); } } translate([-8,-8,0]) union() { // join with an even chamfer hull_join(children=true) { yrot(a1) hj_scale([1.5, 1.5, 2.0], rod_cp) rod(); yrot(a2) hj_scale([1.5, 1.5, 2.0], rod_cp) rod(); } // add a shaped inner gusset hull_join(32) { yrot(a1) hj_scale([2.25, 1.5, 1.0], rod_cp) rod(); yrot(a2) hj_scale([4.0 ,0.6, 1.0], rod_cp) rod(); } } // ****************************** stool.scad ****************************** include <hull_join.scad> $fn = 128; eps = 0.001; seat_size = [20,20,2]; seat_cp = [0,0,seat_size.z*0.5]; // seat center point seat_offset = [0,0,1.0]; // distance join will extend from seat module seat() { rotate_extrude() union() { square([(seat_size.x-seat_size.z)*0.5+eps, seat_size.z]); translate([(seat_size.x-seat_size.z)*0.5,seat_size.z*0.5]) circle(d=seat_size.z); } } leg_size = [2,2,26]; // joins will only be formed around diameter of leg, not around the ends // thus the z component of offsets is 0 and we don't need to set the center point of scaling in z leg_to_seat_offset = [1.5,1.5,0]; leg_to_brace_offset = [0.75,0.75,0]; module leg() { cylinder(h=leg_size.z, d1=leg_size.x, d2=leg_size.x*0.7); } module place_leg() { translate([seat_size.x*0.3,0,seat_size.z*0.5]) rotate(a=10, v=[0,1,0]) children(); } legbrace_size = [18.5,18.5,0.75]; legbrace_join_size = [legbrace_size.z, leg_size.y, legbrace_size.z*2.0]; // size of intersection of legbrace and leg legbrace_offset = [0.10,0.10,0.125]; legbrace_cp = [legbrace_size.x*0.5,0,0]; // center of intersection of legbrace and leg module legbrace() { rotate_extrude() translate([legbrace_size.x*0.5,0]) rotate(-10) scale([1,2.0]) circle(d=legbrace_size.z); } module place_legbrace() { translate([0,0,seat_size.z*0.5+leg_size.z*0.7]) children(); } module placed_leg() { place_leg() leg(); // join seat & leg hull_join(iterations=2) { hj_scale(hj_get_scale_for_offset(seat_size, seat_offset), seat_cp) seat(); place_leg() hj_scale(hj_get_scale_for_offset(leg_size, leg_to_seat_offset)) leg(); } // join leg & legbrace hull_join(iterations=10) { place_leg() hj_scale(hj_get_scale_for_offset(leg_size, leg_to_brace_offset)) leg(); place_legbrace() hj_scale(hj_get_scale_for_offset(legbrace_join_size, legbrace_offset), legbrace_cp) legbrace(); } } module legs() { for(zrot=[0,120,240]) rotate(a=zrot, v=[0,0,1]) placed_leg(); } union() { seat(); place_legbrace() legbrace(); legs(); } // ****************************** pod.scad ****************************** include <BOSL2/std.scad> include <hull_join.scad> eps = 0.001; wall = 1.5; base_size = [10,10,8.5]; base_offset = 8*[1,1,0]; // Make wrapper modules for objects used in a hierarchy with BOSL2 features for positioning and orientation. // After each object to be used in hull_join() capture its transformation matrix. // Then restore() the coordinate system and inside hull_join() reproduce each object as // a child of hjt_scale() which applies scaling before applying the captured matrix. // The (transform=false) parameter is added to wrapper modules when needed to enable suppressing features such as anchor, spin and orientation which modify the transformation matrix. We pass transform=true in the first call of the wrapper before capturing the transformation matrix. Then in hull_join() hjt_scale() can operate on an untransformed version of the object before applying the captured matrix. module base(transform=false) {zcyl(d1=base_size.x+5, d2=base_size.x-5, h=base_size.z, chamfer=0.5, anchor=transform?BOT:CTR, $fn=3) children();} module basex() {tag("podx") zcyl(d1=base_size.x+5-3*wall, d2=base_size.x-5-3*wall, h=base_size.z+2*eps, chamfer=-0.5, $fn=3) children();} body_size = [10,10,10]; body_offset = 3*[1,1,0]; module body() {zcyl(d1=body_size.x+2, d2=body_size.x-2, h=body_size.z, chamfer=0.5, $fn=6) children();} module bodyx() {tag("podx") zcyl(d1=body_size.x+2-2*wall, d2=body_size.x-2-2*wall, h=body_size.z+0.6, chamfer1=-0.5, $fn=6) children();} diff(remove="podx") base(true) let(base_t=transform()) { basex(); zrot_copies([0, 120, 240]) up(0.45) attach(RIGHT,LEFT,overlap=1) body() let(body_t=transform()) { bodyx(); restore() hull_join(iterations=32) { hjt_scale(base_t, hj_get_scale_for_offset(base_size, base_offset)) base(); hjt_scale(body_t, hj_get_scale_for_offset(body_size, body_offset)) body(); } } } // ****************************** siblings.scad ****************************** include <BOSL2/std.scad> include <hull_join.scad> $fs = 0.05; join = 0.33; base_size = [10,15,1]; base_offset = join*[1,1,1]; module base(transform=false) {cuboid(base_size, rounding=0.5, anchor=transform?BACK+BOT:CTR) children();} body_size = [1,10,20]; body_offset = join*[1,1,1]; module body(angle=0) {cuboid(body_size, rounding=0.5, spin=angle) children();} module place_body(angle) zrot(angle) children(); xrot(-35) base(true) let(base_desc=parent(), base_t=transform()) { // if both bodies were direct children of base, // then as siblings body1_t & body2_t would be in different scopes unable to be together in hull_join() // restore(base_desc) allows 2nd body to effectively have base as its parent while being a child of 1st body attach(FRONT+TOP+LEFT,RIGHT,overlap=5) body(-45) let(body1_t=transform()) restore(base_desc) attach(FRONT+TOP+RIGHT,LEFT,overlap=5) body(45) let(body2_t=transform()) restore() hull_join(iterations=8, pairs="all") { hjt_scale(base_t, hj_get_scale_for_offset(base_size, base_offset)) base(); hjt_scale(body1_t, hj_get_scale_for_offset(body_size, body_offset)) body(); hjt_scale(body2_t, hj_get_scale_for_offset(body_size, body_offset)) body(); } } On Sat, Dec 6, 2025 at 4:23 PM Todd Allen <speedebikes@gmail.com> wrote: > Here's an example of how I sometimes chamfer joints using hull(). I have > found this useful but it would be more useful if a similar method could be > applied between parent and child objects using BOSL2's > positioning/alignment/attachment but I don't yet see a good way and would > love it if someone could help me do it. > > module centered_scale(center_point, scale_vector) { > translate(center_point) > scale(scale_vector) > translate(-1*center_point) > children(0); > } > > // chamfered joining of objects > // unions objects with a hull of those objects limited to the intersection > of the objects after scaling > // adjust object scale factors to change the size and angle of the chamfer > module hulled_join(centered_scales) { > union() { > children(); > hull() intersection() { > intersection_for(i=[0:1:$children-1]) > centered_scale(centered_scales[i][0], > centered_scales[i][1]) > children(i); > union() > children(); > } > } > } > > size1 = 10; > size2 = 5; > pos1 = [0,0,0]; > pos2 = [size1*0.5+size2*0.3,0,0]; > > // EXAMPLE 1 > hulled_join([ > [pos1, (1+1.5/size1)*[1,1,1]], > [pos2, (1+1.0/size2)*[1,1,1]] > ]) > { > translate(pos1) > cube(10, center=true); > translate(pos2) > rotate(a=[20,20,0]) cube(5, center=true); > } > > // EXAMPLE 2 > translate([size1*2,0,0]) > hulled_join([ > [pos1, (1+1.0/size1)*[1,1,1]], > [pos2, (1+1.3/size2)*[1,1,1]] > ], $fn=128) > { > translate(pos1) > sphere(d=size1); > translate(pos2) > rotate(a=[90,20,0]) scale([1,0.5,1]) cylinder(h=size1, d=size2, > center=true); > } >