New orx-axidraw (#356)
This commit is contained in:
66
orx-jvm/orx-axidraw/README.md
Normal file
66
orx-jvm/orx-axidraw/README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# orx-axidraw
|
||||
|
||||
GUI for configuring and plotting with an Axidraw pen-plotter.
|
||||
|
||||
Uses the [AxiCLI](https://axidraw.com/doc/cli_api/#introduction) command line tool
|
||||
to communicate with the pen plotter.
|
||||
|
||||
Requires: Python 3.8 or higher.
|
||||
|
||||
This orx create a Python virtual environment and downloads AxiCLI automatically.
|
||||
|
||||
## Usage
|
||||
|
||||
```kotlin
|
||||
fun main() = application {
|
||||
program {
|
||||
val axi = Axidraw(this, PaperSize.A5)
|
||||
axi.resizeWindow()
|
||||
|
||||
val gui = WindowedGUI()
|
||||
gui.add(axi)
|
||||
|
||||
axi.draw {
|
||||
fill = null
|
||||
axi.bounds.grid(4, 6).flatten().forEach {
|
||||
circle(it.center, Double.uniform(20.0, 50.0))
|
||||
}
|
||||
}
|
||||
|
||||
extend(gui)
|
||||
extend {
|
||||
drawer.clear(ColorRGBa.WHITE)
|
||||
axi.display(drawer)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Study the inputs available in the GUI. Most are explained in the [AxiCLI](https://axidraw.com/doc/cli_api/#introduction) documentation page.
|
||||
|
||||
### Important
|
||||
|
||||
* Choose the correct pen-plotter model and servo type in the GUI before plotting.
|
||||
* Always make sure the pen is at the home position before starting to plot. If it's not, unpower the steppers,
|
||||
drag the carriage home (near the Axidraw's CPU), then power the steppers back on.
|
||||
|
||||
### Tips
|
||||
|
||||
* One can repeatedly click on `toggle up/down` and adjust `pen pos down` and `pen pos up`
|
||||
to find the ideal heights for the pen.
|
||||
* Enable `fills occlude strokes` and increase margin value to hide elements near
|
||||
the borders of the paper.
|
||||
* Click `save` to save your SVG file.
|
||||
* Click `plot` to plot the visible design using the current settings.
|
||||
* A [2D camera](https://guide.openrndr.org/extensions/camera2D.html) is enabled by default to place your design on the paper.
|
||||
* Click `resume plotting` after pressing the hardware pause button (or including a pause
|
||||
command on a layer) to continue.
|
||||
* To get a plotting time estimate, enable `preview` and click `plot`. Nothing will be plotted, but the estimate will be shown in the IDE console.
|
||||
|
||||
The `Load` and `Save` buttons *at the top of the GUI* can be used to load and save the plotting settings. In a future version we may embed the plotting settings into the SVG file.
|
||||
|
||||
### Multi color plots
|
||||
|
||||
orx-axidraw makes it easy to create multi-pen plots. To do that, use two or more stroke colors in your design. The order of the lines does not matter. Then, before plotting, call `axi.groupStrokeColors()`. This will group curves into layers based on their stroke colors and insert a pause between layers, allowing you to change the pen.
|
||||
|
||||
When the plotter pauses during plotting, change the pen and click `resume plotting` to continue.
|
||||
18
orx-jvm/orx-axidraw/build.gradle.kts
Normal file
18
orx-jvm/orx-axidraw/build.gradle.kts
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
org.openrndr.extra.convention.`kotlin-jvm`
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.openrndr.application)
|
||||
implementation(libs.openrndr.dialogs)
|
||||
implementation(project(":orx-jvm:orx-gui"))
|
||||
implementation(project(":orx-composition"))
|
||||
implementation(project(":orx-svg"))
|
||||
implementation(project(":orx-image-fit"))
|
||||
implementation(project(":orx-shapes"))
|
||||
implementation(project(":orx-camera"))
|
||||
demoImplementation(project(":orx-camera"))
|
||||
demoImplementation(project(":orx-noise"))
|
||||
demoImplementation(project(":orx-parameters"))
|
||||
demoImplementation(project(":orx-jvm:orx-axidraw"))
|
||||
}
|
||||
59
orx-jvm/orx-axidraw/src/demo/kotlin/DemoAxidraw01.kt
Normal file
59
orx-jvm/orx-axidraw/src/demo/kotlin/DemoAxidraw01.kt
Normal file
@@ -0,0 +1,59 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extra.axidraw.Axidraw
|
||||
import org.openrndr.extra.axidraw.PaperSize
|
||||
import org.openrndr.extra.gui.GUI
|
||||
import org.openrndr.extra.noise.uniform
|
||||
import org.openrndr.extra.parameters.ActionParameter
|
||||
import org.openrndr.extra.parameters.Description
|
||||
import org.openrndr.extra.parameters.IntParameter
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Demonstrates:
|
||||
* - how to create an AxiDraw GUI
|
||||
* - how to add a slider and a button to that GUI
|
||||
* - how to include code to generate new random designs that match
|
||||
* the paper size via `axi.bounds`.
|
||||
* - how to display the generated design using `axi.display`.
|
||||
*
|
||||
* Toggle the GUI by pressing F11.
|
||||
*/
|
||||
fun main() = application {
|
||||
configure {
|
||||
width = PaperSize.A5.size.x * 5
|
||||
height = PaperSize.A5.size.y * 5
|
||||
}
|
||||
program {
|
||||
val axi = Axidraw(this, PaperSize.A5)
|
||||
axi.resizeWindow()
|
||||
|
||||
val gui = GUI()
|
||||
gui.add(axi)
|
||||
|
||||
val settings = @Description("Main") object {
|
||||
@IntParameter("count", 1, 50)
|
||||
var count = 20
|
||||
|
||||
@ActionParameter("generate")
|
||||
fun generate() {
|
||||
axi.clear()
|
||||
axi.draw {
|
||||
val l = min(axi.bounds.width, axi.bounds.height) / 2.0
|
||||
repeat(count) {
|
||||
circle(axi.bounds.center, Double.uniform(l / 4.0, l))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
gui.add(settings)
|
||||
|
||||
settings.generate()
|
||||
|
||||
extend(gui)
|
||||
extend {
|
||||
drawer.clear(ColorRGBa.WHITE)
|
||||
axi.display(drawer)
|
||||
}
|
||||
}
|
||||
}
|
||||
45
orx-jvm/orx-axidraw/src/demo/kotlin/DemoAxidraw02.kt
Normal file
45
orx-jvm/orx-axidraw/src/demo/kotlin/DemoAxidraw02.kt
Normal file
@@ -0,0 +1,45 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extra.axidraw.Axidraw
|
||||
import org.openrndr.extra.axidraw.PaperOrientation
|
||||
import org.openrndr.extra.axidraw.PaperSize
|
||||
import org.openrndr.extra.gui.WindowedGUI
|
||||
import org.openrndr.extra.noise.uniform
|
||||
import org.openrndr.extra.parameters.ActionParameter
|
||||
import org.openrndr.extra.parameters.Description
|
||||
|
||||
/**
|
||||
* Demonstrates:
|
||||
* - How to set the window size based on the chosen paper size.
|
||||
* - How to use a windowed GUI.
|
||||
*
|
||||
*/
|
||||
fun main() = application {
|
||||
program {
|
||||
val axi = Axidraw(this, PaperSize.A5, PaperOrientation.LANDSCAPE)
|
||||
axi.resizeWindow()
|
||||
|
||||
val gui = WindowedGUI()
|
||||
gui.add(axi)
|
||||
|
||||
val settings = @Description("Main") object {
|
||||
|
||||
@ActionParameter("generate")
|
||||
fun generate() {
|
||||
axi.clear()
|
||||
axi.draw {
|
||||
repeat(20) {
|
||||
circle(axi.bounds.center, Double.uniform(50.0, 200.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
gui.add(settings)
|
||||
|
||||
extend(gui)
|
||||
extend {
|
||||
drawer.clear(ColorRGBa.WHITE)
|
||||
axi.display(drawer)
|
||||
}
|
||||
}
|
||||
}
|
||||
44
orx-jvm/orx-axidraw/src/demo/kotlin/DemoAxidraw03.kt
Normal file
44
orx-jvm/orx-axidraw/src/demo/kotlin/DemoAxidraw03.kt
Normal file
@@ -0,0 +1,44 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extra.axidraw.Axidraw
|
||||
import org.openrndr.extra.axidraw.PaperOrientation
|
||||
import org.openrndr.extra.axidraw.PaperSize
|
||||
import org.openrndr.extra.axidraw.configure
|
||||
import org.openrndr.extra.gui.WindowedGUI
|
||||
import org.openrndr.extra.noise.uniform
|
||||
import org.openrndr.extra.shapes.primitives.grid
|
||||
|
||||
/**
|
||||
* Demonstrates:
|
||||
* - How to create layers via `group` and give each layer
|
||||
* a unique pen height and pen speed.
|
||||
*
|
||||
*/
|
||||
fun main() = application {
|
||||
program {
|
||||
val axi = Axidraw(this, PaperSize.A5, PaperOrientation.PORTRAIT)
|
||||
axi.resizeWindow(100.0)
|
||||
|
||||
val gui = WindowedGUI()
|
||||
gui.add(axi)
|
||||
|
||||
axi.clear()
|
||||
axi.draw {
|
||||
fill = null
|
||||
axi.bounds.grid(4, 6).flatten().forEach {
|
||||
group {
|
||||
circle(it.center, 50.0)
|
||||
}.configure(
|
||||
penHeight = Int.uniform(30, 60),
|
||||
penSpeed = Int.uniform(20, 50)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extend(gui)
|
||||
extend {
|
||||
drawer.clear(ColorRGBa.WHITE)
|
||||
axi.display(drawer)
|
||||
}
|
||||
}
|
||||
}
|
||||
48
orx-jvm/orx-axidraw/src/demo/kotlin/DemoAxidraw04.kt
Normal file
48
orx-jvm/orx-axidraw/src/demo/kotlin/DemoAxidraw04.kt
Normal file
@@ -0,0 +1,48 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extra.axidraw.*
|
||||
import org.openrndr.extra.gui.WindowedGUI
|
||||
import org.openrndr.extra.shapes.primitives.grid
|
||||
|
||||
/**
|
||||
* Demonstrates:
|
||||
* - How to create a flattened grid of with 24 items
|
||||
* - How to randomize the order of those items
|
||||
* - How to take chunks of 10 items, then make
|
||||
* a pause to change the pen after plotting each chunk
|
||||
*
|
||||
* Operation: After plotting ten circles, plotting will stop to let you change the pen.
|
||||
* With the second pen installed, click `resume`. It will plot ten circles more.
|
||||
* Change the pen again and click `resume` to plot the remaining 4 circles.
|
||||
* Once done, click `resume` one more time to bring the pen home.
|
||||
*/
|
||||
fun main() = application {
|
||||
program {
|
||||
val axi = Axidraw(this, PaperSize.A5, PaperOrientation.PORTRAIT)
|
||||
axi.resizeWindow(100.0)
|
||||
|
||||
val gui = WindowedGUI()
|
||||
gui.add(axi)
|
||||
|
||||
axi.clear()
|
||||
axi.draw {
|
||||
fill = null
|
||||
axi.bounds.grid(4, 6).flatten()
|
||||
.shuffled().chunked(10).forEach { chunk ->
|
||||
group {
|
||||
chunk.forEach {
|
||||
circle(it.center, 50.0)
|
||||
}
|
||||
}
|
||||
group {
|
||||
}.configure(layerMode = AxiLayerMode.PAUSE)
|
||||
}
|
||||
}
|
||||
|
||||
extend(gui)
|
||||
extend {
|
||||
drawer.clear(ColorRGBa.WHITE)
|
||||
axi.display(drawer)
|
||||
}
|
||||
}
|
||||
}
|
||||
46
orx-jvm/orx-axidraw/src/demo/kotlin/DemoAxidraw05.kt
Normal file
46
orx-jvm/orx-axidraw/src/demo/kotlin/DemoAxidraw05.kt
Normal file
@@ -0,0 +1,46 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extra.axidraw.Axidraw
|
||||
import org.openrndr.extra.axidraw.PaperOrientation
|
||||
import org.openrndr.extra.axidraw.PaperSize
|
||||
import org.openrndr.extra.gui.WindowedGUI
|
||||
import org.openrndr.extra.shapes.primitives.grid
|
||||
|
||||
/**
|
||||
* Demonstrates:
|
||||
* - How to create a flattened grid of with 24 items
|
||||
* - How to apply random colors from a palette to each item.
|
||||
* - How to use `groupStrokeColors()` to plot a multi-pen design.
|
||||
*
|
||||
*/
|
||||
fun main() = application {
|
||||
program {
|
||||
val axi = Axidraw(this, PaperSize.A5, PaperOrientation.PORTRAIT)
|
||||
axi.resizeWindow(100.0)
|
||||
|
||||
val gui = WindowedGUI()
|
||||
gui.add(axi)
|
||||
|
||||
val palette = listOf(
|
||||
ColorRGBa.RED,
|
||||
ColorRGBa.GREEN,
|
||||
ColorRGBa.BLUE
|
||||
)
|
||||
|
||||
axi.clear()
|
||||
axi.draw {
|
||||
fill = null
|
||||
axi.bounds.grid(4, 6).flatten().forEach {
|
||||
stroke = palette.random()
|
||||
circle(it.center, 50.0)
|
||||
}
|
||||
}
|
||||
axi.groupStrokeColors()
|
||||
|
||||
extend(gui)
|
||||
extend {
|
||||
drawer.clear(ColorRGBa.WHITE)
|
||||
axi.display(drawer)
|
||||
}
|
||||
}
|
||||
}
|
||||
506
orx-jvm/orx-axidraw/src/main/kotlin/Axidraw.kt
Normal file
506
orx-jvm/orx-axidraw/src/main/kotlin/Axidraw.kt
Normal file
@@ -0,0 +1,506 @@
|
||||
package org.openrndr.extra.axidraw
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import offset.offset
|
||||
import org.openrndr.Program
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.dialogs.openFileDialog
|
||||
import org.openrndr.dialogs.saveFileDialog
|
||||
import org.openrndr.draw.Drawer
|
||||
import org.openrndr.draw.isolated
|
||||
import org.openrndr.extra.camera.Camera2D
|
||||
import org.openrndr.extra.composition.*
|
||||
import org.openrndr.extra.imageFit.fit
|
||||
import org.openrndr.extra.parameters.*
|
||||
import org.openrndr.extra.svg.loadSVG
|
||||
import org.openrndr.extra.svg.toSVG
|
||||
import org.openrndr.math.IntVector2
|
||||
import org.openrndr.math.Matrix44
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.shape.IntRectangle
|
||||
import org.openrndr.shape.SegmentJoin
|
||||
import org.openrndr.shape.Shape
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.io.path.createTempFile
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
/**
|
||||
* Axidraw reordering optimization types.
|
||||
* See: https://axidraw.com/doc/cli_api/#reordering
|
||||
*/
|
||||
@Suppress("unused")
|
||||
enum class AxidrawOptimizationTypes(val id: Int) {
|
||||
/**
|
||||
* No optimization. Strictly preserve file order.
|
||||
*/
|
||||
None(4),
|
||||
|
||||
/**
|
||||
* Least; Only connect adjoining paths.
|
||||
*/
|
||||
ConnectPaths(0),
|
||||
|
||||
/**
|
||||
* Basic; Also reorder paths for speed
|
||||
*/
|
||||
ReorderPaths(1),
|
||||
|
||||
/**
|
||||
* Full; Also allow path reversal
|
||||
*/
|
||||
ReversePaths(2)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
enum class AxidrawModel(val id: Int) {
|
||||
AxiDrawV2(1),
|
||||
AxidrawV3(1),
|
||||
AxidrawSE_A4(1),
|
||||
AxiDrawV3_A3(2),
|
||||
AxidrawSE_A3(2),
|
||||
AxiDrawV3_XLX(3),
|
||||
AxiDrawMiniKit(4),
|
||||
AxiDrawSE_A1(5),
|
||||
AxiDrawSE_A2(6),
|
||||
AxiDrawV3_B6(7),
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
enum class AxidrawServo(val id: Int) {
|
||||
Standard(2),
|
||||
Brushless(3),
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
enum class PaperSize(val size: IntVector2) {
|
||||
`A-1`(IntVector2(1682, 2378)),
|
||||
`A-2`(IntVector2(1189, 1682)),
|
||||
A0(IntVector2(841, 1189)),
|
||||
A1(IntVector2(594, 841)),
|
||||
A2(IntVector2(420, 594)),
|
||||
A3(IntVector2(297, 420)),
|
||||
A4(IntVector2(210, 297)),
|
||||
A5(IntVector2(148, 210)),
|
||||
A6(IntVector2(105, 148)),
|
||||
A7(IntVector2(74, 105)),
|
||||
A8(IntVector2(52, 74)),
|
||||
A9(IntVector2(37, 52)),
|
||||
A10(IntVector2(26, 37))
|
||||
}
|
||||
|
||||
enum class PaperOrientation {
|
||||
LANDSCAPE,
|
||||
PORTRAIT
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to talk to the axicli command line program
|
||||
*
|
||||
*/
|
||||
@Description("Axidraw")
|
||||
class Axidraw(val program: Program, paperSize: PaperSize, orientation: PaperOrientation = PaperOrientation.PORTRAIT) {
|
||||
|
||||
fun setupAxidrawCli() {
|
||||
|
||||
if (!File("axidraw-venv").exists()) {
|
||||
logger.info { "installing axidraw-cli virtual environment" }
|
||||
invokePython(listOf("-m", "venv", "axidraw-venv"))
|
||||
}
|
||||
val python = venvPython(File("axidraw-venv"))
|
||||
logger.info { "installing axidraw-cli in virtual environment $python" }
|
||||
invokePython(
|
||||
listOf("-m", "pip", "install", "https://cdn.evilmadscientist.com/dl/ad/public/AxiDraw_API.zip"),
|
||||
python
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
setupAxidrawCli()
|
||||
}
|
||||
|
||||
val actualPaperSize = when (orientation) {
|
||||
PaperOrientation.LANDSCAPE -> paperSize.size.yx.vector2
|
||||
PaperOrientation.PORTRAIT -> paperSize.size.vector2
|
||||
}
|
||||
|
||||
/**
|
||||
* API URL to call once plotting is complete. If the string contains
|
||||
* `[filename]` it will be replaced by the name of the file being plotted.
|
||||
* This URL should be URL encoded (for instance use %20 instead of a space).
|
||||
*/
|
||||
var apiURL = ""
|
||||
|
||||
@OptionParameter("model", 50)
|
||||
var model = AxidrawModel.AxiDrawV3_A3
|
||||
|
||||
@OptionParameter("servo", 60)
|
||||
var servo = AxidrawServo.Standard
|
||||
|
||||
@IntParameter("speed pen down", 1, 110, 100)
|
||||
var speedPenDown = 25
|
||||
|
||||
@IntParameter("speed pen up", 1, 110, 110)
|
||||
var speedPenUp = 70
|
||||
|
||||
@IntParameter("acceleration", 1, 100, 120)
|
||||
var acceleration = 75
|
||||
|
||||
/**
|
||||
* Toggle the pen up/down state by powering the pen plotter servo.
|
||||
* Useful for calibrating the pen height. Cover the paper with a
|
||||
* plastic sheet before running this command to avoid accidentally
|
||||
* leaving ink on the paper.
|
||||
*/
|
||||
@ActionParameter("toggle up/down", 125)
|
||||
fun toggleUpDown() {
|
||||
runCMD(
|
||||
listOf(
|
||||
"--mode", "toggle",
|
||||
"--penlift", servo.id.toString(),
|
||||
"--model", model.id.toString(),
|
||||
"--pen_pos_down", "$penPosDown",
|
||||
"--pen_pos_up", "$penPosUp",
|
||||
), false
|
||||
)
|
||||
}
|
||||
|
||||
@IntParameter("pen pos down", 1, 100, 130)
|
||||
var penPosDown = 40
|
||||
|
||||
@IntParameter("pen pos up", 1, 100, 140)
|
||||
var penPosUp = 60
|
||||
|
||||
@IntParameter("pen rate lower", 1, 100, 150)
|
||||
var penRateLower = 50
|
||||
|
||||
@IntParameter("pen rate raise", 1, 100, 160)
|
||||
var penRateRaise = 75
|
||||
|
||||
@IntParameter("pen delay down", -500, 500, 170)
|
||||
var penDelayDown = 0
|
||||
|
||||
@IntParameter("pen delay up", -500, 500, 180)
|
||||
var penDelayUp = 0
|
||||
|
||||
@OptionParameter("optimization", 185)
|
||||
var optimization = AxidrawOptimizationTypes.ConnectPaths
|
||||
|
||||
@BooleanParameter("random start", 190)
|
||||
var randomStart = false
|
||||
|
||||
@BooleanParameter("fills occlude strokes", 200)
|
||||
var occlusion = false
|
||||
|
||||
@IntParameter("margin", 0, 100, 205)
|
||||
var margin = 0
|
||||
|
||||
@BooleanParameter("preview", 210)
|
||||
var preview = false
|
||||
|
||||
@BooleanParameter("const speed", 220)
|
||||
var constSpeed = false
|
||||
|
||||
@BooleanParameter("webhook", 230)
|
||||
var webhook = false
|
||||
|
||||
/**
|
||||
* Creates a temporary SVG file. Used by the AxiCLI "resume" methods. When plotting,
|
||||
* the temporary SVG file is updated to keep track of progress and allow resuming.
|
||||
*/
|
||||
private fun makeTempSVGFile(): File {
|
||||
val tmpFile = createTempFile("axi_${UUID.randomUUID()}", ".svg").toFile()
|
||||
tmpFile.deleteOnExit()
|
||||
return tmpFile
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps track of the most recent output file. Used to resume plotting after a pause.
|
||||
*/
|
||||
private var lastOutputFile = makeTempSVGFile()
|
||||
|
||||
private fun plotArgs(plotFile: File, outputFile: File): List<String> {
|
||||
lastOutputFile = outputFile
|
||||
return listOf(
|
||||
plotFile.absolutePath,
|
||||
"--progress",
|
||||
"--report_time",
|
||||
"--reordering", optimization.id.toString(),
|
||||
if (randomStart) "--random_start" else "",
|
||||
if (occlusion) "--hiding" else "",
|
||||
if (preview) "--preview" else "",
|
||||
if (webhook && apiURL.isNotEmpty())
|
||||
"--webhook" else "",
|
||||
if (webhook && apiURL.isNotEmpty())
|
||||
"--webhook_url ${apiURL.replace("[filename]", plotFile.name)}" else "",
|
||||
"--speed_pendown", "$speedPenDown",
|
||||
"--speed_penup", "$speedPenUp",
|
||||
"--accel", "$acceleration",
|
||||
if (constSpeed) "--const_speed" else "",
|
||||
"--pen_pos_down", "$penPosDown",
|
||||
"--pen_pos_up", "$penPosUp",
|
||||
"--pen_rate_lower", "$penRateLower",
|
||||
"--pen_rate_raise", "$penRateRaise",
|
||||
"--pen_delay_down", "$penDelayDown",
|
||||
"--pen_delay_up", "$penDelayUp",
|
||||
"--penlift", servo.id.toString(),
|
||||
"--model", model.id.toString(),
|
||||
"--output_file", outputFile.absolutePath,
|
||||
).filter { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
private fun compositionDimensions(): CompositionDimensions {
|
||||
return CompositionDimensions(
|
||||
0.0.pixels,
|
||||
0.0.pixels,
|
||||
Length.Pixels.fromMillimeters(actualPaperSize.x),
|
||||
Length.Pixels.fromMillimeters(actualPaperSize.y)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Main variable holding the design to save or plot.
|
||||
*/
|
||||
private val design = drawComposition(compositionDimensions()) { }
|
||||
|
||||
/**
|
||||
* Returns the bounds of the drawable area so user code can draw things
|
||||
* whithout leaving the paper.
|
||||
*/
|
||||
val bounds = IntRectangle(
|
||||
0, 0,
|
||||
(96.0 * actualPaperSize.x / 25.4).toInt(),
|
||||
(96.0 * actualPaperSize.y / 25.4).toInt()
|
||||
).rectangle
|
||||
|
||||
/**
|
||||
* Clears the current design wiping any shapes the user might have
|
||||
* added.
|
||||
*
|
||||
*/
|
||||
fun clear() = design.clear()
|
||||
|
||||
/**
|
||||
* The core method that allows the user to append content to the design.
|
||||
* Use any methods and properties like contour(), segment(), fill, stroke, etc.
|
||||
*/
|
||||
fun draw(f: CompositionDrawer.() -> Unit) {
|
||||
design.draw(f)
|
||||
}
|
||||
|
||||
private fun runCMD(args: List<String>, hold: Boolean = true) {
|
||||
val python = venvPython(File("axidraw-venv"))
|
||||
invokePython(listOf("-m", "axicli") + args, python)
|
||||
}
|
||||
|
||||
/**
|
||||
* Display Axidraw software version
|
||||
*/
|
||||
@ActionParameter("info: version", 300)
|
||||
fun version() = runCMD(listOf("--mode", "version"))
|
||||
|
||||
/**
|
||||
* Display Axidraw system info
|
||||
*/
|
||||
@ActionParameter("info: system", 310)
|
||||
fun sysInfo() = runCMD(listOf("--mode", "sysinfo"))
|
||||
|
||||
@ActionParameter("load", 330)
|
||||
fun onLoad() = openFileDialog(supportedExtensions = listOf("SVG" to listOf("svg"))) {
|
||||
clear()
|
||||
camera.view = Matrix44.IDENTITY
|
||||
val loaded = loadSVG(it)
|
||||
draw {
|
||||
loaded.findGroups().forEach { gn ->
|
||||
if (gn.findGroups().size == 1) {
|
||||
val g = group {
|
||||
gn.findShapes().forEach { shp ->
|
||||
if (shp.attributes["type"] != "margin") {
|
||||
stroke = shp.stroke
|
||||
fill = shp.fill
|
||||
shape(shp.shape)
|
||||
}
|
||||
}
|
||||
}
|
||||
g.attributes.putAll(gn.attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save current design as SVG
|
||||
*/
|
||||
@ActionParameter("save", 340)
|
||||
fun onSave() = saveFileDialog(supportedExtensions = listOf("SVG" to listOf("svg"))) { save(it) }
|
||||
|
||||
private fun save(svgFile: File) {
|
||||
// Create a new SVG with the frame and camera applied
|
||||
val designRendered = drawComposition(compositionDimensions()) {
|
||||
val m = camera.view
|
||||
|
||||
design.findGroups().forEach { gn ->
|
||||
if (gn.findGroups().size == 1) {
|
||||
val g = group {
|
||||
gn.findShapes().forEach { shp ->
|
||||
stroke = shp.stroke
|
||||
fill = shp.fill
|
||||
shape(shp.shape.transform(m))
|
||||
}
|
||||
}
|
||||
g.attributes.putAll(gn.attributes)
|
||||
}
|
||||
}
|
||||
|
||||
// If the user wants a frame covering the design...
|
||||
if (occlusion) {
|
||||
fill = ColorRGBa.WHITE
|
||||
stroke = null
|
||||
shape(makeFrame(margin.toDouble()))?.attributes?.put("type", "margin")
|
||||
}
|
||||
}
|
||||
designRendered.saveToInkscapeFile(svgFile)
|
||||
}
|
||||
|
||||
/**
|
||||
* Plot design using the current settings
|
||||
*/
|
||||
@ActionParameter("plot", 350)
|
||||
fun onPlot() {
|
||||
val svgFile = makeTempSVGFile()
|
||||
save(svgFile)
|
||||
runCMD(plotArgs(svgFile, makeTempSVGFile()))
|
||||
}
|
||||
|
||||
/**
|
||||
* After hitting pause, use this to move the pen home
|
||||
*/
|
||||
@ActionParameter("resume to home", 360)
|
||||
fun goHome() {
|
||||
runCMD(plotArgs(lastOutputFile, makeTempSVGFile()) + listOf("--mode", "res_home"))
|
||||
}
|
||||
|
||||
/**
|
||||
* After hitting pause, use this to continue plotting
|
||||
*
|
||||
*/
|
||||
@ActionParameter("resume plotting", 370)
|
||||
fun resume() {
|
||||
runCMD(plotArgs(lastOutputFile, makeTempSVGFile()) + listOf("--mode", "res_plot"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimization. This can be applied to a lambda function that takes one argument
|
||||
* so it caches the calculation while the argument does not change.
|
||||
*/
|
||||
private fun <A, B> ((A) -> B).lastArgMemo(): (A) -> B {
|
||||
var lastArg: A? = null
|
||||
var lastResult: B? = null
|
||||
|
||||
return { arg ->
|
||||
if (arg == lastArg) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
lastResult as B
|
||||
} else {
|
||||
val result = this(arg)
|
||||
lastArg = arg
|
||||
lastResult = result
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a white frame to cover the borders of the page, to avoid plotting
|
||||
* on the edge of papers, which may damage the pen or make a mess.
|
||||
*/
|
||||
private val makeFrame = { width: Double ->
|
||||
Shape(
|
||||
listOf(
|
||||
bounds.contour.offset(1000.0, SegmentJoin.MITER),
|
||||
bounds.contour.offset(-width).reversed
|
||||
)
|
||||
)
|
||||
}.lastArgMemo()
|
||||
|
||||
/**
|
||||
* Display the composition using [drawer].
|
||||
*/
|
||||
fun display(drawer: Drawer) {
|
||||
drawer.isolated {
|
||||
view *= bounds.fit(drawer.bounds)
|
||||
|
||||
isolated {
|
||||
view *= camera.view
|
||||
composition(design)
|
||||
}
|
||||
|
||||
// Draw frame
|
||||
if (occlusion) {
|
||||
fill = ColorRGBa.WHITE
|
||||
stroke = null
|
||||
shape(makeFrame(margin.toDouble()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the program window to match
|
||||
* the paper size according to the
|
||||
* [ppi] (Pixels Per Inch) value.
|
||||
*/
|
||||
fun resizeWindow(ppi: Double = 96.0) {
|
||||
val app = program.application
|
||||
val resizable = app.windowResizable
|
||||
app.windowResizable = true
|
||||
app.windowSize = Vector2(
|
||||
ppi * actualPaperSize.x / 25.4,
|
||||
ppi * actualPaperSize.y / 25.4
|
||||
)
|
||||
app.windowResizable = resizable
|
||||
}
|
||||
|
||||
val camera by lazy {
|
||||
Camera2D().also {
|
||||
it.setup(program)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds the design putting shapes under groups based on stroke colors and inserts a pause
|
||||
* after each group.
|
||||
*
|
||||
* Call this method after creating a draw composition that uses several stroke colors.
|
||||
* When plotting, change pens after each pause, then click "resume plotting".
|
||||
*
|
||||
* NOTE: this method changes line order. Therefore, avoid it if order is important,
|
||||
* for instance with designs using fill colors to occlude.
|
||||
*
|
||||
*/
|
||||
fun groupStrokeColors() {
|
||||
val colorGroups = design.findShapes().filter { it.stroke != null }.groupBy { it.stroke!! }
|
||||
design.clear()
|
||||
design.draw {
|
||||
var i = 0
|
||||
colorGroups.forEach { (color, nodes) ->
|
||||
val hexColor = "%06x".format(
|
||||
((color.r * 255).toInt() shl 16) + ((color.g * 255).toInt() shl 8) + ((color.b * 255).toInt())
|
||||
)
|
||||
group { cursor.children.addAll(nodes) }.configure(hexColor)
|
||||
|
||||
// Add a pause if it's not the last layer
|
||||
if(++i < colorGroups.size) {
|
||||
group { }.configure(layerMode = AxiLayerMode.PAUSE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only String variable to inspect the current design in SVG format for debugging purposes.
|
||||
*/
|
||||
var svg: String = ""
|
||||
get() = design.toSVG()
|
||||
private set
|
||||
}
|
||||
122
orx-jvm/orx-axidraw/src/main/kotlin/SVG.kt
Normal file
122
orx-jvm/orx-axidraw/src/main/kotlin/SVG.kt
Normal file
@@ -0,0 +1,122 @@
|
||||
package org.openrndr.extra.axidraw
|
||||
import org.openrndr.extra.composition.Composition
|
||||
import org.openrndr.extra.composition.GroupNode
|
||||
import org.openrndr.extra.composition.findGroups
|
||||
import org.openrndr.extra.svg.toSVG
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Axidraw layer mode. The [command] argument will be prepended to the layer name.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
enum class AxiLayerMode(val command: String) {
|
||||
/**
|
||||
* The default mode prepends nothing.
|
||||
*/
|
||||
DEFAULT(""),
|
||||
|
||||
/**
|
||||
* Layer names starting with `%` are not plotted.
|
||||
*/
|
||||
IGNORE("%"),
|
||||
|
||||
/**
|
||||
* Layer names starting with `!` trigger a pause.
|
||||
*/
|
||||
PAUSE("!")
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure an SVG layer name. Certain character sequences are used
|
||||
* by the Axidraw software to control layer speed, height and delay.
|
||||
* Other characters make the layer be ignored, or trigger a pause.
|
||||
* The arguments in this function provide a typed approach to construct
|
||||
* the layer name.
|
||||
* See https://wiki.evilmadscientist.com/AxiDraw_Layer_Control
|
||||
*
|
||||
* @param layerName Human-readable layer name. Multiple layer can use the same name.
|
||||
* @param penSpeed Pen down speed (1..100)
|
||||
* @param penHeight Pen down height (0..100)
|
||||
* @param plotDelay Delay before plotting this layer, in milliseconds
|
||||
* @param layerMode The plotting mode for this layer. See [AxiLayerMode].
|
||||
*/
|
||||
fun GroupNode.configure(
|
||||
layerName: String = "layer",
|
||||
penSpeed: Int? = null,
|
||||
penHeight: Int? = null,
|
||||
plotDelay: Int? = null,
|
||||
layerMode: AxiLayerMode = AxiLayerMode.DEFAULT
|
||||
) {
|
||||
val layerNumber = (parent?.findGroups()?.size ?: 2) - 1
|
||||
|
||||
require(penSpeed == null || penSpeed in 1..100) { "Speed out of 1 .. 100 range" }
|
||||
val actualSpeed = penSpeed?.let { "+S$it" } ?: ""
|
||||
|
||||
require(penHeight == null || penHeight in 0..100) { "Height out of 0 .. 100 range" }
|
||||
val actualHeight = penHeight?.let { "+H$it" } ?: ""
|
||||
|
||||
require(plotDelay == null || plotDelay > 0) { "Delay value should null or above 0" }
|
||||
val actualDelay = plotDelay?.let { "+D$it" } ?: ""
|
||||
|
||||
attributes["inkscape:groupmode"] = "layer"
|
||||
|
||||
attributes["inkscape:label"] = layerMode.command + layerNumber +
|
||||
actualSpeed + actualHeight + actualDelay + " " + layerName
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a [Composition] to an Inkscape file. Includes expected XML namespaces
|
||||
* and sets an XML header with the view window size. Strips an extra wrapping `<g>` tag to
|
||||
* make special layer names work with the Axidraw pen plotter.
|
||||
*
|
||||
* @param file Should point to the desired file name and path.
|
||||
* @param postProcess Optional function to do post-processing on the SVG XML before saving it.
|
||||
*/
|
||||
fun Composition.saveToInkscapeFile(
|
||||
file: File,
|
||||
postProcess: (String) -> String = { xml -> xml }
|
||||
) {
|
||||
namespaces["xmlns:inkscape"] = "http://www.inkscape.org/namespaces/inkscape"
|
||||
namespaces["xmlns:sodipodi"] = "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
namespaces["xmlns:svg"] = "http://www.w3.org/2000/svg"
|
||||
|
||||
val svg = StringBuilder(toSVG())
|
||||
|
||||
val header = """
|
||||
<sodipodi:namedview
|
||||
id="namedview7112"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#eeeeee"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.0"
|
||||
inkscape:cx="${bounds.width.value / 2}"
|
||||
inkscape:cy="${bounds.height.value / 2}"
|
||||
inkscape:window-width="${bounds.width}"
|
||||
inkscape:window-height="${bounds.height}"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="openrndr-svg" />
|
||||
""".trimIndent()
|
||||
|
||||
// Remove the wrapping <g>, otherwise layers don't work.
|
||||
// Also remove duplicated <g><g> and </g></g> which show up when
|
||||
// drawing a composition into another composition.
|
||||
val updated = svg.replace(
|
||||
Regex("""(<g\s?>(.*)</g>)""", RegexOption.DOT_MATCHES_ALL), "$2"
|
||||
).replace(
|
||||
"(<g >\\W?)+<g ".toRegex(setOf(RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL)),
|
||||
"<g "
|
||||
).replace(
|
||||
"(\\W?</g>)+".toRegex(setOf(RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL)),
|
||||
"\n</g>"
|
||||
).replace(
|
||||
Regex("""(<svg.*?>)""", RegexOption.DOT_MATCHES_ALL), "$1$header"
|
||||
)
|
||||
file.writeText(postProcess(updated))
|
||||
}
|
||||
71
orx-jvm/orx-axidraw/src/main/kotlin/python.kt
Normal file
71
orx-jvm/orx-axidraw/src/main/kotlin/python.kt
Normal file
@@ -0,0 +1,71 @@
|
||||
package org.openrndr.extra.axidraw
|
||||
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Determines the appropriate Python executable name based on the operating system.
|
||||
*
|
||||
* On Windows systems, it returns "python.exe", while on other operating systems, it returns "python3".
|
||||
*
|
||||
* @return The name of the Python executable appropriate for the current operating system.
|
||||
*/
|
||||
fun systemPython(): String {
|
||||
val executable = if (System.getProperty("os.name").lowercase().contains("windows")) {
|
||||
"python.exe"
|
||||
} else {
|
||||
"python3"
|
||||
}
|
||||
return executable
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the Python executable in a given virtual environment.
|
||||
* The path varies depending on the operating system.
|
||||
*
|
||||
* @param venv the directory of the virtual environment
|
||||
* @return the absolute path to the Python executable within the virtual environment
|
||||
*/
|
||||
fun venvPython(venv: File): String {
|
||||
val executable = if (System.getProperty("os.name").lowercase().contains("windows")) {
|
||||
"${venv.absolutePath}/Scripts/python.exe"
|
||||
} else {
|
||||
"${venv.absolutePath}/bin/python"
|
||||
}
|
||||
return executable
|
||||
}
|
||||
|
||||
|
||||
fun invokePython(arguments: List<String>, executable: String = systemPython()): String {
|
||||
val result: String
|
||||
try {
|
||||
|
||||
val pb = ProcessBuilder()
|
||||
.let {
|
||||
it.command(listOf(executable) + arguments)
|
||||
//it.redirectError(File("python.error.txt"))
|
||||
it.inheritIO()
|
||||
}
|
||||
.start()
|
||||
.let {
|
||||
val `is` = it.inputStream
|
||||
val bis = BufferedInputStream(`is`)
|
||||
val br = bis.bufferedReader()
|
||||
result = br.readText().trim()
|
||||
val error = it.waitFor()
|
||||
println("Python returned: $error")
|
||||
|
||||
// Error detection disabled because pressing the pause button on the Axidraw
|
||||
// returns "1", and we don't want the program to close when that happens.
|
||||
// There's no obvious way to distinguish between actual errors and pressing the pause button.
|
||||
// if (error != 0) {
|
||||
// error("Python invoke failed with error $error")
|
||||
// }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
error("\n\nPython 3.8 or higher is required but failed to run. Is it installed?\n\n")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user