| title | Plotting |
|---|---|
| description | From the visible ink to a plan, a file and a machine. |
Rendering makes the visible ink. Planning puts it in order: fragments merge into chains, the chains get a route, and gaps smaller than the nib close. Every export and the machine read the same plan, so the SVG, the G-code and the plot agree.
/_sig/Toolkit.plan.mdx /_sig/Toolkit.draw.mdx
plan sets the path settings for this run. Call it once in the sketch body, before the return. It returns nothing. optimize is the tour budget. bridge is the gap the pen closes without a lift, or false. The gap is a length: mm(1) is one millimetre, and a bare number is millimetres too. draw chooses the part of the ordered drawing to plot, in the same way: once, before the return. progress is a fraction of the chains. chains is a range of them. minutes is an interval of the estimated time, and budget keeps the longest prefix that fits. A range never re-plans or reorders. Ink that a later shape hid stays hidden.
import { sketch, circle, ui } from 'occlude';
export default sketch({ aspect: [1, 1], seed: 4 }, (t) => {
t.draw({ progress: [0, ui(0.6, { min: 0, max: 1 })] });
return t.grid({ cols: 8, rows: 8 }).faces.map((f) => circle(f.centroid, f.bounds.w * 0.4));
});/_sig/shader.mdx
A shader is a program that runs along every stroke in the plan. It reads the arc length s, the point p there, and the stroke it walks. s and p are in drawable units, the same units as t.bounds() and every shape. So a field of the sketch reads p directly. It answers with what the pen does: more passes, another pen, a dash, or no ink. Give it to t.plan({ shader }), and the stroke splits wherever the answer changes. A split lands on the nib grid.
ctx.pen is the name of the stroke's pen, the same word that a pen answer takes. ctx.slot is its number in the pen table. ctx.length is the length of the stroke in drawable units. ctx.mm holds s, p and length on the paper, in millimetres. A pen answer can name a pen that the drawing uses or a pen that the sketch declares in pens. Any other name refuses.
import { sketch, shader, circle, mm, distance } from 'occlude';
export default sketch({ aspect: [1, 1], seed: 7 }, (t) => {
const b = t.bounds();
t.plan({
shader: shader((s, p) => ({
keep: distance(p, [b.w * 0.62, b.h * 0.62]) > b.w * 0.18,
dash: p[1] < b.h * 0.5 ? [mm(3), mm(0.8)] : undefined,
})),
});
return [
t.times(24, (i) => circle(b.w * 0.5, b.h * 0.5, 3 + i * 1.7)),
circle(b.w * 0.74, b.h * 0.28, b.w * 0.13, { opaque: true }),
];
});:::note
The example hides ink twice, and the two holes do not look alike. The opaque circle occludes, and the strokes stop at its edge. The shader refuses to draw, and the strokes stop wherever it says. decimate and wobble are the same idea with the answer fixed in advance. A shader runs last of all, and never puts ink outside what the engine cleared.
:::
:::note The program runs once for each nib step of every stroke, so keep it cheap. A dense hatch at a 0.14mm nib is about 240 000 calls and 34 milliseconds of planning, against 4 without a shader. A drawing with no shader pays none of it. :::
:::note The studio previews the PLAN, so a shader shows on screen. The plan keeps lines, arcs and cubics exactly as the render made them, so the preview is no coarser than the paper. It also shows the merges and the bridges, because the machine draws those too. :::
:::warning
A shader renumbers the plan. passes doubles the chains, a dash multiplies them, and keep: false removes them. A t.draw({ chains }) range written against the unshaded plan covers a different part of the shaded one. ctx.index is the number of the chain before shading.
:::
:::warning
The route is already chosen when a shader runs. A pen answer costs a pen change the plan did not order around. Weight from passes is real ink on paper. The preview cannot show it, because the second pass covers the first exactly. Sixteen passes is the most a program can ask for.
:::
A modifier runs on one side of the occlusion solve or the other. The side is the modifier's own, and not an option. smooth, roughen and deform run BEFORE the solve. They change the shape's own outline, so they change what the shape hides. wobble, decimate and dash run AFTER it, over the ink that survives. They tremble, thin and cut the drawing, and the scene stays as the engine solved it. Inside each side the stack runs in the order the sketch wrote it. The plan comes after all of them, and a shader after the plan. The shapes page holds each modifier's own options.
Below, the same opaque disc over the same rules. On the left, roughen runs before the solve, so the rough edge is the edge that hides. On the right, wobble runs after it, so the cut stays a clean circle and the surviving ink trembles.
import { sketch, circle, line, roughen, wobble, mm } from 'occlude';
export default sketch({ aspect: [2, 1] }, (t) => [
t.times(30, (k, u) => line(4, 6 + u * 88, 96, 6 + u * 88)),
circle(50, 50, 30, { opaque: true, modifiers: [roughen(mm(2.5))] }),
t.times(30, (k, u) => line(104, 6 + u * 88, 196, 6 + u * 88, { modifiers: [wobble(mm(1.2))] })),
circle(150, 50, 30, { opaque: true, modifiers: [wobble(mm(1.2))] }),
]);/_sig/exportSvg.mdx /_sig/exportPng.mdx /_sig/exportGcode.mdx
Headless export, after initOcclude() once. exportSvg writes one path for each chain in plot order, with arcs and cubics kept exact. exportPng rasterises at scale pixels for each millimetre. exportGcode writes one job for each pen. profile describes the machine: bed, resolution, travelFeed, zMode, arcSupport and yAxis. All three honour t.draw. The studio's Export panel does the same for the current selection. t.draw({ minutes }) and t.draw({ budget }) read the time of the plot, and only a machine knows its time. So an export of such a sketch needs timing. penOf gives the feed and penDelay of each pen, and opts gives the motion limits of your machine profile. The same numbers go into estimatePlanMs. Without timing, the export refuses the sketch and names the option.
import { sketch, circle } from 'occlude';
import { initOcclude, render, exportSvg, exportGcode, exportPng } from 'occlude/host';
const def = sketch({ aspect: [1, 1] }, (t) => circle(50, 50, 40));
const profile = { bed: [300, 218], resolution: 0.2, travelFeed: 8000, zMode: true, arcSupport: false, yAxis: 'down' };
await initOcclude();
const out = render(def, { paper: 'A4' });
const svg = exportSvg(def, { paper: 'A4', background: '#f6f2ea' });
const jobs = exportGcode(def, { paper: 'A4', profile });
const png = exportPng(def, { paper: 'A4', scale: 11.81 });
// A sketch that calls t.draw({ minutes: [0, 3] }) exports with the machine's timing.
const timing = {
penOf: () => ({ feed: 3000, penDelay: 150 }),
opts: { travelFeed: 8000, acceleration: 800, travelAcceleration: 1500, junctionDeviation: 0.05, minimumCruiseRatio: 0.5 },
};
const firstMinutes = exportPng(def, { paper: 'A4', scale: 11.81, timing });This is a pointer, not an entry. The studio's Machine page holds the machine profiles: an EBB board over Web Serial, or a G-code controller such as GRBL. A profile is the bed, the axes, the pen heights and feeds, and the motion limits. Plot, pause and resume, homing, the pen ladder and the motion tests are on that page. The device notes cover each controller's quirks.
/_sig/estimatePlanMs.mdx
The one time model. It reads the plan's chains, the pens' feeds and the machine's acceleration. It returns the estimate that the Drawing panel, t.draw({ minutes }) and the machine share.