2D lineart - culling hidden lines for SVG output / Plotting

Hi All, I would like generatively create something like this, in a 2D lineart/doodle sketch style.

Using PShape seems like the way to go, as a “Building” class with variants for Window, Roof, Door, etc (I’m a beginner with Processing) It seems straightforward to start at the background and progressively overlay objects - The problem is I cannot use fill() to set a white background of each house instance, as I want to use the SVG renderer / output to my plotter.

The only thing I can think of is to draw to the screen initially and use get() to look for all the points of overlap, then somehow use that data to cull lines during the SVG render? Pretty daunting for me, I’m a beginner at Processing.

Perhaps the 2D Boolean operations in GitHub - micycle1/PGS: Processing Geometry Suite but checking if anyone has done something similar, I might be over-thinking this? Thanks!

2 Likes

Ok, I think GitHub - micycle1/PGS: Processing Geometry Suite is a way forward - Here’s my quick check

The boxes on the left have fills which wouldn’t work on the plotter.

The boxes on the right are made of :

  1. The foreground box (green), and
  2. the subtraction of the foreground box from the background box (red)

No fill() used on the right hand side - So a bit of playing around to progressively add one house at a time to a resulting shape. Still would be keen to hear how anyone else would approach this one, Cheers!

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

boolean plotter = false;

void setup() 
{
  //size(10680, 6240); //a4
  size(1068, 624);
  if (plotter)
  {
    beginRecord(SVG, "test.svg");
  }

  background(255);
  noFill();
  stroke(0);
  strokeWeight(1.0);
  smooth();
}

void draw()
{
  //rect(10, 10, width - 20, height - 20);
  
  //The original houses on the left side (with fills to hide any background houses) 
  House backgroundHouse = new House(100, 100);
  House foregroundHouse = new House(140, 200);

  backgroundHouse.Draw();
  foregroundHouse.Draw();
  
  //Houses put through Processing Geometry Suite on the right side
  int destX = 500;
  int destY = 100;
  PShape bground = PGS_Transformation.translateTo(backgroundHouse.s, backgroundHouse.x, backgroundHouse.y);
  PShape fground = PGS_Transformation.translateTo(foregroundHouse.s, foregroundHouse.x, foregroundHouse.y);
  
  //Show the subtraction of the foreground house from the background
  PShape subtract = PGS_ShapeBoolean.subtract(bground, fground);
  PGS_Conversion.disableAllFill(subtract);
  PGS_Conversion.setAllStrokeColor(subtract, color(100, 0, 0), 2);
  shape(subtract, destX, destY);
  
  //Show the foreground house
  PGS_Conversion.disableAllFill(fground);
  PGS_Conversion.setAllStrokeColor(fground, color(0, 100, 0), 2);
  shape(fground, destX, destY);
  
  if (plotter)
  {
    endRecord();
  }
  noLoop();
}

class House //ok it's not much of a house, it's a box :)
{
  PShape s;
  int x;
  int y;
  
  House(int xPos, int yPos)
  {
    x = xPos;
    y = yPos;
    
    s = createShape();
    s.beginShape();
    s.fill(230);
    s.vertex(0, 0);
    s.vertex(0, 200);
    s.vertex(200, 200);
    s.vertex(200, 0);
    s.endShape(CLOSE);
  }
  
  void Draw()
  {
    shape(s, x, y);
  }
}

1 Like

@mrWoodo – Thanks so much for sharing this. Were you eventually able to arrive at the effect that you wanted using PGS_ShapeBoolean.subtract()?

1 Like

Hi Jeremy, yes that was the way to go - I got a bit sidetracked and didn’t finish off my project but here’s a small test overlaying hundreds of objects, and subtracting as I went. Probably not the most efficient way of doing it, but worked out well!

1 Like

Hi @mrWoodo, I’m working with SVGs and an AxiDraw pen plotter and came across your posts here. PGS does look promising, but when I ran the sketch as-is and opened the SVG in Illustrator, I realized that the composition retains the internal lines from both intersected squares. In the attached example on the right, I nudged the shapes apart by a bit and you can see that they share two lines.


AxiDraw can draw this just fine, but it will draw those overlapping lines twice. Did you experience this overlap with your final results and if so, was it just not a dealbreaker for your application?

1 Like

Hi @computershawn, for cleanup (and because I output to an ancient Roland plotter that uses HPGL), I use vpype - Check out the linemerge / linesimplify parameters which might help.

I’ve since gone a completely different route since posting this though - I ran into troubles once there’s too many polygons overlaid on each other. It’s too much work to keep subtracting / unioning / and you end up with runtime errors. This isn’t really a fault of PGS (I tried JTS as well with similar results) as it’s pushing these libs too far.

What I’m trying now is a bit hard to explain, but roughly :
1 - For every output polygon, fill it with a solid colour (starting at 1) and draw that result to an off screen buffer. Keep adding these and incrementing the colour by 1.
2 - Now agains that buffered image, extract a ‘mask’ image one at a time, starting at colour 1 - Convert that mask image to black / white. (So Mask image 1 = only the pixels that were colour 1).
3 - Run PoTrace against that mask, and convert that output to a ‘Mask’ Polygon.
4 - Then calculate the intersection between your original polygon and the Mask Polygon from Step 3, and plot that. Keep doing that for every Polygon + Mask pair.

So instead of building up a progressively bigger and bigger set of polygons, you’re only running the intersection once per shape, in Step 4. (The higher the resolution you use for the buffer image, the better the mask for PoTrace to work with).

Ridiculous I know :slight_smile: See how you go with vpype, but if you run into troubles I’ll post my alternate approach code, Cheers!

1 Like

@mrWoodo my apologies for such a delayed reply! Your brute force method of fixing the issue is way over my head haha. BUT… I took your advice and looked into vpype, and it turns out it has a plug-in called occult that removes hidden lines with just a single command. Ideally I could do hidden line removal within my Processing sketch, but the vpype/occult combo only adds one step and seems to work just fine. Thanks for your insights :raised_hands:t4:

1 Like

This is possible in Processing with PGS shape boolean operations.

The approach is: for each shape, subtract the union of shapes visually above it.

l will include the algorithm given below as a method in the next version of the library (which has been WIP for some time now!).

Example

Input

[left] Circles shaded by layer (lighter = visually on top). [right] Translucent fill to reveal overlap.

Line culling

Note that some edges duplicated/overlap where shapes have been cut away from each other.

Line culling + dissolve remaining lines

Having removed any overlapping edges, we get pure linework for svg/plotting.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import micycle.pgs.PGS_Conversion;
import micycle.pgs.PGS_ShapeBoolean;
import org.locationtech.jts.dissolve.LineDissolver;

PShape cullLines(PShape shape) {
	var layers = PGS_Conversion.getChildren(shape); // visual top last
	Collections.reverse(layers); // visual top first
	PShape union = layers.get(0); // cascading union

	List<PShape> layersOut = new ArrayList<>();
	layersOut.add(layers.get(0));
	for (int i = 1; i < layers.size(); i++) {
		// for each shape, subtract the union of shapes visually above it
		var layer = PGS_ShapeBoolean.subtract(layers.get(i), union);
		layersOut.add(layer);
		union = PGS_ShapeBoolean.union(union, layers.get(i));
	}

	var culled = PGS_Conversion.fromChildren(layersOut);
	var dissolved = toPShape(LineDissolver.dissolve(fromPShape(culled)));
	
	return dissolved;
	}
1 Like

@micycle woah. I’d heard of PGS but couldn’t find examples of implementing this hidden line removal process. I’ll try this out with my next project :raised_hands:t4:

2 Likes

@micycle
I’m trying this code exactly as posted (using your latest version, 1.4.0, permanent install), and I’m getting this error:
The function fromPShape(PShape) does not exist

This looks very promising, if I can get my head around even some of it. Thank you!

You’re right the example doesn’t quite work.

However, there’s now a standalone method for this at PGS_Processing.removeHiddenLines(). See docs for this method here.

1 Like

Thank you! This is great. One problem so far though, the stroke becomes wide and pink. I can change that back but I’d like to know if there’s a more elegant solution.

import processing.pdf.*;
import micycle.pgs.*;

void setup() {
  size(250, 200);
  noLoop();
  beginRecord(PDF, "alien.pdf");
  strokeWeight(1);
  stroke(0);
  fill(255);
}

void draw() {
  // Make the parent shape
  PShape alien = createShape(GROUP);

  // Make two shapes
  PShape head = createShape(ELLIPSE, 125, 50, 75, 75);
  PShape body = createShape(RECT, 100, 50, 50, 100);

  // Add the two "child" shapes to the parent group
  alien.addChild(head);
  alien.addChild(body);

  PShape newAlien = PGS_Processing.removeHiddenLines(alien);
 
  // the above operation results in a wide pink stroke
  // which can be set like below. Is there a better way?
  
  for(int i=0;i< newAlien.getChildren().length; i++){
    newAlien.getChild(i).setStroke(0);
    newAlien.getChild(i).setStrokeWeight(1);
  }
  
  shape(newAlien);

  endRecord();
}

Also, there’s just this bit of overlapping strokes.

Again, thank you.

Alright, I did find this to work!
newAlien.disableStyle();

I was under the impression the LineDissolver used within the method would handle this, but perhaps it only dissolves lines having the same endpoints.

For now you can call PGS_ShapeBoolean.union() on the output and that merges those overlapping bits.

You can also use PGS_Conversion.setAllStrokeColor(shape, color, strokeWeight) to apply stroke styling to all child shapes in one line.

shape(alien);
newAlien = PGS_Transformation.translate(newAlien, 100, 0);
shape(newAlien);
newAlien = PGS_Transformation.translate(newAlien, 100, 0);
shape(PGS_Conversion.setAllStrokeColor(PGS_ShapeBoolean.union(newAlien), color(237, 50, 162,128), 8));

image

2 Likes