@thi.ng/axidraw 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2022-12-06T17:16:38Z
3
+ - **Last updated**: 2022-12-10T14:04:38Z
4
4
  - **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
5
5
 
6
6
  All notable changes to this project will be documented in this file.
@@ -9,6 +9,35 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
9
9
  **Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
10
10
  and/or version bumps of transitive dependencies.
11
11
 
12
+ ## [0.2.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/axidraw@0.2.0) (2022-12-10)
13
+
14
+ #### 🚀 Features
15
+
16
+ - major updates/additions ([eb41c28](https://github.com/thi-ng/umbrella/commit/eb41c28))
17
+ - extract polyline() as standalone fn
18
+ - add complete() syntax sugar
19
+ - update UP/DOWN commands to accept opt. pen level/position
20
+ - add RESET command
21
+ - extract various draw commands into separate methods, simplify draw()
22
+ - update draw() w/ FSM to pause/resume/cancel processing
23
+ - add AxiDrawState FSM enum
24
+ - add AxiDrawControl class, use as default controller
25
+ - update AxiDrawOpts w/ new options
26
+ - update connect() to throw error if unsuccessful
27
+ - add SIGINT signal handler to handle Ctrl+C
28
+ - update .draw() to auto-wrap command seq ([60aaad2](https://github.com/thi-ng/umbrella/commit/60aaad2))
29
+ - add PolylineOpts, update polyline() ([c8a271f](https://github.com/thi-ng/umbrella/commit/c8a271f))
30
+
31
+ #### 🩹 Bug fixes
32
+
33
+ - fix polyline(), only apply custom speed for drawing ([c43b6f5](https://github.com/thi-ng/umbrella/commit/c43b6f5))
34
+ - update draw calls to disable cmd wrapping ([4cd5e53](https://github.com/thi-ng/umbrella/commit/4cd5e53))
35
+ - fix waiting for start/stop/home commands ([42bf4eb](https://github.com/thi-ng/umbrella/commit/42bf4eb))
36
+
37
+ #### ⏱ Performance improvements
38
+
39
+ - remove obsolete UP command (and delay) in polyline() ([f71c64b](https://github.com/thi-ng/umbrella/commit/f71c64b))
40
+
12
41
  ## [0.1.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/axidraw@0.1.0) (2022-12-06)
13
42
 
14
43
  #### 🚀 Features
package/README.md CHANGED
@@ -11,12 +11,18 @@ This project is part of the
11
11
 
12
12
  - [About](#about)
13
13
  - [Declarative vs. imperative](#declarative-vs-imperative)
14
- - [No SVG support](#no-svg-support)
14
+ - [Units, limits & clipping](#units-limits--clipping)
15
+ - [Path planning](#path-planning)
16
+ - [thi.ng/geom support](#thinggeom-support)
17
+ - [SVG support](#svg-support)
15
18
  - [Serial port support](#serial-port-support)
16
19
  - [Status](#status)
17
20
  - [Installation](#installation)
18
21
  - [Dependencies](#dependencies)
19
22
  - [API](#api)
23
+ - [Example usage](#example-usage)
24
+ - [Basics](#basics)
25
+ - [geom-axidraw example](#geom-axidraw-example)
20
26
  - [Authors](#authors)
21
27
  - [License](#license)
22
28
 
@@ -27,9 +33,10 @@ Minimal AxiDraw plotter/drawing machine controller for Node.js.
27
33
  This package provides a super-lightweight alternative to control an [AxiDraw
28
34
  plotter](https://axidraw.com/) directly from Node.js, using a small custom set
29
35
  of medium/high-level drawing commands. Structurally, these custom commands are
30
- [thi.ng/hiccup-like](https://github.com/thi-ng/umbrella/blob/develop/packages/hiccup/)
36
+ [thi.ng/hiccup](https://github.com/thi-ng/umbrella/blob/develop/packages/hiccup/)-like
31
37
  S-expressions, which can be easily serialized to/from JSON and are translated to
32
- [EBB commands](https://evil-mad.github.io/EggBot/ebb.html) for the plotter.
38
+ the native [EBB commands](https://evil-mad.github.io/EggBot/ebb.html) for the
39
+ plotter.
33
40
 
34
41
  ### Declarative vs. imperative
35
42
 
@@ -42,28 +49,68 @@ following the pattern of other packages in the
42
49
  until the very last moment before being sent to the machine for physical
43
50
  output...
44
51
 
45
- ### No SVG support
46
-
47
- This package does **not** provide conversion from SVG or any other geometry
48
- format conversions. Whilst not containing a full SVG parser (only single paths),
49
- the family of
52
+ ### Units, limits & clipping
53
+
54
+ This package performs **no bounds checking nor clipping** and expects all given
55
+ coordinates to be valid and within machine limits. Coordinates can be given in
56
+ any unit, but if not using millimeters (default), a conversion factor to inches
57
+ (`unitsPerInch`) **MUST** be provided as part of the [options
58
+ object](https://docs.thi.ng/umbrella/axidraw/interfaces/AxiDrawOpts.html) given
59
+ to the `AxiDraw` constructor. Clipping can be handled by the geom or
60
+ geom-axidraw packages (see below)...
61
+
62
+ ### Path planning
63
+
64
+ Path planning is considered a higher level operation than what's addressed by
65
+ this package and is therefore out of scope. The
66
+ [thi.ng/geom-axidraw](https://github.com/thi-ng/umbrella/tree/develop/packages/geom-axidraw)
67
+ provides some configurable point & shape sorting functions, but this is an
68
+ interim solution and a full path/route planning facility is currently still
69
+ outstanding and awaiting to be ported from other projects.
70
+
71
+ ### thi.ng/geom support
72
+
73
+ The [thi.ng/geom](https://github.com/thi-ng/umbrella/tree/develop/packages/geom)
74
+ package provides numerous shape types & operations to generate & transform
75
+ geometry. Additionally,
76
+ [thi.ng/geom-axidraw](https://github.com/thi-ng/umbrella/tree/develop/packages/geom-axidraw)
77
+ can act as bridge API and provides the polymorphic
78
+ [`asAxiDraw()`](https://docs.thi.ng/umbrella/geom-axidraw/functions/asAxiDraw.html)
79
+ function to convert single shapes or entire shape groups/hierarchies directly
80
+ into the draw commands used by this (axidraw) package. See package readme for
81
+ more details and examples.
82
+
83
+ ### SVG support
84
+
85
+ This package does **not** provide any direct conversions from SVG or any other
86
+ geometry format. But again, whilst not containing a full SVG parser (at current
87
+ only single paths can be parsed), the family of
50
88
  [thi.ng/geom](https://github.com/thi-ng/umbrella/tree/develop/packages/geom)
51
- packages provides numerous other shape types & operations which can be directly
89
+ packages provides numerous shape types & operations which can be directly
52
90
  utilized to output generated geometry together with this package...
53
91
 
54
- The only built-in conversion provided is the [`AxiDraw.polyline()`]() method to
55
- convert an array of points (representing a polyline) into an array of drawing
56
- commands. All other conversions are out of scope for this package (& for now).
92
+ The only built-in conversion provided here is the
93
+ [`polyline()`](https://docs.thi.ng/umbrella/axidraw/functions/polyline.html)
94
+ utility function to convert an array of points (representing a polyline) to an
95
+ array of drawing commands (with various config options). All other conversions
96
+ are out of scope for this package (& for now).
57
97
 
58
98
  ### Serial port support
59
99
 
60
100
  We're using the [serialport](https://serialport.io/) NPM package to submit data
61
- directly to the drawing machine. That pacakge includes native bindings for
101
+ directly to the drawing machine. That package includes native bindings for
62
102
  Linux, MacOS and Windows.
63
103
 
64
- The `AxiDraw.connect()` function (see example below) attempts to find the
65
- drawing machine by matching a given regexp with available port names. The
66
- default regexp might only work on Mac, but YMMV!
104
+ The
105
+ [`AxiDraw.connect()`](https://docs.thi.ng/umbrella/axidraw/classes/AxiDraw.html#connect)
106
+ function (see example below) attempts to find the drawing machine by matching a
107
+ given regexp with available port names. The default regexp might only work on
108
+ Mac, but YMMV!
109
+
110
+ At some point it would also be worth looking into
111
+ [WebSerial](https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API)
112
+ support to enable plotting directly from the browser. Right now this package is
113
+ only aimed at Node.js though...
67
114
 
68
115
  ## Status
69
116
 
@@ -94,7 +141,7 @@ node --experimental-repl-await
94
141
  > const axidraw = await import("@thi.ng/axidraw");
95
142
  ```
96
143
 
97
- Package sizes (brotli'd, pre-treeshake): ESM: 1.01 KB
144
+ Package sizes (brotli'd, pre-treeshake): ESM: 1.67 KB
98
145
 
99
146
  ## Dependencies
100
147
 
@@ -109,47 +156,87 @@ Package sizes (brotli'd, pre-treeshake): ESM: 1.01 KB
109
156
 
110
157
  [Generated API docs](https://docs.thi.ng/umbrella/axidraw/)
111
158
 
112
- ```ts tangle:export/readme.js
113
- import { AxiDraw } from "@thi.ng/axidraw";
114
- import { circle, vertices } from "@thi.ng/geom";
159
+ ### Example usage
160
+
161
+ #### Basics
162
+
163
+ ```js tangle:export/readme-basic.js
164
+ import { AxiDraw, polyline } from "@thi.ng/axidraw";
115
165
 
116
166
  (async () => {
117
167
 
118
168
  // instantiate w/ default options (see docs for info)
119
- // default paper size is DIN A4
120
169
  const axi = new AxiDraw();
121
170
 
122
- // connect to 1st serial port matching given regexp
123
- await axi.connect(/^\/dev\/tty\.usbmodem/);
171
+ // connect to 1st serial port matching given pre-string or regexp
172
+ // (the port used here is the default arg)
173
+ await axi.connect("/dev/tty.usbmodem");
124
174
  // true
125
175
 
126
- // compute 60 points on a circle at (100,50) w/ radius 30
127
- // (all units in mm)
128
- const verts = vertices(circle([100, 50], 30), { num: 60, last: true });
129
- // [
130
- // [ 130, 50 ],
131
- // [ 129.8356568610482, 53.1358538980296 ],
132
- // [ 129.34442802201417, 56.23735072453278 ],
133
- // ...
134
- // ]
176
+ // vertices defining a polyline of a 100x100 mm square (top left at 20,20)
177
+ const verts = [[20, 20], [120, 20], [120, 120], [20, 120], [20, 20]];
135
178
 
136
- // convert to drawing commands (w/ default opts)
137
- const path = axi.polyline(verts)
179
+ // convert to drawing commands (w/ custom speed, 25%)
180
+ // see docs for config options
181
+ const path = polyline(verts, { speed: 0.25 })
138
182
  // [
139
- // [ 'u' ],
140
- // [ 'm', [ 130, 50 ], 1 ],
141
- // [ 'd' ],
142
- // [ 'm', [ 129.8356568610482, 53.1358538980296 ], 1 ],
143
- // [ 'm', [ 129.34442802201417, 56.23735072453278 ], 1 ],
144
- // ...
183
+ // ["m", [20, 20]],
184
+ // ["d"],
185
+ // ["m", [120, 20], 0.25],
186
+ // ["m", [120, 120], 0.25],
187
+ // ["m", [20, 120], 0.25],
188
+ // ["m", [20, 20], 0.25],
189
+ // ["u"]
145
190
  // ]
146
191
 
147
- // draw/send seq of commands (incl. start/end sequence, configurable)
148
- await axi.draw([["start"], ...path, ["stop"]]);
192
+ // draw/send seq of commands
193
+ // by default the given commands will be wrapped with a start/end
194
+ // command sequence, configurable via options given to AxiDraw ctor)...
195
+ await axi.draw(path);
149
196
 
150
197
  })();
151
198
  ```
152
199
 
200
+ ### geom-axidraw example
201
+
202
+ Result shown here: https://mastodon.thi.ng/@toxi/109473655772673067
203
+
204
+ ```js tangle:export/readme-geom.js
205
+ import { AxiDraw } from "@thi.ng/axidraw";
206
+ import { asCubic, group, pathFromCubics, star } from "@thi.ng/geom";
207
+ import { asAxiDraw } from "@thi.ng/geom-axidraw";
208
+ import { map, range } from "@thi.ng/transducers";
209
+
210
+ (async () => {
211
+ // create group of bezier-interpolated star polygons,
212
+ // with each path using a slightly different configuration
213
+ const geo = group({ translate: [100, 100] }, [
214
+ ...map(
215
+ (t) =>
216
+ pathFromCubics(
217
+ asCubic(star(90, 6, [t, 1]), {
218
+ breakPoints: true,
219
+ scale: 0.66,
220
+ })
221
+ ),
222
+ range(0.3, 1.01, 0.05)
223
+ ),
224
+ ]);
225
+
226
+ // connect to plotter
227
+ const axi = new AxiDraw();
228
+ await axi.connect();
229
+ // convert geometry to drawing commands & send to plotter
230
+ await axi.draw(asAxiDraw(geo, { samples: 40 }));
231
+ })();
232
+ ```
233
+
234
+ Other selected toots/tweets:
235
+
236
+ - https://mastodon.thi.ng/@toxi/109474947869078797
237
+ - https://mastodon.thi.ng/@toxi/109483553358349473
238
+ - more to come...
239
+
153
240
  ## Authors
154
241
 
155
242
  Karsten Schmidt
package/api.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { IDeref } from "@thi.ng/api";
1
2
  import type { ILogger } from "@thi.ng/logger";
2
3
  import type { ReadonlyVec } from "@thi.ng/vectors";
3
4
  /** Start command sequence (configurable via {@link AxiDrawOpts}) */
@@ -6,15 +7,18 @@ export declare type StartCommand = ["start"];
6
7
  export declare type StopCommand = ["stop"];
7
8
  /** Return plotter to initial XY position */
8
9
  export declare type HomeCommand = ["home"];
10
+ /** Reset curr position as home (0,0) */
11
+ export declare type ResetCommand = ["reset"];
9
12
  /** Turn XY motors on/off */
10
13
  export declare type MotorCommand = ["on" | "off"];
11
14
  /** Pen config, min/down position, max/up position (in %) */
12
15
  export declare type PenConfigCommand = ["pen", number?, number?];
13
16
  /**
14
- * Pen up/down, optional delay (in ms), if omitted values used from
15
- * {@link AxiDrawOpts}.
17
+ * Pen up/down, optional delay (in ms), optional custom level/position. If
18
+ * omitted, default values used from {@link AxiDrawOpts}. Using -1 as delay also
19
+ * uses default.
16
20
  */
17
- export declare type PenUpDownCommand = ["u" | "d", number?];
21
+ export declare type PenUpDownCommand = ["u" | "d", number?, number?];
18
22
  /**
19
23
  * Move to abs pos (in worldspace coords, default mm), optional speed factor
20
24
  * (default: 1)
@@ -22,7 +26,10 @@ export declare type PenUpDownCommand = ["u" | "d", number?];
22
26
  export declare type MoveXYCommand = ["m", ReadonlyVec, number?];
23
27
  /** Explicit delay (in ms) */
24
28
  export declare type WaitCommand = ["w", number];
25
- export declare type DrawCommand = StartCommand | StopCommand | HomeCommand | MotorCommand | PenConfigCommand | PenUpDownCommand | MoveXYCommand | WaitCommand;
29
+ export declare type DrawCommand = StartCommand | StopCommand | HomeCommand | ResetCommand | MotorCommand | PenConfigCommand | PenUpDownCommand | MoveXYCommand | WaitCommand;
30
+ /**
31
+ * Global plotter drawing configuration. Also see {@link DEFAULT_OPTS}.
32
+ */
26
33
  export interface AxiDrawOpts {
27
34
  /**
28
35
  * Conversion factor from geometry worldspace units to inches.
@@ -58,13 +65,13 @@ export interface AxiDrawOpts {
58
65
  /**
59
66
  * Delay after pen up
60
67
  *
61
- * @defaultValue 300
68
+ * @defaultValue 150
62
69
  */
63
70
  delayUp: number;
64
71
  /**
65
72
  * Delay after pen down
66
73
  *
67
- * @defaultValue 300
74
+ * @defaultValue 150
68
75
  */
69
76
  delayDown: number;
70
77
  /**
@@ -89,13 +96,92 @@ export interface AxiDrawOpts {
89
96
  * Logger instance
90
97
  */
91
98
  logger: ILogger;
99
+ /**
100
+ * Optional implementation to pause, resume or cancel the processing of
101
+ * drawing commands (see {@link AxiDrawControl} for default impl).
102
+ *
103
+ * @remarks
104
+ * If a control is provided, it will be checked prior to processing each
105
+ * individual command. Drawing will be paused if the control state is in
106
+ * {@link AxiDrawState.PAUSE} state and the control will be rechecked every
107
+ * {@link AxiDrawOpts.refresh} milliseconds for updates. In paused state,
108
+ * the pen will be automatically lifted (if it wasn't already) and when
109
+ * resuming it will be sent down again (if it was originally down).
110
+ *
111
+ * Draw commands are only sent to the machine if no control is provided at
112
+ * all or if the control is in the {@link AxiDrawState.CONTINUE} state.
113
+ */
114
+ control?: IDeref<AxiDrawState>;
115
+ /**
116
+ * Refresh interval for checking the control FSM in paused state.
117
+ *
118
+ * @defaultValue 1000
119
+ */
120
+ refresh: number;
121
+ /**
122
+ * If true (default), installs SIGINT handler to lift pen when the Node.js
123
+ * process is terminated.
124
+ */
125
+ sigint: boolean;
92
126
  }
93
127
  export declare const START: StartCommand;
94
128
  export declare const STOP: StopCommand;
95
129
  export declare const HOME: HomeCommand;
130
+ export declare const RESET: ResetCommand;
96
131
  export declare const PEN: PenConfigCommand;
97
132
  export declare const UP: PenUpDownCommand;
98
133
  export declare const DOWN: PenUpDownCommand;
99
134
  export declare const ON: MotorCommand;
100
135
  export declare const OFF: MotorCommand;
136
+ /**
137
+ * FSM state enum for (interactive) control for processing of drawing commands.
138
+ * See {@link AxiDraw.draw} and {@link AxiDrawControl} for details.
139
+ */
140
+ export declare enum AxiDrawState {
141
+ /**
142
+ * Draw command processing can continue as normal.
143
+ */
144
+ CONTINUE = 0,
145
+ /**
146
+ * Draw command processing is suspended indefinitely.
147
+ */
148
+ PAUSE = 1,
149
+ /**
150
+ * Draw command processing is cancelled.
151
+ */
152
+ CANCEL = 2
153
+ }
154
+ /**
155
+ * Drawing behavior options for a single polyline.
156
+ */
157
+ export interface PolylineOpts {
158
+ /**
159
+ * Speed factor (multiple of globally configured draw speed). Depending on
160
+ * pen used, slower speeds might result in thicker strokes.
161
+ *
162
+ * @defaultValue 1
163
+ */
164
+ speed: number;
165
+ /**
166
+ * Pen down (Z) position (%) for this particular shape/polyline. Will be
167
+ * reset to globally configured default at the end of the shape.
168
+ */
169
+ down: number;
170
+ /**
171
+ * Delay for pen down command at the start of this particular
172
+ * shape/polyline.
173
+ */
174
+ delayDown: number;
175
+ /**
176
+ * Delay for pen up command at the end this particular shape/polyline.
177
+ */
178
+ delayUp: number;
179
+ /**
180
+ * If enabled, no pen up/down commands will be included.
181
+ * {@link PolylineOpts.speed} is the only other option considered then.
182
+ *
183
+ * @defaultValue false
184
+ */
185
+ onlyGeo: boolean;
186
+ }
101
187
  //# sourceMappingURL=api.d.ts.map
package/api.js CHANGED
@@ -1,8 +1,28 @@
1
1
  export const START = ["start"];
2
2
  export const STOP = ["stop"];
3
3
  export const HOME = ["home"];
4
+ export const RESET = ["reset"];
4
5
  export const PEN = ["pen"];
5
6
  export const UP = ["u"];
6
7
  export const DOWN = ["d"];
7
8
  export const ON = ["on"];
8
9
  export const OFF = ["off"];
10
+ /**
11
+ * FSM state enum for (interactive) control for processing of drawing commands.
12
+ * See {@link AxiDraw.draw} and {@link AxiDrawControl} for details.
13
+ */
14
+ export var AxiDrawState;
15
+ (function (AxiDrawState) {
16
+ /**
17
+ * Draw command processing can continue as normal.
18
+ */
19
+ AxiDrawState[AxiDrawState["CONTINUE"] = 0] = "CONTINUE";
20
+ /**
21
+ * Draw command processing is suspended indefinitely.
22
+ */
23
+ AxiDrawState[AxiDrawState["PAUSE"] = 1] = "PAUSE";
24
+ /**
25
+ * Draw command processing is cancelled.
26
+ */
27
+ AxiDrawState[AxiDrawState["CANCEL"] = 2] = "CANCEL";
28
+ })(AxiDrawState || (AxiDrawState = {}));
package/axidraw.d.ts CHANGED
@@ -1,33 +1,47 @@
1
- import type { Fn0 } from "@thi.ng/api";
1
+ import type { IReset } from "@thi.ng/api";
2
2
  import { ReadonlyVec, Vec } from "@thi.ng/vectors";
3
3
  import { SerialPort } from "serialport";
4
4
  import { AxiDrawOpts, DrawCommand } from "./api.js";
5
5
  export declare const DEFAULT_OPTS: AxiDrawOpts;
6
- export declare class AxiDraw {
6
+ export declare class AxiDraw implements IReset {
7
7
  serial: SerialPort;
8
8
  opts: AxiDrawOpts;
9
9
  isConnected: boolean;
10
+ isPenDown: boolean;
11
+ penLimits: [number, number];
10
12
  pos: Vec;
13
+ targetPos: Vec;
11
14
  constructor(opts?: Partial<AxiDrawOpts>);
15
+ reset(): this;
12
16
  /**
13
17
  * Async function. Attempts to connect to the drawing machine via given
14
18
  * (partial) serial port path/name, returns true if successful.
15
19
  *
20
+ * @remarks
21
+ * First matching port will be used. If `path` is a sting, a port name must
22
+ * only start with it in order to be considered a match.
23
+ *
24
+ * An error is thrown if no matching port could be found.
25
+ *
16
26
  * @param path
17
27
  */
18
- connect(path?: RegExp): Promise<boolean>;
28
+ connect(path?: string | RegExp): Promise<void>;
19
29
  /**
20
30
  * Async function. Converts sequence of {@link DrawCommand}s into actual EBB
21
- * commands and sends them via configured serial port to the AxiDraw. The
22
- * optional `cancel` predicate is checked prior to each individual command
23
- * and processing is stopped if that function returns a truthy result.
24
- *
25
- * Returns number of milliseconds taken for drawing.
31
+ * commands and sends them via configured serial port to the AxiDraw. If
32
+ * `wrap` is enabled (default), the given commands will be automatically
33
+ * wrapped with start/stop commands via {@link complete}. Returns total
34
+ * number of milliseconds taken for drawing (incl. any pauses caused by the
35
+ * control).
26
36
  *
27
37
  * @remarks
28
38
  * This function is async and if using `await` will only return once all
29
39
  * commands have been processed or cancelled.
30
40
  *
41
+ * The `control` implementation/ provided as part of {@link AxiDrawOpts} can
42
+ * be used to pause, resume or cancel the drawing (see
43
+ * {@link AxiDrawOpts.control} for details).
44
+ *
31
45
  * Reference:
32
46
  * - http://evil-mad.github.io/EggBot/ebb.html
33
47
  *
@@ -42,23 +56,35 @@ export declare class AxiDraw {
42
56
  * ```
43
57
  *
44
58
  * @param commands
45
- * @param cancel
59
+ * @param wrap
46
60
  */
47
- draw(commands: Iterable<DrawCommand>, cancel?: Fn0<boolean>): Promise<number>;
61
+ draw(commands: Iterable<DrawCommand>, wrap?: boolean): Promise<number>;
48
62
  /**
49
- * Takes an array of 2D points and converts them into an array of
50
- * {@link DrawCommand}s. The optional `speed` factor can be used to control
51
- * draw speed (default: 1).
63
+ * Syntax sugar for drawing a **single** command only, otherwise same as
64
+ * {@link AxiDraw.draw}.
65
+ *
66
+ * @param cmd
67
+ */
68
+ draw1(cmd: DrawCommand): Promise<number>;
69
+ motorsOn(): void;
70
+ motorsOff(): void;
71
+ penConfig(down?: number, up?: number): void;
72
+ penUp(delay?: number, level?: number): number;
73
+ penDown(delay?: number, level?: number): number;
74
+ moveTo(p: ReadonlyVec, tempo?: number): number;
75
+ home(): number;
76
+ protected onSignal(): Promise<void>;
77
+ protected send(msg: string): void;
78
+ /**
79
+ * Sends pen up/down config
52
80
  *
53
81
  * @remarks
54
- * Unless `onlyGeo` is explicitly enabled, the resulting command sequence
55
- * will also contain necessary pen up/down commands.
82
+ * Reference:
83
+ * - https://github.com/evil-mad/AxiDraw-Processing/blob/80d81a8c897b8a1872b0555af52a8d1b5b13cec4/AxiGen1/AxiGen1.pde#L213
56
84
  *
57
- * @param pts
58
- * @param speed
59
- * @param onlyGeo
85
+ * @param id
86
+ * @param x
60
87
  */
61
- polyline(pts: ReadonlyVec[], speed?: number, onlyGeo?: boolean): DrawCommand[];
62
- protected send(msg: string): void;
88
+ protected sendPenConfig(id: number, x: number): void;
63
89
  }
64
90
  //# sourceMappingURL=axidraw.d.ts.map
package/axidraw.js CHANGED
@@ -1,60 +1,90 @@
1
+ import { isString } from "@thi.ng/checks";
1
2
  import { delayed } from "@thi.ng/compose";
2
- import { assert, unsupported } from "@thi.ng/errors";
3
+ import { assert, ioerror, unsupported } from "@thi.ng/errors";
3
4
  import { ConsoleLogger } from "@thi.ng/logger";
4
- import { abs2, mulN2, set2, sub2 } from "@thi.ng/vectors";
5
+ import { abs2, mulN2, set2, sub2, zero, ZERO2, } from "@thi.ng/vectors";
5
6
  import { SerialPort } from "serialport";
6
- import { DOWN, HOME, OFF, ON, PEN, UP, } from "./api.js";
7
+ import { AxiDrawState, HOME, OFF, ON, PEN, UP, } from "./api.js";
8
+ import { AxiDrawControl } from "./control.js";
9
+ import { complete } from "./polyline.js";
7
10
  export const DEFAULT_OPTS = {
8
11
  logger: new ConsoleLogger("axidraw"),
12
+ control: new AxiDrawControl(),
13
+ refresh: 1000,
9
14
  unitsPerInch: 25.4,
10
15
  stepsPerInch: 2032,
11
16
  speed: 4000,
12
17
  up: 60,
13
18
  down: 30,
14
- delayUp: 300,
15
- delayDown: 300,
19
+ delayUp: 150,
20
+ delayDown: 150,
16
21
  preDelay: 0,
17
22
  start: [ON, PEN, UP],
18
23
  stop: [UP, HOME, OFF],
24
+ sigint: true,
19
25
  };
20
26
  export class AxiDraw {
21
27
  constructor(opts = {}) {
22
28
  this.isConnected = false;
29
+ this.isPenDown = false;
23
30
  this.pos = [0, 0];
31
+ this.targetPos = [0, 0];
24
32
  this.opts = { ...DEFAULT_OPTS, ...opts };
33
+ this.penLimits = [this.opts.down, this.opts.up];
34
+ }
35
+ reset() {
36
+ zero(this.pos);
37
+ zero(this.targetPos);
38
+ return this;
25
39
  }
26
40
  /**
27
41
  * Async function. Attempts to connect to the drawing machine via given
28
42
  * (partial) serial port path/name, returns true if successful.
29
43
  *
44
+ * @remarks
45
+ * First matching port will be used. If `path` is a sting, a port name must
46
+ * only start with it in order to be considered a match.
47
+ *
48
+ * An error is thrown if no matching port could be found.
49
+ *
30
50
  * @param path
31
51
  */
32
- async connect(path = /^\/dev\/tty\.usbmodem/) {
52
+ async connect(path = "/dev/tty.usbmodem") {
53
+ const isStr = isString(path);
33
54
  for (let port of await SerialPort.list()) {
34
- if (path.test(port.path)) {
55
+ if ((isStr && port.path.startsWith(path)) ||
56
+ (!isStr && path.test(port.path))) {
35
57
  this.opts.logger.info(`using device: ${port.path}...`);
36
58
  this.serial = new SerialPort({
37
59
  path: port.path,
38
60
  baudRate: 38400,
39
61
  });
40
62
  this.isConnected = true;
41
- return true;
63
+ if (this.opts.sigint) {
64
+ this.opts.logger.debug("installing signal handler...");
65
+ process.on("SIGINT", this.onSignal.bind(this));
66
+ }
67
+ return;
42
68
  }
43
69
  }
44
- return false;
70
+ ioerror(`no matching device for ${path}`);
45
71
  }
46
72
  /**
47
73
  * Async function. Converts sequence of {@link DrawCommand}s into actual EBB
48
- * commands and sends them via configured serial port to the AxiDraw. The
49
- * optional `cancel` predicate is checked prior to each individual command
50
- * and processing is stopped if that function returns a truthy result.
51
- *
52
- * Returns number of milliseconds taken for drawing.
74
+ * commands and sends them via configured serial port to the AxiDraw. If
75
+ * `wrap` is enabled (default), the given commands will be automatically
76
+ * wrapped with start/stop commands via {@link complete}. Returns total
77
+ * number of milliseconds taken for drawing (incl. any pauses caused by the
78
+ * control).
53
79
  *
54
80
  * @remarks
55
81
  * This function is async and if using `await` will only return once all
56
82
  * commands have been processed or cancelled.
57
83
  *
84
+ * The `control` implementation/ provided as part of {@link AxiDrawOpts} can
85
+ * be used to pause, resume or cancel the drawing (see
86
+ * {@link AxiDrawOpts.control} for details).
87
+ *
58
88
  * Reference:
59
89
  * - http://evil-mad.github.io/EggBot/ebb.html
60
90
  *
@@ -69,103 +99,152 @@ export class AxiDraw {
69
99
  * ```
70
100
  *
71
101
  * @param commands
72
- * @param cancel
102
+ * @param wrap
73
103
  */
74
- async draw(commands, cancel) {
104
+ async draw(commands, wrap = true) {
75
105
  assert(this.isConnected, "AxiDraw not yet connected, need to call .connect() first");
76
106
  let t0 = Date.now();
77
- if (!cancel)
78
- cancel = () => false;
79
- const { opts: config, pos } = this;
80
- const { stepsPerInch, unitsPerInch, speed, preDelay } = config;
81
- // scale factor: worldspace units -> motor steps
82
- const scale = stepsPerInch / unitsPerInch;
83
- let targetPos = [0, 0];
84
- let delta = [0, 0];
85
- for (let $cmd of commands) {
86
- if (cancel())
87
- break;
107
+ const { control, logger, preDelay, refresh } = this.opts;
108
+ for (let $cmd of wrap ? complete(commands) : commands) {
109
+ if (control) {
110
+ let state = control.deref();
111
+ if (state === AxiDrawState.PAUSE) {
112
+ const penDown = this.isPenDown;
113
+ if (penDown)
114
+ this.penUp();
115
+ do {
116
+ await delayed(0, refresh);
117
+ } while ((state = control.deref()) === AxiDrawState.PAUSE);
118
+ if (state === AxiDrawState.CONTINUE && penDown) {
119
+ this.penDown();
120
+ }
121
+ }
122
+ if (state === AxiDrawState.CANCEL) {
123
+ this.penUp();
124
+ break;
125
+ }
126
+ }
88
127
  const [cmd, a, b] = $cmd;
89
128
  let wait = -1;
90
129
  switch (cmd) {
91
130
  case "start":
92
131
  case "stop":
93
- this.draw(config[cmd], cancel);
132
+ await this.draw(this.opts[cmd], false);
94
133
  break;
95
134
  case "home":
96
- this.draw([["m", [0, 0]]], cancel);
135
+ wait = this.home();
136
+ break;
137
+ case "reset":
138
+ this.reset();
97
139
  break;
98
140
  case "on":
99
- this.send("EM,1,1\r");
141
+ this.motorsOn();
100
142
  break;
101
143
  case "off":
102
- this.send("EM,0,0\r");
144
+ this.motorsOff();
103
145
  break;
104
146
  case "pen":
105
- {
106
- let val = a !== undefined ? a : config.down;
107
- // unit ref:
108
- // https://github.com/evil-mad/AxiDraw-Processing/blob/80d81a8c897b8a1872b0555af52a8d1b5b13cec4/AxiGen1/AxiGen1.pde#L213
109
- this.send(`SC,5,${(7500 + 175 * val) | 0}\r`);
110
- val = b !== undefined ? b : config.up;
111
- this.send(`SC,4,${(7500 + 175 * val) | 0}\r`);
112
- this.send(`SC,10,65535\r`);
113
- }
147
+ this.penConfig(a, b);
114
148
  break;
115
149
  case "u":
116
- wait = a !== undefined ? a : config.delayUp;
117
- this.send(`SP,1,${wait}\r`);
150
+ wait = this.penUp(a, b);
118
151
  break;
119
152
  case "d":
120
- wait = a !== undefined ? a : config.delayDown;
121
- this.send(`SP,0,${wait}\r`);
153
+ wait = this.penDown(a, b);
122
154
  break;
123
155
  case "w":
124
156
  wait = a;
125
157
  break;
126
158
  case "m":
127
- {
128
- mulN2(targetPos, a, scale);
129
- sub2(delta, targetPos, pos);
130
- set2(pos, targetPos);
131
- config.logger.info("target", targetPos, "delta", delta);
132
- const maxAxis = Math.max(...abs2([], delta));
133
- wait = (1000 * maxAxis) / (speed * (b || 1));
134
- this.send(`XM,${wait | 0},${delta[0] | 0},${delta[1] | 0}\r`);
135
- }
159
+ wait = this.moveTo(a, b);
136
160
  break;
137
161
  default:
138
162
  unsupported(`unknown command: ${$cmd}`);
139
163
  }
140
164
  if (wait > 0) {
141
165
  wait = Math.max(0, wait - preDelay);
142
- config.logger.debug(`waiting ${wait}ms...`);
166
+ logger.debug(`waiting ${wait}ms...`);
143
167
  await delayed(0, wait);
144
168
  }
145
169
  }
146
170
  return Date.now() - t0;
147
171
  }
148
172
  /**
149
- * Takes an array of 2D points and converts them into an array of
150
- * {@link DrawCommand}s. The optional `speed` factor can be used to control
151
- * draw speed (default: 1).
173
+ * Syntax sugar for drawing a **single** command only, otherwise same as
174
+ * {@link AxiDraw.draw}.
152
175
  *
153
- * @remarks
154
- * Unless `onlyGeo` is explicitly enabled, the resulting command sequence
155
- * will also contain necessary pen up/down commands.
156
- *
157
- * @param pts
158
- * @param speed
159
- * @param onlyGeo
176
+ * @param cmd
160
177
  */
161
- polyline(pts, speed = 1, onlyGeo = false) {
162
- const commands = pts.map((p) => ["m", p, speed]);
163
- return onlyGeo
164
- ? commands
165
- : [UP, commands[0], DOWN, ...commands.slice(1), UP];
178
+ draw1(cmd) {
179
+ return this.draw([cmd], false);
180
+ }
181
+ motorsOn() {
182
+ this.send("EM,1,1\r");
183
+ }
184
+ motorsOff() {
185
+ this.send("EM,0,0\r");
186
+ }
187
+ penConfig(down, up) {
188
+ down = down !== undefined ? down : this.opts.down;
189
+ this.sendPenConfig(5, down);
190
+ this.penLimits[0] = down;
191
+ up = up !== undefined ? up : this.opts.up;
192
+ this.sendPenConfig(4, up);
193
+ this.penLimits[1] = up;
194
+ this.send(`SC,10,65535\r`);
195
+ }
196
+ penUp(delay, level) {
197
+ if (level !== undefined)
198
+ this.sendPenConfig(4, level);
199
+ delay = delay !== undefined && delay >= 0 ? delay : this.opts.delayUp;
200
+ this.send(`SP,1,${delay}\r`);
201
+ this.isPenDown = false;
202
+ return delay;
203
+ }
204
+ penDown(delay, level) {
205
+ if (level !== undefined)
206
+ this.sendPenConfig(5, level);
207
+ delay = delay !== undefined && delay >= 0 ? delay : this.opts.delayDown;
208
+ this.send(`SP,0,${delay}\r`);
209
+ this.isPenDown = true;
210
+ return delay;
211
+ }
212
+ moveTo(p, tempo = 1) {
213
+ const { pos, targetPos, opts } = this;
214
+ // apply scale factor: worldspace units -> motor steps
215
+ mulN2(targetPos, p, opts.stepsPerInch / opts.unitsPerInch);
216
+ const delta = sub2([], targetPos, pos);
217
+ set2(pos, targetPos);
218
+ const maxAxis = Math.max(...abs2([], delta));
219
+ const duration = (1000 * maxAxis) / (opts.speed * tempo);
220
+ this.send(`XM,${duration | 0},${delta[0] | 0},${delta[1] | 0}\r`);
221
+ return duration;
222
+ }
223
+ home() {
224
+ return this.moveTo(ZERO2);
225
+ }
226
+ async onSignal() {
227
+ this.opts.logger.warn(`SIGNINT received, stop drawing...`);
228
+ this.penUp(0);
229
+ this.motorsOff();
230
+ await delayed(0, 100);
231
+ process.exit(1);
166
232
  }
167
233
  send(msg) {
168
234
  this.opts.logger.debug(msg);
169
235
  this.serial.write(msg);
170
236
  }
237
+ /**
238
+ * Sends pen up/down config
239
+ *
240
+ * @remarks
241
+ * Reference:
242
+ * - https://github.com/evil-mad/AxiDraw-Processing/blob/80d81a8c897b8a1872b0555af52a8d1b5b13cec4/AxiGen1/AxiGen1.pde#L213
243
+ *
244
+ * @param id
245
+ * @param x
246
+ */
247
+ sendPenConfig(id, x) {
248
+ this.send(`SC,${id},${(7500 + 175 * x) | 0}\r`);
249
+ }
171
250
  }
package/control.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { IDeref, IReset } from "@thi.ng/api";
2
+ import { AxiDrawState } from "./api.js";
3
+ export declare class AxiDrawControl implements IDeref<AxiDrawState>, IReset {
4
+ interval: number;
5
+ state: AxiDrawState;
6
+ constructor(interval?: number);
7
+ deref(): AxiDrawState;
8
+ reset(): this;
9
+ pause(): void;
10
+ resume(): void;
11
+ cancel(): void;
12
+ }
13
+ //# sourceMappingURL=control.d.ts.map
package/control.js ADDED
@@ -0,0 +1,27 @@
1
+ import { AxiDrawState } from "./api.js";
2
+ export class AxiDrawControl {
3
+ constructor(interval = 1000) {
4
+ this.interval = interval;
5
+ this.state = AxiDrawState.CONTINUE;
6
+ }
7
+ deref() {
8
+ return this.state;
9
+ }
10
+ reset() {
11
+ this.state = AxiDrawState.CONTINUE;
12
+ return this;
13
+ }
14
+ pause() {
15
+ if (this.state === AxiDrawState.CONTINUE) {
16
+ this.state = AxiDrawState.PAUSE;
17
+ }
18
+ }
19
+ resume() {
20
+ if (this.state === AxiDrawState.PAUSE) {
21
+ this.state = AxiDrawState.CONTINUE;
22
+ }
23
+ }
24
+ cancel() {
25
+ this.state = AxiDrawState.CANCEL;
26
+ }
27
+ }
package/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from "./api.js";
2
2
  export * from "./axidraw.js";
3
+ export * from "./control.js";
4
+ export * from "./polyline.js";
3
5
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,2 +1,4 @@
1
1
  export * from "./api.js";
2
2
  export * from "./axidraw.js";
3
+ export * from "./control.js";
4
+ export * from "./polyline.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/axidraw",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Minimal AxiDraw plotter/drawing machine controller for Node.js",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -50,15 +50,17 @@
50
50
  "typescript": "^4.8.4"
51
51
  },
52
52
  "keywords": [
53
+ "2d",
54
+ "async",
53
55
  "axidraw",
56
+ "driver",
54
57
  "geometry",
55
58
  "io",
56
59
  "logger",
57
60
  "node",
58
- "plotter",
59
- "polygon",
61
+ "penplotter",
60
62
  "polyline",
61
- "serial",
63
+ "serialport",
62
64
  "typescript"
63
65
  ],
64
66
  "publishConfig": {
@@ -80,11 +82,17 @@
80
82
  },
81
83
  "./axidraw": {
82
84
  "default": "./axidraw.js"
85
+ },
86
+ "./control": {
87
+ "default": "./control.js"
88
+ },
89
+ "./polyline": {
90
+ "default": "./polyline.js"
83
91
  }
84
92
  },
85
93
  "thi.ng": {
86
94
  "status": "alpha",
87
95
  "year": 2022
88
96
  },
89
- "gitHead": "54c5f22520162bac0d58426a3f86ca636e797f71\n"
97
+ "gitHead": "5178ea1d7f4b2de86cf5101bdd73f6567518c0bd\n"
90
98
  }
package/polyline.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { ReadonlyVec } from "@thi.ng/vectors";
2
+ import { DrawCommand, PolylineOpts } from "./api.js";
3
+ /**
4
+ * Takes an array of 2D points and yields an iterable of {@link DrawCommand}s.
5
+ * The drawing behavior can be customized via additional {@link PolylineOpts}
6
+ * given.
7
+ *
8
+ * @remarks
9
+ * The resulting command sequence assumes the pen is in the **up** position at
10
+ * the beginning of the line. Each polyline will end with a {@link UP} command.
11
+ *
12
+ * @param pts
13
+ * @param opts
14
+ */
15
+ export declare function polyline(pts: ReadonlyVec[], opts: Partial<PolylineOpts>): IterableIterator<DrawCommand>;
16
+ /**
17
+ * Syntax sugar. Takes an iterable of draw commands, adds {@link START} as
18
+ * prefix and {@link STOP} as suffix. I.e. it creates a "complete" drawing...
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * [...complete([ ["m", [0, 0]] ])]
23
+ * // [ ["start"], ["m", [0, 0]], ["stop"] ]
24
+ * ```
25
+ *
26
+ * @param commands
27
+ */
28
+ export declare function complete(commands: Iterable<DrawCommand>): Generator<DrawCommand, void, undefined>;
29
+ //# sourceMappingURL=polyline.d.ts.map
package/polyline.js ADDED
@@ -0,0 +1,54 @@
1
+ import { DOWN, START, STOP, UP } from "./api.js";
2
+ /**
3
+ * Takes an array of 2D points and yields an iterable of {@link DrawCommand}s.
4
+ * The drawing behavior can be customized via additional {@link PolylineOpts}
5
+ * given.
6
+ *
7
+ * @remarks
8
+ * The resulting command sequence assumes the pen is in the **up** position at
9
+ * the beginning of the line. Each polyline will end with a {@link UP} command.
10
+ *
11
+ * @param pts
12
+ * @param opts
13
+ */
14
+ export function* polyline(pts, opts) {
15
+ if (!pts.length)
16
+ return;
17
+ const { speed, delayDown, delayUp, down, onlyGeo } = {
18
+ speed: 1,
19
+ onlyGeo: false,
20
+ ...opts,
21
+ };
22
+ if (onlyGeo) {
23
+ for (let p of pts)
24
+ yield ["m", p, speed];
25
+ return;
26
+ }
27
+ yield ["m", pts[0]];
28
+ if (down !== undefined)
29
+ yield ["pen", down];
30
+ yield delayDown != undefined ? ["d", delayDown] : DOWN;
31
+ for (let i = 1, n = pts.length; i < n; i++)
32
+ yield ["m", pts[i], speed];
33
+ yield delayUp != undefined ? ["u", delayUp] : UP;
34
+ // reset pen to configured defaults
35
+ if (down !== undefined)
36
+ yield ["pen"];
37
+ }
38
+ /**
39
+ * Syntax sugar. Takes an iterable of draw commands, adds {@link START} as
40
+ * prefix and {@link STOP} as suffix. I.e. it creates a "complete" drawing...
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * [...complete([ ["m", [0, 0]] ])]
45
+ * // [ ["start"], ["m", [0, 0]], ["stop"] ]
46
+ * ```
47
+ *
48
+ * @param commands
49
+ */
50
+ export function* complete(commands) {
51
+ yield START;
52
+ yield* commands;
53
+ yield STOP;
54
+ }