Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[orx-g-code] Add g-code tools for pen plotting #285

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions orx-g-code/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# orx-g-code

Utilities for generating *g-code* from compositions.

>*Make sure to verify compatibility to your hardware and that the
commands do not exceed the machines limits before running any output of this.*

## Generator

A [Generator](src/commonMain/kotlin/Generator.kt) generates g-code for the following operations:
- `setup`: Setup code at the beginning of a file.
- `moveTo`: A move operation to the given location.
- `preDraw`: Start drawing sequence. Pen down, laser on, etc.
- `drawTo`: A draw operation to the given location.
- `postDraw`: End draw sequence. Lift pen, turn laser off, etc.
- `end`: End of file sequence.
- `comment`: Insert a comment.

These are used by the `toCommands()` extensions in [shape.kt](src/commonMain/kotlin/extensions/shape.kt) to convert a composition
to a set of Commands.

It only supports absolute moves, so `G90 absolute positioning` should be included in the setup.

`basicGrblSetup` defines an example that outputs g-code, which *should be*
compatible with [grbl v1.1](https://github.com/grbl/grbl).

See [DemoGcodeGenerator.kt](src/jvmDemo/kotlin/DemoGcodeGenerator.kt) for an example of how to use.

## Plot (jvm only)

The [Plot Extension](src/jvmMain/kotlin/Plot.kt) provides a quick setup for drawing, rendering and exporting files for
a pen plotter.
See [DemoSimplePlot.kt](src/jvmDemo/kotlin/DemoSimplePlot.kt) for an example of how to use.


<!-- __demos__ -->
## Demos
### DemoInteractivePlot
[source code](src/jvmDemo/kotlin/DemoInteractivePlot.kt)

![DemoInteractivePlotKt](https://raw.githubusercontent.com/openrndr/orx/media/orx-g-code/images/DemoInteractivePlotKt.png)

### DemoSimplePlot
[source code](src/jvmDemo/kotlin/DemoSimplePlot.kt)

![DemoSimplePlotKt](https://raw.githubusercontent.com/openrndr/orx/media/orx-g-code/images/DemoSimplePlotKt.png)
41 changes: 41 additions & 0 deletions orx-g-code/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import ScreenshotsHelper.collectScreenshots

@Suppress("DSL_SCOPE_VIOLATION")
plugins {
org.openrndr.extra.convention.`kotlin-multiplatform`
}

kotlin {
jvm {
@Suppress("UNUSED_VARIABLE")
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}

sourceSets {
@Suppress("UNUSED_VARIABLE")
val commonMain by getting {
dependencies {
implementation(libs.openrndr.application)
implementation(libs.openrndr.draw)
implementation(libs.openrndr.shape)
implementation(libs.openrndr.extensions)
}
}

@Suppress("UNUSED_VARIABLE")
val jvmTest by getting {
dependencies {
runtimeOnly(libs.slf4j.simple)
}
}

@Suppress("UNUSED_VARIABLE")
val jvmDemo by getting {
dependencies {
runtimeOnly(libs.slf4j.simple)
}
}
}
}
Binary file added orx-g-code/images/DemoInteractivePlotKt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added orx-g-code/images/DemoSimplePlotKt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
129 changes: 129 additions & 0 deletions orx-g-code/src/commonMain/kotlin/Generator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package org.openrndr.extra.gcode

import org.openrndr.math.Vector2
import kotlin.math.absoluteValue
import kotlin.math.pow
import kotlin.math.roundToInt

typealias Command = String
typealias Commands = List<Command>

fun Command.asCommands(): Commands = listOf(this)

fun Commands.toGcode(): String = StringBuilder()
.also { sb -> this.forEach { sb.appendLine(it) } }
.toString()

/**
* Consecutive identical commands are removed, ignoring comments.
* Comments are lines starting with ";" or "(".
*/
fun Commands.withoutDuplicates(): Commands {
var lastCommand = 0
return this.filterIndexed { i, c ->
when (i) {
0 -> true
else -> {
if (c.startsWith(';') || c.startsWith('(')) {
true
} else {
!c.contentEquals(this[lastCommand]).also { lastCommand = i }
}
}
}
}
}

/**
* Double to String rounded to absolute value of [decimals].
* Helper to be used in generator functions.
*/
fun Double.roundedTo(decimals: Int = 3): String {
val f = 10.0.pow(decimals.absoluteValue)
return when {
decimals != 0 -> "${this.times(f).roundToInt().div(f)}"
else -> "${this.roundToInt()}"
}
}

/**
* Generates g-code for defined operations.
*/
data class Generator(

/**
* Setup code at the beginning of a file.
*/
val setup: Commands,

/**
* A move operation to the given location.
*/
val moveTo: (Vector2) -> Commands,

/**
* Start drawing sequence. Pen down, laser on, etc.
*/
val preDraw: Commands,

/**
* A draw operation to the given location.
*/
val drawTo: (Vector2) -> Commands,

/**
* End draw sequence. Lift pen, turn laser off, etc.
* Called after a draw action before a move is performed.
*/
val postDraw: Commands,

/**
* End of file sequence.
*/
val end: Commands = emptyList(),

/**
* Insert a comment.
*/
val comment: (String) -> Commands
)

/**
* All operations are empty command lists on default and have to be defined explicitly.
*/
fun noopGenerator() = Generator(
setup = emptyList(),
moveTo = { _ -> emptyList() },
preDraw = emptyList(),
drawTo = { _ -> emptyList() },
postDraw = emptyList(),
) { _ -> emptyList() }

/**
* Creates a [Generator] to be used by grbl controlled pen plotters.
* [drawRate] sets the feed rate used for drawing operations.
* Moves are performed with G0. When [moveRate] is set, moves are instead
* done with G1 and the given rate as feedRate.
* Can be customized by overwriting individual fields with *.copy*.
*/
fun basicGrblSetup(
drawRate: Double = 500.0,
moveRate: Double? = null,
) = Generator(
setup = listOf(
"G21", // mm
"G90", // Absolute positioning
),
moveTo = when (moveRate) {
null -> { p -> "G0 X${p.x.roundedTo()} Y${p.y.roundedTo()}".asCommands() }
else -> { p -> "G1 X${p.x.roundedTo()} Y${p.y.roundedTo()} F$drawRate".asCommands() }
},
preDraw = listOf("M3 S255"),
drawTo = { p -> "G1 X${p.x.roundedTo()} Y${p.y.roundedTo()} F$drawRate".asCommands() },
postDraw = listOf("M3 S0"),
end = listOf(
"G0 X0 Y0",
"G90",
),
comment = { c -> listOf(";$c") }
)
75 changes: 75 additions & 0 deletions orx-g-code/src/commonMain/kotlin/extensions/shape.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.openrndr.extra.gcode.extensions

import org.openrndr.extra.gcode.Command
import org.openrndr.extra.gcode.Commands
import org.openrndr.extra.gcode.Generator
import org.openrndr.shape.Composition
import org.openrndr.shape.Segment
import org.openrndr.shape.ShapeContour

fun List<Composition>.toCommands(generator: Generator, distanceTolerance: Double): Commands = this
.flatMap {it.toCommands(generator, distanceTolerance) }
.let { commands -> (generator.setup + commands + generator.end) }

fun Composition.toCommands(generator: Generator, distanceTolerance: Double): Commands {

val sequence = generator.comment("begin composition").toMutableList()

this.findShapes().forEachIndexed { index, shapeNode ->
sequence += generator.comment("begin shape: $index")
shapeNode.shape.contours.forEach {
sequence += it.toCommands(generator, distanceTolerance)
}
sequence += generator.comment("end shape: $index")
}

sequence += generator.comment("end composition")

return sequence
}

fun ShapeContour.toCommands(generator: Generator, distanceTolerance: Double): Commands {

val sequence = mutableListOf<Command>()

val isDot =
this.segments.size == 1 && this.segments.first().start.squaredDistanceTo(this.segments.first().end) < distanceTolerance
if (isDot) {
sequence += generator.moveTo(this.segments.first().start)
sequence += generator.preDraw
sequence += generator.comment("dot")
sequence += generator.postDraw
return sequence
}

this.segments.forEachIndexed { i, segment ->

// On first segment, move to beginning of segment and tool down
if (i == 0) {
sequence += generator.moveTo(segment.start)
sequence += generator.preDraw
}

// Draw segment
sequence += segment.toCommands(generator, distanceTolerance)
}

// Close after last segment
if (this.closed) {
generator.drawTo(this.segments.first().start)
}

sequence += generator.postDraw

return sequence.toList()
}

fun Segment.toCommands(generator: Generator, distanceTolerance: Double): Commands {
return if (this.control.isEmpty()) {
generator.drawTo(this.end)
} else {
this.adaptivePositions(distanceTolerance).flatMap {
generator.drawTo(it)
}
}
}
64 changes: 64 additions & 0 deletions orx-g-code/src/jvmDemo/kotlin/DemoGcodeGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import org.openrndr.application
import org.openrndr.drawComposition
import org.openrndr.extra.gcode.asCommands
import org.openrndr.extra.gcode.basicGrblSetup
import org.openrndr.extra.gcode.extensions.toCommands
import org.openrndr.extra.gcode.toGcode
import org.openrndr.extra.gcode.withoutDuplicates

fun main() = application {
configure {
width = 800
height = 800
}

program {

val generator = basicGrblSetup(drawRate = 500.0).copy(
preDraw = (40..80 step 5).flatMap { listOf("M3 S$it", "G4 P0.08") },
postDraw = "M3 S40".asCommands(),
)

val comp = drawComposition {
lineSegment(10.0, 10.0, 20.0, 20.0)
}

// Convert composition to g-code, print to console and exit
val commands = generator.setup + comp.toCommands(generator, 0.05) + generator.end
val gCode = commands.withoutDuplicates().toGcode()
println(gCode)
application.exit()

/* Output:
G21
G90
;begin composition
;begin shape: 0
G0 X10.0 Y10.0
M3 S40
G4 P0.08
M3 S45
G4 P0.08
M3 S50
G4 P0.08
M3 S55
G4 P0.08
M3 S60
G4 P0.08
M3 S65
G4 P0.08
M3 S70
G4 P0.08
M3 S75
G4 P0.08
M3 S80
G4 P0.08
G1 X20.0 Y20.0 F500.0
M3 S40
;end shape: 0
;end composition
G0 X0 Y0
G90
*/
}
}
Loading