Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Holding tabs for cutting.** Short uncut bridges that keep each piece attached to the
sheet until it is snapped out by hand — the opposite of the overcut, and wanted just as
often. Count, width and a minimum contour length are set in the knife profile; bridges are
distributed by arc length, nudged off sharp corners (where a bridge tears instead of
snapping), kept clear of the stretch the overcut re-traces, and never produce a fragment
too small to cut. Off by default. The canvas now draws the prepared cutting path — overcut
tail and tab gaps — over the faded artwork, so where a piece stays attached can be checked
before a sheet of vinyl is committed.
- **Travel routes around the magnets.** Pen-up moves that would cross a keep-out zone now
detour clear of it — every leg, including the first one out and the return home — and the
detours are drawn on the canvas so the route can be checked before starting. The plot-time
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,11 @@ unattended **Raspberry Pi** setup needs.
the **knife profile** (its own Z, dwell and cut feed), places imports **1:1** at the
position the file gives them, offers only outline conversions (a fill would shred a
sticker), previews in cut red, and prepares the toolpath for a drag knife — an
**overcut** past the closing point so the piece releases, and optional **blade-offset
compensation** at corners (off until you've measured your holder).
**overcut** past the closing point so the piece releases, optional **blade-offset
compensation** at corners (off until you've measured your holder), and **holding tabs** —
short uncut bridges that keep each piece attached until you snap it out, placed away from
corners and clear of the overcut. The canvas shows the path the blade will follow, gaps
and all.
- **Paper type.** Preview the drawing on the stock you are about to use — plain, cream,
dotted, grid, lined, kraft, or black card (which previews the artwork light, for a white
or metallic pen). Appearance only: it never touches the generated paths.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-12
38 changes: 38 additions & 0 deletions openspec/changes/archive/2026-09-12-holding-tabs/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## Context

`prepareForCut` was two transforms in a fixed order (compensate, then overcut) on one polyline,
returning one polyline. Tabs break that shape: one contour becomes several strokes.

## Decisions

- **Tabs are a split, not a new concept.** The writer already puts the tool up between strokes, so
a tabbed contour is just several strokes and the bridges come out for free. Nothing downstream —
ordering, framing, the estimate — needs to know tabs exist.
- **Order: compensate → overcut → tabs, and `closed` decided once up front.** Tabs have to come
last, because after them there is no closed contour left for the overcut to recognise and it
would silently do nothing. For the same reason "was this closed?" is answered before any
transform runs and carried through: it is a property of the artwork, not of the intermediate path.
- **Tabs stay out of the stretch the overcut re-traces.** Otherwise the overcut cuts through the
first bridge — the interaction the issue flagged, and the one that would make both features look
broken at once.
- **Bridges are nudged off corners.** Even distribution by arc length is the v1 rule, but a bridge
landing on a corner tears rather than snapping, so a tab within a millimetre of a sharp turn
shuffles along the contour (bounded, so it cannot walk into its neighbour).
- **The count is reduced, not honoured blindly.** Each bridge needs cut on both sides of it; asking
for fifty on a short contour would produce a dotted line, so the number is capped by what the
contour can afford.
- **No degenerate fragments.** `orderPolylines` drops anything shorter than two points, so a
zero-length slice would vanish silently and leave an unexplained gap in the cut. Slices that
collapse are discarded explicitly instead.
- **Stroke reversal is already off when cutting** (#58), which resolves the issue's other worry:
tab positions are relative to the contour's start, and a reversed contour would move them.
- **The cut preview draws the prepared path over faded artwork.** Grey is the drawing, red is what
the knife does. Fading rather than hiding keeps the object selectable and draggable.

## Risks / Trade-offs

- Tab width is a length along the path, not a chord: on a tight curve the bridge is very slightly
longer than the straight-line gap. At 0.5 mm the difference is far below the kerf.
- A tab is placed by arc length from the contour's start, which is wherever the importer began the
path. Two visually identical contours can therefore have bridges in different places. Consistent
placement would need a canonical start point, which is a bigger idea than this needs.
43 changes: 43 additions & 0 deletions openspec/changes/archive/2026-09-12-holding-tabs/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
## Why

A closed contour is severed the moment its loop closes — and the overcut added in #58 exists to
make sure of it. That is right for the last piece and wrong for every piece before it:

- a freed piece can shift or lift under the drag knife while later contours are still being cut;
- pieces come loose during the job rather than at weeding time, so the sheet cannot be handled as
one until it is finished;
- there is no way to cut a part that should stay put — a hinged flap, or something to be snapped
out by hand later.

Overcut and tabs are opposite controls, and both are wanted, per job.

## What Changes

- **`src/plot/tabs.ts`** — a pure transform: a closed cut path in, the strokes that should actually
be cut out, with the bridge spans omitted. No new idea is needed downstream, because the G-code
writer already lifts the tool between strokes.
- **Tab settings in the knife profile**: count, width (default 0.5 mm), and a minimum contour
length below which a contour gets none. **Off by default** — a cut with no tabs is what the
machine did before, and a piece that unexpectedly stays attached is as surprising as one that
does not.
- **Bridges avoid sharp corners**, where a bridge tears instead of snapping, by nudging along the
contour until they sit on a straight span.
- **Bridges keep clear of the overcut**, which re-traces the start of the path — a tab there would
be cut straight through, making both features look broken at once.
- **The canvas shows the prepared cut path**: the overcut tail and the tab gaps, drawn over faded
artwork, because where a piece stays attached is exactly what has to be checked before a sheet of
vinyl is committed.

## Capabilities

### Modified Capabilities
- `cutting-mode`: cut contours can be interrupted by holding tabs, and the preview shows them.

## Impact

- **Code:** new `src/plot/tabs.ts` (+ tests); `src/plot/cut.ts`, `src/plot/knife.ts` (via
`CutOptions`), `src/ui/SettingsPage.tsx`, `src/ui/PlotCanvas.tsx`, `src/ui/App.tsx`.
- **Behaviour:** with tabs off, `prepareForCut` produces exactly what it did before, pinned by a
test. Drawing mode is untouched.
- **Hardware:** unverified — a real sticker has to confirm the piece stays put during the job and
snaps out cleanly afterwards.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
## ADDED Requirements

### Requirement: Holding tabs keep a cut piece attached

Cutting SHALL be able to leave short uncut bridges in a closed contour, so the piece stays attached
to the sheet until it is snapped out by hand. The number of bridges, their width, and the contour
length below which none are placed SHALL be configurable, and bridges SHALL be off by default.

#### Scenario: Cutting with tabs

- **WHEN** a closed contour is cut with tabs enabled
- **THEN** the contour is cut as several strokes separated by uncut bridges of the configured width

#### Scenario: Off by default

- **WHEN** the operator has not enabled tabs
- **THEN** contours are cut exactly as they were before the feature existed

#### Scenario: Small detail is not bridged

- **WHEN** a contour is shorter than the configured minimum
- **THEN** it is cut without tabs, because a bridge as long as the shape destroys it

#### Scenario: More tabs than the contour can hold

- **WHEN** more bridges are asked for than the contour can carry with cut between them
- **THEN** the number is reduced, rather than the contour being cut as a dotted line

### Requirement: Bridges are placed where they will hold

Bridges SHALL be distributed around the contour, SHALL avoid sharp corners, and SHALL NOT be placed
where an overcut would cut through them.

#### Scenario: Away from corners

- **WHEN** a contour has sharp corners
- **THEN** no bridge is placed on one, because a bridge on a corner tears instead of snapping

#### Scenario: Clear of the overcut

- **WHEN** both an overcut and tabs are enabled
- **THEN** no bridge lies in the stretch the overcut re-traces, so the overcut cannot cut through it

#### Scenario: Every piece is drawable

- **WHEN** any combination of tab count and width is used
- **THEN** every resulting stroke is one the machine can cut, with no degenerate fragments

### Requirement: The preview shows what the blade will follow

In cutting mode the canvas SHALL show the prepared cutting path — including the overcut and the
gaps left by holding tabs — distinctly from the artwork it came from.

#### Scenario: Seeing where the piece stays attached

- **WHEN** tabs are enabled
- **THEN** the gaps are visible on the canvas before the job is started
26 changes: 26 additions & 0 deletions openspec/changes/archive/2026-09-12-holding-tabs/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## 1. Transform

- [x] 1.1 `src/plot/tabs.ts`: `applyTabs`, arc-length distribution, corner avoidance, count capping
- [x] 1.2 Unit tests: off by default, span count, exact width removed, even spacing, open path
untouched, short contour untouched, absurd count capped, no degenerate fragments across many
count/width combinations, bridges off corners, bridges clear of the overcut

## 2. Integration

- [x] 2.1 `CutOptions` extends `TabOptions`; `prepareForCut` applies compensate → overcut → tabs
with `closed` decided up front
- [x] 2.2 Tests: order preserved, unchanged output with tabs off, open geometry still one stroke

## 3. UI

- [x] 3.1 Tab count, width and minimum contour in the knife profile on the settings page
- [x] 3.2 Canvas draws the prepared cut path (overcut + gaps) over faded artwork

## 4. Docs, gate, verification

- [x] 4.1 README, CHANGELOG
- [x] 4.2 `mise run ci` green
- [x] 4.3 Verified in the browser: 4 tabs of 2 mm on a rectangle produce 5 cut spans in the emitted
G-code, with gaps measuring exactly 2.00 mm, evenly spread, each ≥130 mm from a corner
- [ ] 4.4 ⚙ HARDWARE: cut a real sticker with tabs on — confirm the piece stays attached during the
job and snaps out cleanly by hand
56 changes: 56 additions & 0 deletions openspec/specs/cutting-mode/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,59 @@ The canvas SHALL draw a cutting job's geometry as cut lines rather than in a pen

- **WHEN** the job is in cutting mode
- **THEN** the artwork is drawn in the cut colour, and pen selection is not offered

### Requirement: Holding tabs keep a cut piece attached

Cutting SHALL be able to leave short uncut bridges in a closed contour, so the piece stays attached
to the sheet until it is snapped out by hand. The number of bridges, their width, and the contour
length below which none are placed SHALL be configurable, and bridges SHALL be off by default.

#### Scenario: Cutting with tabs

- **WHEN** a closed contour is cut with tabs enabled
- **THEN** the contour is cut as several strokes separated by uncut bridges of the configured width

#### Scenario: Off by default

- **WHEN** the operator has not enabled tabs
- **THEN** contours are cut exactly as they were before the feature existed

#### Scenario: Small detail is not bridged

- **WHEN** a contour is shorter than the configured minimum
- **THEN** it is cut without tabs, because a bridge as long as the shape destroys it

#### Scenario: More tabs than the contour can hold

- **WHEN** more bridges are asked for than the contour can carry with cut between them
- **THEN** the number is reduced, rather than the contour being cut as a dotted line

### Requirement: Bridges are placed where they will hold

Bridges SHALL be distributed around the contour, SHALL avoid sharp corners, and SHALL NOT be placed
where an overcut would cut through them.

#### Scenario: Away from corners

- **WHEN** a contour has sharp corners
- **THEN** no bridge is placed on one, because a bridge on a corner tears instead of snapping

#### Scenario: Clear of the overcut

- **WHEN** both an overcut and tabs are enabled
- **THEN** no bridge lies in the stretch the overcut re-traces, so the overcut cannot cut through it

#### Scenario: Every piece is drawable

- **WHEN** any combination of tab count and width is used
- **THEN** every resulting stroke is one the machine can cut, with no degenerate fragments

### Requirement: The preview shows what the blade will follow

In cutting mode the canvas SHALL show the prepared cutting path — including the overcut and the
gaps left by holding tabs — distinctly from the artwork it came from.

#### Scenario: Seeing where the piece stays attached

- **WHEN** tabs are enabled
- **THEN** the gaps are visible on the canvas before the job is started
16 changes: 13 additions & 3 deletions src/plot/__tests__/cut.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,19 +168,29 @@ describe('prepareForCut', () => {
it('compensates first, then overcuts the compensated path', () => {
// Order matters: the overcut has to re-trace the path the blade actually
// follows, which is the compensated one.
const [out] = prepareForCut([square], { overcutMm: 2, bladeOffsetMm: 0.5 });
const [out] = prepareForCut([square], {
...DEFAULT_CUT_OPTIONS,
overcutMm: 2,
bladeOffsetMm: 0.5,
});
const compensated = applyBladeOffset(square, 0.5);
expect(out.slice(0, compensated.length)).toEqual(compensated);
expect(polylineLength(out)).toBeCloseTo(polylineLength(compensated) + 2, 6);
});

it('passes open geometry through with corner compensation only', () => {
const [out] = prepareForCut([openLine], { overcutMm: 2, bladeOffsetMm: 0 });
const [out] = prepareForCut([openLine], {
...DEFAULT_CUT_OPTIONS,
overcutMm: 2,
bladeOffsetMm: 0,
});
expect(out).toEqual(openLine);
});

it('leaves everything alone with both features off', () => {
const polys = [square, openLine];
expect(prepareForCut(polys, { overcutMm: 0, bladeOffsetMm: 0 })).toEqual(polys);
expect(
prepareForCut(polys, { ...DEFAULT_CUT_OPTIONS, overcutMm: 0, bladeOffsetMm: 0 }),
).toEqual(polys);
});
});
Loading