{{pkg.description}}
AXI-SCAPE #000, 12-layer watercolor painting/plot of cellular automata, © 2023 Karsten Schmidt
This package provides a super-lightweight alternative to control an AxiDraw plotter directly from Node.js, using a small custom set of medium/high-level drawing commands. Structurally, these custom commands are thi.ng/hiccup-like S-expressions, which can be easily serialized to/from JSON and are translated to the native EBB commands for the plotter.
Due to AxiDraw's lack of G-Code support, most other available AxiDraw support libraries are providing only a purely imperative API to control the machine. In contrast, this package utilizes a more declarative approach, also very much following the pattern of other packages in the thi.ng/umbrella monorepo, which allows (geometry) data to be inspected, augmented, converted/transformed, serialized up until the very last moment before being sent to the machine for physical output...
By default, bounds checking and coordinate clamping are applied (against a user
defined bounding rect or paper size), however this can be disabled (in which
case all given coordinates are expected to be valid and within machine limits).
Coordinates can be given in any unit, but if not using millimeters (default), a
conversion factor to inches (unitsPerInch
) MUST be provided as part of the
options
object given
to the AxiDraw
constructor. Actual geometry clipping can be handled by the
geom or geom-axidraw packages...
The bounding rect can be either defined by a tuple of [[minX,minY], [maxX,maxY]]
(in worldspace units) or as paper size defined as a
quantity()
. The
default value is DIN A3 landscape.
If given as paper size (e.g. via thi.ng/units presets), the actual units used to define these dimensions are irrelevant and will be automatically converted.
Path planning is considered a higher level operation than what's addressed by this package and is therefore out of scope. The thi.ng/geom-axidraw provides some configurable point & shape sorting functions, but this is an interim solution and a full path/route planning facility is currently still outstanding and awaiting to be ported from other projects.
The thi.ng/geom
package provides numerous shape types & operations to generate & transform
geometry. Additionally,
thi.ng/geom-axidraw
can act as bridge API and provides the polymorphic
asAxiDraw()
function to convert single shapes or entire shape groups/hierarchies directly
into the draw commands used by this (axidraw) package. See package readme for
more details and examples.
This package does not provide any direct conversions from SVG or any other geometry format. But again, whilst not containing a full SVG parser (at current only single paths can be parsed), the family of thi.ng/geom packages provides numerous shape types & operations which can be directly utilized to output generated geometry together with this package...
The only built-in conversion provided here is the
polyline()
utility function to convert an array of points (representing a polyline) to an
array of drawing commands (with various config options). All other conversions
are out of scope for this package (& for now).
We're using the serialport NPM package to submit data directly to the drawing machine. That package includes native bindings for Linux, MacOS and Windows.
The
AxiDraw.connect()
function (see example below) attempts to find the drawing machine by matching a
given regexp with available port names. The default regexp might only work on
Mac, but YMMV!
At some point it would also be worth looking into WebSerial support to enable plotting directly from the browser. Right now this package is only aimed at Node.js though...
The main
draw()
function provided by this package is async and supports custom implementations
to pause, resume or cancel the processing of further drawing commands. By the
default
AxiDrawControl
is used as default implementation.
If a control is provided, it will be checked prior to processing each individual command. Drawing will be paused if the control state is in paused state and the control will be rechecked every N milliseconds for updates (configurable). In paused state, the pen will be automatically lifted (if it wasn't already) and when resuming it will be sent down again (if it was originally down). Draw commands are only sent to the machine if no control is provided at all or if the control is in the "continue" state.
The draw()
function also records several
metrics, useful
for further analysis (or to identify optimizations) of the plotting process.
These metrics include:
- total duration
- total distance traveled
- draw distance (i.e. only whilst pen is down)
- number of pen up/down commands (i.e. to consider servo lifespan)
- total number of commands
{{meta.status}}
{{repo.supportPackages}}
{{repo.relatedPackages}}
{{meta.blogPosts}}
{{pkg.install}}
{{pkg.size}}
{{pkg.deps}}
{{repo.examples}}
{{pkg.docs}}
All
DrawCommand
s
are expressed as S-expression-like, thi.ng/hiccup-style elements, aka JS
arrays/tuples of [command, ...args]
. The following commands are supported. All
also as predefined constants or factory functions for the parametric ones:
Command | Preset/factory | Description |
---|---|---|
["comment", msg] |
COMMENT |
Ignored, but logged during plotting |
["d", delay?, level?] / ["u", delay?, level?] |
DOWN / UP |
Move pen up/down w/ optional delay & level (0-100) |
["home"] |
HOME |
Move to home position (origin) |
["m", [x,y], speed?] |
MOVE_REL |
Move to relative position w/ optional speed factor |
["M", [x,y], speed?] |
MOVE |
Move to absolute position w/ optional speed factor |
["on"] / ["off"] |
ON / OFF |
Turn motors on/off |
["pen", down, up] |
PEN |
Pen config (up/down levels) |
["reset"] |
RESET |
Execute user defined reset sequence(1) |
["restore"] |
RESTORE |
Restore saved pen up/down levels |
["save"] |
SAVE |
Save current pen up/down levels |
["start"] |
START |
Execute user defined start sequence(1) |
["stop"] |
STOP |
Execute user defined stop sequence(1) |
["w", delay] |
WAIT |
Wait N milliseconds |
- (1) See AxiDrawOpts for details.
Additionally, the following command sequence generators are provided (see their docs for details and code examples):
import { AxiDraw, polyline } from "@thi.ng/axidraw";
(async () => {
// instantiate w/ default options (see docs for info)
const axi = new AxiDraw();
// connect to 1st serial port matching given pre-string or regexp
// (the port used here is the default arg)
await axi.connect("/dev/tty.usbmodem");
// true
// vertices defining a polyline of a 100x100 mm square (top left at 20,20)
const verts = [[20, 20], [120, 20], [120, 120], [20, 120], [20, 20]];
// convert to drawing commands (w/ custom speed, 25%)
// see docs for config options
const path = polyline(verts, { speed: 0.25 })
// [
// ["m", [20, 20]],
// ["d"],
// ["m", [120, 20], 0.25],
// ["m", [120, 120], 0.25],
// ["m", [20, 120], 0.25],
// ["m", [20, 20], 0.25],
// ["u"]
// ]
// draw/send seq of commands
// by default the given commands will be wrapped with a start/end
// command sequence, configurable via options given to AxiDraw ctor)...
await axi.draw(path);
})();
Result shown here: https://mastodon.thi.ng/@toxi/109473655772673067
import { AxiDraw } from "@thi.ng/axidraw";
import { asCubic, group, pathFromCubics, star } from "@thi.ng/geom";
import { asAxiDraw } from "@thi.ng/geom-axidraw";
import { map, range } from "@thi.ng/transducers";
(async () => {
// create group of bezier-interpolated star polygons,
// with each path using a slightly different configuration
const geo = group({ translate: [100, 100] }, [
...map(
(t) =>
pathFromCubics(
asCubic(star(90, 6, [t, 1]), {
breakPoints: true,
scale: 0.66,
})
),
range(0.3, 1.01, 0.05)
),
]);
// connect to plotter
const axi = new AxiDraw();
await axi.connect();
// convert geometry to drawing commands & send to plotter
await axi.draw(asAxiDraw(geo, { samples: 40 }));
})();
This example illustrates how the linearPalette()
command sequence
generator can be used to paint random dots with a brush which gets re-dipped in
different paints every 10 dots...
Also see
InterleaveOpts
for more details...
import { points } from "@thi.ng/geom";
import { asAxiDraw } from "@thi.ng/geom-axidraw";
import { repeatedly } from "@thi.ng/transducers";
import { randMinMax2 } from "@thi.ng/vectors";
// configure palette
// "linear" here means the palette slots are arranged in a line
// (there's also a radialPalette() function for circular/elliptical palette layouts)
const palette = linearPalette({
// first palette slot is near the world origin (slight offset)
pos: [2, 0],
// 2mm jitter radius (to not always move to exact same position)
jitter: 2,
// palette has 5 paint slots
num: 5,
// each slot 40mm separation along Y-axis
// (needs to be measured/determined manually)
step: [0, 40],
// dip brush 3x each time
repeat: 3,
});
// define point cloud of 100 random points
// using a random palette slot each time (for each refill)
// assign axidraw-specific attribs to refill brush every 10 dots
const cloud = points(
// pick random points
[...repeatedly(() => randMinMax2([], [10, 10], [190, 190]), 100)],
// shape attributes
{
__axi: {
interleave: {
// every 10 elements/dots...
num: 10,
// execute these commands...
commands: () => [
// first brush cleaning in water
// (we decide to use the last palette slot for that)
...palette(4),
// now "refill" brush at a random other slot
...palette(Math.floor(Math.random() * 4))
]
}
}
}
);
// AxiDraw setup
const axi = new AxiDraw();
...
// convert geometry into axidraw commands and send to plotter
axi.draw(asAxiDraw(cloud));
Other selected toots/tweets: