1. Draw a line

Use the page from Setup. Its drawing starts with:

plot.line(12, 12, 92, 12);

The two points are measured from the drawing's corner, in millimetres. Add another shape before drawRoute():

plot.circle(60, 40, 15);

The circle has a radius of 15 mm. Reload to see the line and circle. The engine records shapes; drawRoute() shows their planned route on the canvas.

Basic shapes are recorded directly in millimetres.
Lines, circles and rectangles become pen paths. circle() takes a radius.

2. Paper and placement

The drawing uses millimetres already. Choosing paper does not resize its strokes.

Set the paper format

Add paperSize to the import and replace the engine declaration:

import { PlotterEngine, paperSize } from "https://cdn.jsdelivr.net/gh/seb-prjcts-be/vanilla.penplotter@v0.5.0/vanilla.penplotter.js";

const plot = new PlotterEngine({
  units: "mm",
  page: { ...paperSize("A2", "landscape"), margin: 12 }
});

Use the format and orientation of your paper. The line stays 80 mm long, near the paper corner.

Place it on the machine

In an example's pen panel, X and Y locate the paper corner from the machine origin. Set both to zero when those corners coincide. Use the paper already on your machine.

Pixel drawing of the plotter with A2 paper, an 80 mm line near the paper corner, X along the rails and Y across the bed.

In my setup, X runs up the long rails and Y across to the right. The bed preview shows X to the right and Y down. The machine origin is the same point in both views.

The panel's Turn on the bed rotates the paper and drawing together. Check the preview against the actual sheet. Changing the format keeps stroke lengths; turning or changing X/Y changes where they land. Export links keep the original drawing coordinates.

Place a plan in your own code

Add Planner and Renderer to the import. After drawing, replace drawRoute() with:

const position = { x: 0, y: 0 };
const placed = Planner.placePlan(plot.plan(), position, 0);
Renderer.drawBed(canvas.getContext("2d"), placed, {
  bed: { width: 594, height: 432 },
  sheet: { ...position, width: 594, height: 420 }
});

Enter your paper position in position. The last argument to placePlan() is the turn: 0, 90, 180 or 270 degrees. Use the placed plan for the bed preview and driver; the source drawing stays unchanged.

A square sheet is placed on the bed and rotated through four quarter-turn positions.
The square sheet turns on the bed with its drawing. placePlan() changes the machine moves; the source drawing stays unchanged.

3. Plot the drawing

The Setup steps show how to place paper, connect and plot with an example panel. Each example supplies that panel. Its drawing code makes the shapes; the panel handles the machine.

For your own application, open a Web Serial connection from a button click and pass a checked plan to driver.run(). The frame and wave source shows how the shared panel is attached.

A complete JavaScript drawing is planned and then plotted on paper.
Prepare the drawing, then plot its plan.

A3 H with DrawCore

DrawCore uses GRBL and moves the pen along Z. Its connection and settings are separate from the EBB panel in the repository examples. The A3 H test page lets you read the controller, set the XY work origin and try a short line.

In your own page, add a button after the canvas:

<button id="plot-drawcore">Plot the drawing</button>

Add this to the drawing module. The settings match the small tests on an A3 H with DrawCore V2.09:

import { createAutoSerialTransport, detectDriver } from "https://cdn.jsdelivr.net/gh/seb-prjcts-be/vanilla.penplotter@v0.5.0/src/driver/index.js";

let connection;
let driver;

document.querySelector("#plot-drawcore").onclick = async function () {
  if (!driver) {
    connection = createAutoSerialTransport();
    await connection.open();
    driver = await detectDriver(connection, {
      drawcore: {
        travel: { width: 420, height: 297 },
        penUp: 0.5, penDown: 5, penFeed: 1000,
        drawFeed: 600, travelFeed: 900,
        axes: { swapXY: true, xDirection: -1, yDirection: -1 }
      }
    });
  }
  if (!confirm("Plot at the configured work origin? Check the paper and pen.")) return;
  await driver.run(plot.plan(), { confirmed: true });
};

The drawing must fit inside the configured travel bounds, and the controller must report an idle work position at X0 Y0. The driver checks those before moving. It leaves the machine's origin unchanged. These pen heights and directions belong to the tested setup; check them on yours.

DrawCore supports one pen and one complete drawing at a time. driver.abort() stops submitting drawing commands, waits for accepted movement to finish, then raises the pen. Buffered moves may still run before it stops. Errors and emergency stop request feed-hold; pen lift cannot be guaranteed in those cases. Sessions and automatic resume are not implemented for this driver.

4. Two ways to work

Draw everything, then plot

All shapes are prepared before the pen draws them.
Add the shapes to one engine and plot its plan with driver.run(). Use strategy: "drawn" to keep their drawing order.

Draw one object, plot it, then the next

Each object is prepared after the previous object has finished plotting.
Use driver.session(). Make one object, await session.run(), then make the next. The pen returns home after the last object.

An engine keeps its drawing after a plot. Running its plan again draws the same strokes again. Start a new engine for each new object to keep earlier strokes out of that object's plan. Ink remains on the paper.

Three lines, one at a time

Add this to the Setup module. It uses a log connection, so the machine stays still:

import { EbbDriver, createLogTransport } from "https://cdn.jsdelivr.net/gh/seb-prjcts-be/vanilla.penplotter@v0.5.0/src/driver/ebb.js";

const driver = new EbbDriver({
  transport: createLogTransport(), profile: "idraw-hse-a2"
});

const result = await driver.session(async function (session) {
  for (let index = 0; index < 3; index += 1) {
    const object = new PlotterEngine({
      units: "mm", page: { width: 594, height: 420 }
    });
    object.line(12, 12 + index * 5, 17, 12 + index * 5);
    await session.run(object.plan({ strategy: "drawn" }));
  }
}, { confirmed: true });
console.log(result);

The result should be { status: "complete", jobs: 3 }. Each line is calculated only after the previous one finishes. A session uses one pen and waits between objects; it does not accept new strokes during a moving job.

The session has been tested with simulated connections and three successive lines on the iDraw HSE / A2. Try the three-line example on screen or with the plotter.

Pens and layers

plot.tool({ id: "blue", name: "Blue fineliner", color: "#234f84", width: 0.3 });
plot.layer("waves", { toolId: "blue" });
plot.polyline([{ x: 20, y: 40 }, { x: 105, y: 80 }, { x: 190, y: 40 }]);

plot.tool({ id: "red", name: "Red fineliner", color: "#d94b39", width: 0.3 });
plot.layer("accents", { toolId: "red" });
plot.circle(105, 145, 24);
Two pens draw separate layers and one layer is switched off.
Each layer uses its assigned pen. The driver waits for you to change pens. Set a layer’s visible property to false to leave it out of the plan; its paths remain in the document.

Fills

A filled shape becomes parallel pen strokes.
The patch is made of parallel pen strokes. Smaller spacing puts them closer together.
const boundary = [
  { x: 30, y: 40 },
  { x: 180, y: 40 },
  { x: 165, y: 230 },
  { x: 45, y: 245 }
];

plot.hatch(boundary, {
  spacing: 2.4,
  angle: Math.PI / 4
});

plot.stipple(boundary, {
  count: 240,
  minDistance: 1.5,
  seed: 42
});
Hatch, cross-hatch and stipple shown as three different ways to fill a shape with pen strokes.
A fill becomes pen strokes: hatch in one direction, cross-hatch in two, or tiny dashes for stipple. The spacing changes the density.

Cleanup and route planning

plot.optimize({
  duplicateTolerance: 0.01,
  mergeTolerance: 0.05,
  simplifyTolerance: 0.03
});

const plan = plot.plan({
  strategy: "nearest",
  drawSpeed: 40,             // mm/s; chosen operating settings, not vendor defaults
  travelSpeed: 40,
  acceleration: 800,         // mm/s², the estimate ramps like the machine
  travelAcceleration: 300,
  liftDelay: 0.6,            // s per stroke: pen down, and up again
  toolChangeDelay: 15
});

console.log(plan.stats);

Tolerances, coordinates and distance statistics use document units. Planner speeds use mm/s; G-code feed rates use mm/min. HPGL and G-code exports convert coordinates to physical units (96 CSS pixels = 1 inch). SVG retains the document units.

After editing the document, export, statistics and preview rebuild stale plans automatically, retaining the previous optimization and planning settings. This also covers edits to returned paths, layers and tools. Document data must remain JSON-serializable.

Input order beside planned order, with shorter pen-up travel in the planned version.
Cream solid lines show the input drawing; sage solid lines show the planned drawing. Ochre dotted lines are moves with the pen raised: they leave no ink. Same drawing, less travel with the pen raised: 2,404 mm becomes 422 mm in this example.
The same outline before and after deduplication, merging and simplification.
Remove duplicate paths, join touching paths and simplify straight segments. Here, 6 paths with 126 points become 1 path with 6 points. This cleans the geometry before the route is planned.

Files

An SVG circle and lightning shape become pen paths and three export formats.
Ochre shows imported geometry becoming pen strokes. Sage connectors lead from one plan to SVG, HPGL and G-code.

Export the current drawing as a string:

const svg = plot.exportSVG();
const hpgl = plot.exportHPGL({ unitsPerMm: 40 });
const gcode = plot.exportGCode({ penUp: "M5", penDown: "M3 S1000" });

SVG is useful for inspecting or sharing the drawing. HPGL and G-code need commands and settings suited to the receiving machine. The SVG example imports supported paths and basic shapes.

Pen-lift calibration

The EBB driver keeps the controller's existing calibration unless you provide penLift. Its standard-servo settings and machine profiles are described in Architecture.

Use the modules directly

Import from src/geometry, src/optimizer, src/planner or src/renderer. Each stage accepts and returns the documented data objects. You can use these modules separately from PlotterEngine.

Core reference

CallUnits and result
new PlotterEngine({ units, page })Creates an engine and document. Use explicit units and page dimensions; the starter uses millimetres.
line(), circle(), polyline()Coordinates use document units. Circle takes a radius; polyline takes points. Geometry is added to the current layer.
paperSize(format, orientation, units)Returns A0–A6 dimensions; defaults to portrait and mm. Also exposed as PlotterEngine.paperSize(). Does not scale geometry.
optimize(options), plan(options)Cleanup uses document-unit tolerances. Plan returns one PlotPlan; nearest is greedy, drawn preserves path order and direction. Speeds are mm/s.
drawRoute(context), drawBed(context, options)DrawRoute previews planned routes. DrawBed previews machine moves with bed and sheet dimensions in millimetres.
exportSVG/HPGL/GCode/JSON()Return strings from the current plan. HPGL and G-code convert to physical units; machine commands require configuration.
EbbDriverSeparate driver and transport. Physical tests cover the iDraw HSE/A2 only. Compile and log a dry run before a confirmed physical job.
driver.session(prepare, options)Runs one object at a time, retains position between objects and returns home after the last. One pen; await each session.run(plan).