New orx-axidraw (#356)
This commit is contained in:
@@ -35,7 +35,16 @@ val main: SourceSet by sourceSets.getting
|
|||||||
|
|
||||||
@Suppress("UNUSED_VARIABLE")
|
@Suppress("UNUSED_VARIABLE")
|
||||||
val demo: SourceSet by sourceSets.creating {
|
val demo: SourceSet by sourceSets.creating {
|
||||||
val skipDemos = setOf("openrndr-demos", "orx-minim", "orx-realsense2", "orx-runway", "orx-video-profiles", "orx-midi", "orx-syphon")
|
val skipDemos = setOf(
|
||||||
|
"openrndr-demos",
|
||||||
|
"orx-axidraw",
|
||||||
|
"orx-midi",
|
||||||
|
"orx-minim",
|
||||||
|
"orx-realsense2",
|
||||||
|
"orx-runway",
|
||||||
|
"orx-syphon",
|
||||||
|
"orx-video-profiles",
|
||||||
|
)
|
||||||
if (project.name !in skipDemos) {
|
if (project.name !in skipDemos) {
|
||||||
collectScreenshots(project, this@creating) { }
|
collectScreenshots(project, this@creating) { }
|
||||||
}
|
}
|
||||||
@@ -141,4 +150,4 @@ if (shouldPublish) {
|
|||||||
setRequired({ isReleaseVersion && gradle.taskGraph.hasTask("publish") })
|
setRequired({ isReleaseVersion && gradle.taskGraph.hasTask("publish") })
|
||||||
sign(publishing.publications)
|
sign(publishing.publications)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -765,6 +765,7 @@ class CompositionDrawer(
|
|||||||
groupNode.children.forEach {
|
groupNode.children.forEach {
|
||||||
it.parent = groupNode
|
it.parent = groupNode
|
||||||
}
|
}
|
||||||
|
groupNode.attributes.putAll(node.attributes)
|
||||||
groupNode
|
groupNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ val openrndrClassifier: String by (gradle as ExtensionAware).extra(
|
|||||||
include(
|
include(
|
||||||
listOf(
|
listOf(
|
||||||
"openrndr-demos",
|
"openrndr-demos",
|
||||||
|
"orx-jvm:orx-axidraw",
|
||||||
"orx-jvm:orx-boofcv",
|
"orx-jvm:orx-boofcv",
|
||||||
"orx-camera",
|
"orx-camera",
|
||||||
"orx-jvm:orx-chataigne",
|
"orx-jvm:orx-chataigne",
|
||||||
|
|||||||
Reference in New Issue
Block a user