OPENRNDR plotting tricks (Axidraw etc.)

Thread for useful tricks when working with plotters and OPENRNDR.

1. Boolean operations (clipMode)

A useful operation when generating designs to plot is using boolean operations. Here some examples:

  • ClipMode.REVERSE_DIFFERENCE draws shapes as occluded by (behind) existing content

latest.Dedupe-2021-02-26-18.24.01

imports
import aBeLibs.geometry.dedupe
import org.openrndr.KEY_SPACEBAR
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.dialogs.saveFileDialog
import org.openrndr.shape.ClipMode
import org.openrndr.shape.drawComposition
import org.openrndr.svg.saveToFile
fun main() {
    application {
        program {
            // create design
            val svg = drawComposition {
                fill = null
                circle(width / 2.0 - 100.0, height / 2.0, 100.0)
                circle(width / 2.0 + 100.0, height / 2.0, 100.0)
                clipMode = ClipMode.REVERSE_DIFFERENCE
                circle(width / 2.0, height / 2.0, 100.0)
            }
            extend {
                drawer.clear(ColorRGBa.WHITE)
                // render design
                drawer.composition(svg)
            }
            keyboard.keyDown.listen {
                when (it.key) {
                    KEY_SPACEBAR -> saveFileDialog(supportedExtensions = listOf("svg")) { file ->
                        // save design for plotting
                        svg.saveToFile(file)
                    }
                }
            }
        }
    }
}

  • ClipMode.UNION combines shapes

latest.Dedupe-2021-02-26-18.22.20

val svg = drawComposition {
  fill = null // note that there's no fill!
  circle(width / 2.0 - 100.0, height / 2.0, 100.0)
  clipMode = ClipMode.UNION
  circle(width / 2.0, height / 2.0, 100.0)
  circle(width / 2.0 + 100.0, height / 2.0, 100.0)
}
  • ClipMode.INTERSECTION keeps the areas in which a new shape overlaps with existing ones

latest.Dedupe-2021-02-26-18.23.43

  • ClipMode.DIFFERENCE uses a shape to delete parts of the existing shapes

latest.Dedupe-2021-02-26-18.21.12

2 Likes

2. Deduplicating segments

When plotting the REVERSE_DIFFERENCE example above we might have a small issue: let’s open it in Inkscape and move the shapes a bit:
image

The issue is that we have repeated lines. This is not an issue when showing the design on the screen, but when plotting, some lines are plotted twice. This will make plotting slower and some lines may look darker than others. No big deal with 3 circles, but with complex designs it does make a difference.

To get around this issue I wrote a simple function called .dedupe(). Using it couldn’t be easier: call the method before saving the file like this:

svg.dedupe().saveToFile(file)

Here’s the source code for Composition.dedupe() and for the required Segment.contains():

/**
 * For a Composition, filter out bezier segments contained in longer bezier segments.
 * The goal is to avoid drawing lines multiple times with a plotter.
 */
fun Composition.dedupe(err: Double = 1.0): Composition {
    val segments = this.findShapes().flatMap {
        it.shape.contours.flatMap { contour -> contour.segments }
    }
    val deduped = mutableListOf<Segment>()
    segments.forEach { curr ->
        if (deduped.none { other -> other.contains(curr, err) }) {
            deduped.add(curr)
        }
    }
    return drawComposition {
        contours(deduped.map { it.contour })
    }
}
/**
 * Simple test to see if a segment contains a different Segment.
 * Compares start, end and two points at 1/3 and 2/3.
 * Returns false when comparing a Segment to itself.
 */
fun Segment.contains(other: Segment, error: Double = 0.5): Boolean =
    this !== other &&
            this.on(other.start, error) != null &&
            this.on(other.end, error) != null &&
            this.on(other.position(1.0 / 3), error) != null &&
            this.on(other.position(2.0 / 3), error) != null

It probably doesn’t cover every case but maybe it helps someone?

2 Likes

3. Manipulating SVG files

Instead of writing one program that produces a finished design ready for plotting it can be useful to work in multiple passes. For example, write a program that creates a basic design, then a second one that applies an effect to the output of the first program.

If you have several such effects you can experiment with applying them in different order, or with different parameters, etc.

An example of such an SVG altering effect can look like this:

// load svg
val original = loadSVG("my_generative.svg").findShapes().map { it.shape }

// some kind of contour I will use to cut and bend my svg file
val knife = contour { ... }

// process all the contours from the original file using .split()
val cutLines = original.flatMap {
    it.contours // get contours from shapes
}.flatMap {
    split(it, knife) // For the image below I also apply .bend(), not shown
}

// make a new svg with the modified contours
val modified = drawComposition {
    fill = null
    stroke = ColorRGBa.BLACK
    contours(cutLines)
}

// save result
saveFileDialog(supportedExtensions = listOf("svg")) {
    it.writeText(writeSVG(modified))
}

That’s the approach I used to produce this design:

One program produced the base design, a second one was used for cutting and bending segments.

2 Likes

4. Find the contour-point nearest to another point

2021-03-06-121214_388x352_scrot

One aspect we can consider when generating a design to plot is this: do previously added lines and curves influence the properties of the line or curve I will add next?

Personally I find it most interesting when curves interact with other curves, when lines start or end at other lines, or cross them at specified locations or angles, or when something happens at the intersections of lines.

OPENRNDR offers many tools (methods) to help with this. We can query distances, intersections, normals, we can offset curves, create sub-curves, sample them at various locations and much more.

In this post I will just mention one of such tools: ShapeContour.nearest(point: Vector2). With it we can ask: what point in this contour is the closest to this other point?

import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.shape.Circle
import org.openrndr.shape.LineSegment

fun main() {
    application {
        program {
            // create two nested circles positioned relative to the canvas
            // Circle is a "mathematical circle" (a data type)
           //  `.contour` converts it into a drawable one.
            val c1 = Circle(drawer.bounds.position(0.45, 0.45), 150.0).contour
            val c2 = Circle(drawer.bounds.position(0.55, 0.55), 50.0).contour

            // Create a list with 60 LineSegments. Each one connects
            // a position sampled from the first circle with the nearest 
            // point in the second circle.
            val lineCount = 60
            val lines = List(lineCount) {
                val a = c1.position(it / lineCount.toDouble()) // 0.0 .. 1.0
                val b = c2.nearest(a).position
                LineSegment(a, b)
            }

            extend {
                drawer.apply { // make `this` = drawer
                    clear(ColorRGBa.WHITE)
                    fill = null
                    stroke = ColorRGBa.BLACK
                    contour(c1)
                    contour(c2)
                    lineSegments(lines)
                }
            }
        }
    }
}

Another result using the same approach:
2021-03-06-121824_413x356_scrot

More about Shape, ShapeContour and Segment