PShape Intersections

Hi there :wave:

I’m using processing to generate SVG files for pen plotting.

I have a PShape object which is a custom geometry, I would like to use that geometry as a mask/intersection or a boolean check to then draw a some additional geometry within it as a fill.

I’m not able to use textures or PImage objects for the fill as I need to be able to export an SVG at the end of the script.

I’m aware of RShape from geometrive, which does offer a handy .intersection() function, although I’m not sure how to get that to play nicely with my PShape objects.

Are there any other methods I could investigate to solve my problem?
(I’m trying to avoid writing lengthy region checks)

For reference; the geometries that I would like to fill are these sections of a circle:

Thanks for any help you can offer.

1 Like

This is a bit of a hack, maybe not an answer you’re looking for …

Sometimes I render ‘whole’ shapes in the Processing sketch, then apply the ‘boolean’ operation I desire in Inkscape (you might even automate this using Inkscape’s command line mode). So, the fill is one set of shapes, and the mask another. Additionally, you can separate shapes into layers to make the Inkscape work easier.

I’d rather expedite the process of getting results onto paper when I’m plotting, even if the sketch isn’t ‘fully-automated.’

2 Likes

Processing Geometry Suite provides 2D shape boolean operations like Geomerative but takes in (and outputs) PShapes.

4 Likes

Wow! That’s a seriously well featured suite!

There’s a lot of methods in there that I’d been putting off trying for fear of over-complication.

I’m going to enjoy playing with this package, thank you very much!

Yeah that is a bit of a hack… :grimacing:
I once lined some disparate parts of a plot up in Inkscape and still feel a little guilty about it.

I guess I take the opinion that the final drawing is only an abstraction of the input code if it remains as unadulterated as possible throughout the entire digital workflow.
I’m either a purist or a pedant and I’m not sure which is worse! :sweat_smile:

1 Like

I’m having some issues getting .intersection to work as I’d expect.

As above I’m taking my sectioned circle and trying to fill each section with some diagonal lines.

I’m applying your .intersection() method as below:

      shape(section);
      if (i%2==0) {
        fill = PGS_ShapeBoolean.intersect(section, leftFill);
      } else {
        fill = PGS_ShapeBoolean.intersect(section, rightFill);
      }
      PGS_Conversion.setAllStrokeColor(fill, 0, 1);
      shape(fill);

I’m seeing vertical lines being output, not the diagonal lines that are contained in rightFill & leftFill.

Any thoughts?

Can you post your full code? I’m not fully clear about what you’re trying to do… slice the vertical strips? What are leftFill and rightFill exactly?

Excuse my messy code!
The idea is that I generate the sectioned circle as above and then fill each section with an alternating fill, leftFill & rightFill. Which are PShape objects made up of many diagonal lines with varying spacing.

I had hoped that .intersect() would allow me to make a masked PShape object to apply rightFill & leftFill to the main sectioned circle shape. Although I’m guessing that I’m getting my use cases mixed up…

import processing.svg.*;
import micycle.pgs.*;

void setup() {
  size(420, 594);
  background(255);
  noLoop();
  noFill();
  beginRecord(SVG, "sectionedCircle.svg");
}

PShape createLineFills(float a, float maxspacing) {
  float spacing = maxspacing;
  PShape line = createShape();
  
  line.beginShape(LINES);
  for (float x=-height; x<=height; x+=spacing) {
    line.vertex(x, -height);
    line.vertex(x, height);
    spacing = map(noise(x*0.01), 0, 1, 0, maxspacing);
  }
  line.endShape();
  line.rotate(a);
  return line;
}

float sagitta(float r, float s) {
  return sqrt(2*s*r-s*s);
}

void drawSectionedLinedCircle(float x, float y, float r, int n) {
  translate(x, y);
  float rInc = (2*r)/n;
  float s = rInc;
  float a1 = 0;
  float a2 = asin(sagitta(r, s)/r);
  float maxShift = 100;
  float scale = 0.15;

  PShape leftFill = createLineFills(QUARTER_PI, 50);
  PShape rightFill = createLineFills(-QUARTER_PI, 50);
  PShape fill;

  while (a2<=PI) {
    for (float i=0; i<n; i++) {
      
      pushMatrix();
      translate(0, map(noise(i*scale), 0, 1, -maxShift, maxShift));
      
      PShape section = createShape();
      section.beginShape();

      for (float a=a1; a<=a2; a+=radians(0.5)) {
        section.vertex(r*cos(a), r*sin(a));
      }
      
      for (float a=a2; a>=a1; a-=radians(0.5)) {
        section.vertex(r*cos(-a), r*sin(-a));
      }
      section.endShape(CLOSE);
      
      a1 = a2;
      s += rInc;
      a2 = asin(sagitta(r, s)/r);
      if (s>=r)
        a2 += 2*(HALF_PI-a2);
      //println(degrees(a2));

      shape(section);
      if (i%2==0) {
        fill = PGS_ShapeBoolean.intersect(section, leftFill);
      } else {
        fill = PGS_ShapeBoolean.intersect(section, rightFill);
      }
      PGS_Conversion.setAllStrokeColor(fill, 150, 2);
      shape(fill);
      popMatrix();
    }
  }
}

void draw() {
  drawSectionedLinedCircle(width/2, height/2, width/2, 9);
  endRecord();
}
1 Like

Your approach was correct, but you were thrown off by the way geometry operations work. Lines are 1D objects (they have no “height”) so intersecting a line with a polygon returns nothing. The solution is to buffer the lines into actual 2D polygons so intersection() becomes a meaningful operation.

I’ve also had to replace line.rotate(a) with a PGS rotation method. The problem with .rotate() is that it affects the shape’s rotation matrix only and not the actual coordinates of its vertices. A PShape’s rotation matrix is not accessible and so the buffer() method would otherwise buffer the unrotated (vertical) lines.

So in the end only your createLineFills() method needed changing in the last few lines.

PShape createLineFills(float a, float maxspacing) {
  float spacing = maxspacing;
  PShape line = createShape();

  line.beginShape(LINES);
  for (float x=-height; x<=height; x+=spacing) {
    line.vertex(x, -height);
    line.vertex(x, height);
    spacing = map(noise(x*0.01), 0, 1, 0, maxspacing);
  }
  line.endShape();
  line = PGS_Transformation.rotateAroundCenter(line, a);
  line = PGS_Morphology.buffer(line, 1);
  return line;
}

image

4 Likes

Ah Thanks so much!
I think I’m going to spend some time exploring the quirks of PGS further.

Glad to be able to put a lid on this little project. The final output is interesting to look at, but I’m very excited to be able to quickly and easily create fills for custom geometry.

Thanks Again!

4 Likes

@micycle PGS is exactly the tool that will make one of my projects easier to do. There’s something in your above ‘createLineFills’ method here that’s baffling me though. It seems like all lines are somehow getting doubled? Even if I strip out the ‘for’ loops and use line.vertex to draw a single diagonal from the upper-left to the lower-right of the container, it renders (and saves as an SVG) with two diagonal lines separated by a few pixels. Is this a known quirk, and/or is there a way to have it draw just a single line?

Edit: I just read your response above about needing the lines to form a closed loop in order for the intersection operation to work.

What I said before was actually incorrect. PGS can (at least now) intersect a LINES shape with a polygon, returning the linework as expected. For instance, a hilbert curve intersected with a heart-shaped polygon:

1 Like

@micycle sweet, is that a more recent development? I need to check which version of PGS I’m using and update if it’s not the current.

Well I’m not sure why I didn’t think it worked like that before. Either way, latest dev build is here.

1 Like

Thanks for sharing this project! :raised_hands:t4:

Hey @micycle quick question on using intersect… I have a shape called fillyFill which contains several closed shapes created with beginShape…endShape(CLOSE). Now when I try to ‘shape mask’ them using a circle…

PGS_ShapeBoolean.intersect(circ, fillyFill)

I get an error that reads TopologyException: side location conflict: arg 1 [ (311.98916659225284, 486.17156982421875, NaN) ]. If I remove CLOSE from endShape, the intersection works. Are there some conditions where multiple shapes can’t be intersected by a shape? Or maybe my multiple shapes within fillyFill need to be added to a group? I’m baffled :slightly_smiling_face:

Intersecting multiple shapes with another one should be no big deal. The error message you’re encountering originates from the base geometry library, suggesting that there may be an issue with your shape’s structure or configuration.

First, try making fillyFill a ‘GROUP’ shape and add all the closed shapes to it. This might solve the issue.

If that doesn’t do the trick, let me know. We can dive deeper into the problem to figure out what’s going on.

Thanks for getting back to me @micycle! I tried using GROUP. Maybe my syntax is wonky, because when I do that, my intersected geometry seems to disappear :man_shrugging:t4:

If you have a few minutes, I’ve attached a sample sketch for reference. There’s a render method and a renderGroup method. In both cases, providing CLOSE as an arg in endShape will break things. Somehow Processing doesn’t seem to play nice with PGS_ShapeBoolean.intersect. Totally possible that I’m doing something wrong…

int radius = 70;
float[][] pts = {
  {130, 330, 130, 143, 270, 172, 270, 330},
  {130, 331, 130, 148, 270, 180, 270, 331},
  {130, 332, 130, 154, 270, 188, 270, 332},
  {130, 333, 130, 160, 270, 197, 270, 333},
  {130, 334, 130, 169, 270, 206, 270, 334},
  {130, 335, 130, 178, 270, 214, 270, 335},
  {130, 336, 130, 187, 270, 223, 270, 336},
  {130, 337, 130, 197, 270, 230, 270, 337}
};

void setup() {
  size(400, 400);
  noLoop();
}

void draw() {
  background(0);

  // Intersects multiple shapes only if endShape
  // is not provided with the CLOSE arg
  render(-80, 0);

  // When running renderGroup, intersected
  // geometry does not seem to be there
  renderGroup(80, 0);
}

void render(int x, int y) {
  PVector loc = new PVector(width / 2 + x, height / 2 + y);
  PShape s = createShape(ELLIPSE, loc.x, loc.y, 2 * radius, 2 * radius);
  s.setFill(0);
  s.setStroke(255);
  PShape fillyFill = createShape();

  for (int j = 0; j < pts.length; j++) {
    fillyFill.setFill(0);
    fillyFill.setStroke(255);

    float[] points = pts[j];
    float p0x = points[0];
    float p0y = points[1];
    float pNx = points[points.length - 2];
    float pNy = points[points.length - 1];

    fillyFill.beginShape();
    fillyFill.vertex(p0x + x, p0y + y);
    for (int i = 0; i < points.length / 2 - 2; i++) {
      float px = points[2 * i + 2];
      float py = points[2 * i + 3];
      fillyFill.vertex(px + x, py + y);
    }
    fillyFill.vertex(pNx + x, pNy + y);
    //fillyFill.endShape(CLOSE); // <- THIS DOES NOT WORK :(
    fillyFill.endShape();
  }

  PShape f1 = PGS_ShapeBoolean.intersect(s, fillyFill);
  PGS_Conversion.setAllFillColor(f1, color(0));
  PGS_Conversion.setAllStrokeColor(f1, color(255, 127, 0), 1);

  shape(s);
  shape(f1);
}

void renderGroup(int x, int y) {
  PVector loc = new PVector(width / 2 + x, height / 2 + y);
  PShape s = createShape(ELLIPSE, loc.x, loc.y, 2 * radius, 2 * radius);
  s.setFill(0);
  s.setStroke(255);
  PShape fillyFill = createShape(GROUP);

  for (int j = 0; j < pts.length; j++) {
    PShape temp = createShape();
    temp.setFill(0);
    temp.setStroke(255);

    float[] points = pts[j];
    float p0x = points[0];
    float p0y = points[1];
    float pNx = points[points.length - 2];
    float pNy = points[points.length - 1];

    temp.beginShape();
    temp.vertex(p0x + x, p0y + y);
    for (int i = 0; i < points.length / 2 - 2; i++) {
      float px = points[2 * i + 2];
      float py = points[2 * i + 3];
      temp.vertex(px + x, py + y);
    }
    temp.vertex(pNx + x, pNy + y);
    //temp.endShape(CLOSE); // <- THIS DOES NOT WORK
    temp.endShape();
    fillyFill.addChild(temp);
  }

  // Intersect the shape layer and the hatching layer
  PShape f1 = PGS_ShapeBoolean.intersect(s, fillyFill);

  // Set the color of the new hatching layer
  PGS_Conversion.setAllFillColor(f1, color(0));
  PGS_Conversion.setAllStrokeColor(f1, color(255, 127, 0), 1);

  shape(s);
  shape(f1);
}

It seems the way you’re making this shape is creating a lot of overlapping/nearly overlapping lines and it seems closing it is creating an invalid (self-intersecting) shape.

image

In fact we can check this with a JTS internal operation:

import org.locationtech.jts.operation.valid.IsValidOp;

IsValidOp o = new IsValidOp(PGS_Conversion.fromPShape(fillyFill));
println(o.getValidationError());

Result: Self-intersection at or near point (50.0, 148.0, NaN).

Aw man :slightly_frowning_face: I’m not well versed in matters of topology, so not sure if there’s a quick solution. One idea is to make it so that those overlapping lines (the area you circled in red) no longer overlap. Those lines are not part of the final composition after all; Maybe Processing will be happier if I programmatically prevent those overlaps (Update: This didn’t work lol). The other idea is to work around the self-intersecting issue by separating the overlapping closed shapes into individual shapes, and intersecting each of them with the ‘mask’ shape.

Ultimately I’m plotting this work on an AxiDraw pen plotter. I know that the Axi library has some methods for removing overlapping geometry and such, so things might work out afterall. Thanks for your time and insights Michael :raised_hands:t4: