Skip to content

[BUG] partition_mask()/partition_cut_mask()/partition() clips the first/last pattern strangely #1754

@aseanwatson

Description

@aseanwatson

Describe the bug
Things which rely on _partition_cutpath leave partial subpaths at the edges.

Here's the logic:

function _partition_cutpath(l, h, cutsize, cutpath, gap, cutpath_centered) =
    let(
//--snip--
        reps_raw = ceil(l/(cutsize.x+gap)),
        reps = reps_raw%2==0 && cutpath_centered ? reps_raw+1 : reps_raw,
        cplen = (cutsize.x+gap) * reps,
        path = deduplicate(concat(
            [[-l/2, cutpath[0].y*cutsize.y]],
            [for (i=[0:1:reps-1], pt=cutpath) v_mul(pt,cutsize)+[i*(cutsize.x+gap)+gap/2-cplen/2,0]],
            [[ l/2, cutpath[len(cutpath)-1].y*cutsize.y]]
        )),
//--snip--

Code To Reproduce Bug

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

partition(cutsize=[10,5], cutpath="hammerhead")
    cuboid([47,10,5]);
Image

The interesting part is that 47 isn't divisible by 10.

Here's a more interesting example, pay particular attention to the gap = 3 line:

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

l = 47;
cutsize=[10,5];
ycopies(n=5, spacing=30)
{
    gap = $idx;

    xdistribute(25)
    {
        ydistribute(15)
        {
            partition(cutsize=cutsize, gap=gap, cutpath="dovetail") cuboid([l,10,5]);
        }
        fwd(10)
        text(str(
                "gap = ", gap,
                "; cutsize.x+gap = ", cutsize.x+gap,
                "; f = ", floor(l / (cutsize.x+gap)),
                "; c = ", ceil(l / (cutsize.x+gap)),
                "; rem = ", l % (cutsize.x+gap)));
    }
}
Image

It's a problem whenever the cutpath does not have x always go up.

Expected behavior
High-level: No extra floating bits at the ends.

More specifically, if there are reps subpaths, there will be reps-1 gaps. Pick the biggest reps such that reps*cutsize.x + (reps-1)*gap <= l and, if cutpath_centered, ensure reps is odd.

I think that means something like this:

 function _partition_cutpath(l, h, cutsize, cutpath, gap, cutpath_centered) =
     let(
         check = assert(is_finite(l))
             assert(is_finite(h))
             assert(is_finite(gap))
             assert(is_finite(cutsize) || is_vector(cutsize,2))
+            assert(l >= cutsize.x)
             assert(is_string(cutpath) || is_path(cutpath,2)),
         cutsize = is_vector(cutsize)? cutsize : [cutsize*2, cutsize],
         cutpath = is_path(cutpath)? cutpath :
             _partition_subpath(cutpath),
+        // pick the biggest reps_raw such that cplen will be <= l
+        reps_raw = 1+floor((l - cutsize.x)/(cutsize.x + gap)),
-        reps_raw = ceil(l/(cutsize.x+gap)),
+        // if cutpath_centered, make sure reps is odd so a subpath will be centered on the cutpath
+        // be sure to always pick a smaller value so we don't truncate the path
+        reps = reps_raw%2==0 && cutpath_centered ? reps_raw-1 : reps_raw,
-        reps = reps_raw%2==0 && cutpath_centered ? reps_raw+1 : reps_raw,
+        // there will be reps copies of the subpath and reps-1 gaps
+        cplen = reps*cutsize.x + (reps-1)*gap,
-        cplen = (cutsize.x+gap) * reps,
          path = deduplicate(concat(
-             [[-cplen/2, cutpath[0].y*cutsize.y]],
+             [[-l/2, cutpath[0].y*cutsize.y]],
-             [for (i=[0:1:reps-1], pt=cutpath) v_mul(pt,cutsize)+[i*(cutsize.x+gap)+gap/2-cplen/2,0]],
+             [for (i=[0:1:reps-1], pt=cutpath) v_mul(pt,cutsize)+[i*(cutsize.x+gap),0]],
-             [[ l/2, cutpath[len(cutpath)-1].y*cutsize.y]]
+             [[ cplen/2, cutpath[len(cutpath)-1].y*cutsize.y]]
         )),

Even though I'm presenting the above as a diff, I haven't tested it. I don't know what the policy is for breaking changes, but I only started looking at this because I was wrestling with tweaking parameters to try to get the preview to look the way I wanted.

If you like this approach, I'm happy to make a PR and do some validation. I just wanted to start by checking to see if you need a more backward-compatible solution.

Screenshots
See above.

Additional context
Add any other context about the problem here.

  • OpenSCAD Version: 2025.06.22 (git de4f48109)
  • Other libraries used: none

There's some other odd stuff, not really related to this bug, in the function.

It takes an h parameter (and callers pass in the extrusion height) which isn't used.

I don't think this code is really necessary as long as the cutpath points have 0 <= x <= 1:

        stidxs = [for (i = idx(path)) if (path[i].x < -l/2) i],
        enidxs = [for (i = idx(path)) if (path[i].x > +l/2) i],
        stidx = stidxs? last(stidxs) : 0,
        enidx = enidxs? enidxs[0] : -1,
        trunc = select(path, stidx, enidx)

But, if someone "scribbles outside the lines", silently truncating might be a surprise.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions