All notable changes to Etcher are documented here. The format follows Keep a Changelog and this project adheres to Semantic Versioning.
[0.2.5] — 2026-05-15
New annotation kind — dimension — for measurement-style labeling.
A horizontal-or-angled shaft with V-arrows on both ends and a
black, slidable label. Arrow color follows the active swatch; label
stays black with a white halo so it's legible on any color.
Added
- Dimension tool (
kind: "dimension"). Two endpoints (geometry: {a: [x,y], b: [x,y]}); label text + position along the shaft live inmetadata.titleandmetadata.title_offset(0–1, default 0.5 = midpoint). Drawn either by click-drag (commit on release) or by two-click rubberband (first click locks endpoint A, the line follows the cursor, second click commits endpoint B). After commit, drops straight into inline-edit mode for the label. - Slidable label — in cursor mode, click and drag the label to
slide it along the shaft. Persists as
metadata.title_offset. - Endpoint corner handles + body-drag translate, double-click to re-edit the label, eraser supports the new kind.
Etcher.Annotation's@kindswidened to include"callout","text", and"dimension"(the schema docs were also out of date for callout/text — fixed in passing).
Changed
- The bundled
Etcher.Annotationschema now accepts the same kinds the JS toolbar exposes. Consumers using the bundled storage need no migration if their CHECK constraint was already widened for callout/text — add'dimension'to the same allow-list. - Inline text editor input pinned to black so the typed text stays readable on the white-ish input background regardless of the shape's stroke color (light pastels were nearly invisible).
[0.2.4] — 2026-05-15
Cross-browser fixes + a callout stability sweep — mostly fallout from the 0.2.3 title fix not being symmetric across the rendering paths.
Fixed
- Title / callout / text-shape labels render at the same vertical
position in Firefox as in Safari. The text elements were created
with
dominant-baseline: hanging, which Safari and Firefox interpret differently for Latin text — Safari renders glyphs ABOVE the hanging baseline while Firefox renders them BELOW per spec. Combined with the wrap helper'sdy="1em"on the first tspan, callout text rendered correctly in Safari but floated below the rect in Firefox. All three render paths (_renderTitleSibling, thecase "text"branch, and the callout branch in_renderShape) now overridedominant-baselinetoalphabeticon the text element each render. Both browsers honor alphabetic identically: the alphabetic baseline lands attext.y + 1em, putting the text cleanly inside the rect with the underline / leader attaching at the rect's bottom edge. - Callouts no longer grow exponentially when text overflows. The
width-fit font cap that 0.2.3 added to
_renderTitleSiblingwas missing from the callout render path, so a long callout label triggered the same multi-line wrap → grow → wider font → more wraps feedback loop the title fix originally addressed. Same cap applied to callouts. - Click on a title or callout/text-shape handle no longer triggers
a phantom resize.
_startTitleHandleDragand_startHandleDragunconditionally firedetcher:updatedand snapped geometry to the rendered (shrunk) box onpointerup, even when the user just clicked without dragging. Both now use a 3-px screen-space dead zone (matching the body-drag and title-drag handlers) so a bare click is a no-op. - Callout corner drags no longer shrink the box on every
interaction. Drag math used
_renderedBox(shrink-fit visual) as the start reference, so each drag computed a new geometry of(visual + delta)instead of(geometry + delta). With shrink-fit on, that baked the shrink offset back into storage every drag — the callout visibly shrunk a bit each time, then converged. Drag math now uses pointer DELTA (pt - startPt) against the fullgeometry.text_box, so dragging a handle by Δpx grows or shrinks geometry by exactly Δpx; the visual continues to shrink-fit independently. Anchor drag (idx 0) still uses absolute pt — no visual/storage offset there. - The
_startHandleDragonUpsnap-to-_renderedBoxis skipped for callouts (delta math already keeps geometry consistent with the visible drag). Text shapes still snap on release — same 0.2.x behavior, no regression.
[0.2.3] — 2026-05-14
Single bug fix — stops the runaway growth of shape titles on drag / click. No API change, no behavior change for code that doesn't hit the bug.
Fixed
- Shape title text no longer balloons on every interaction. When
a title's content overflowed the default box width at the
height-derived font-size,
_fillTextWithWrappedTspanswrapped it onto multiple lines.actualH = measured.height + pad·2then exceeded the inputth, that taller height got persisted back intometadata.title_boxon release, the next render derived an even larger font, more lines wrapped, and the title grew exponentially per interaction (title_box.hgoing 22 → 54 → 273 → … in three drags)._renderTitleSiblingnow caps the font-size so the title fits the box width on a single line (floor of 10 px), boundingactualHto one line of text. The shrink-to-text rendering + handle-drag commit are unchanged; with the cap in place the system has a fixed point instead of a feedback loop.
0.2.2 — 2026-05-14
Follow-up patch to 0.2.1: restore body-grab on the editing shape,
keep the tooltip from blocking the satellite title label, and make
edit-mode survive a click on a sibling shape now that shapes are
pointer-events: none.
Fixed
- Body-grab restored on the editing shape. With 0.2.1 flipping
every shape to
pointer-events: none, the click-drag-the-body- to-move-the-shape gesture stopped firing because the shape's own pointerdown listener no longer saw any events. The currently-edit-mode shape now re-enablespointer-events: visiblePaintedvia a.etcher-shape.is-editingrule — only THAT shape catches its own pointerdown; the rest of the shapes stay invisible to events so pan/zoom still passes through them. - Tooltip no longer covers the title satellite. When the cursor was over a shape's movable title label, the hover tooltip rendered above the parent shape — directly on top of the title the user was trying to grab. The doc-level hover hit-test now detects when the cursor is inside a title's bbox and suppresses the tooltip for that hit; hover styling on the parent shape stays applied so it's still clear which annotation is targeted.
- Edit-mode and tooltip-pin survive a click on a sibling shape.
Both outside-click handlers (the edit-mode tear-down and the
tooltip-pin tear-down) used
e.target.closest(".etcher-shape")to detect "is this click on a shape?" — but since 0.2.1 shapes arepointer-events: none, the click's DOM target is OSD's canvas, not the shape. The handlers now fall back to an image-px hit-test via_shapeAt(pt)so clicking a different shape switches edit mode or the pin instead of tearing down to empty.
Internal
_setHoveredShape/2(was /1) gains anonTitleflag.- New helper
_pointOnTitleOf/2for the title-bbox hit-test.
0.2.1 — 2026-05-14
Patch release: pan / zoom now work over shapes.
Fixed
- Scroll-wheel zoom and click-drag pan on the underlying viewer
stopped working whenever the cursor was over an annotation
(rectangle, circle, polygon, freehand, callout, text). Root
cause: shapes had
pointer-events: visiblePaintedso they caught wheel + pointerdown before OSD's MouseTracker on the canvas sibling could see them — pointer events bubble UP the DOM, not sideways. Shapes are nowpointer-events: none, and hover + click are re-detected at the document level via image-px hit-testing (reuses the eraser's per-kind point-in- shape check). Hover styling, tooltips, click-to-pin, click-to- edit, and dblclick-inline-edit all continue to work; pan and zoom now pass through every annotation cleanly.
Internal
- Renamed
_eraserHit/2to a shared_shapeContainsPoint/2helper. The eraser keeps a thin alias for readability at its call sites. - New helpers:
_shapeAt/1(topmost-shape lookup),_onShapeTap/1(shared tap-handling entry),_wireGlobalShapeListeners/0+_unwireGlobalShapeListeners/0,_setHoveredShape/1. - Tap-vs-drag disambiguation with a 5px dead-zone keeps a quick click-without-drag firing the shape's selection / pin / edit flow, while any drag-with-movement passes through to OSD's pan unchanged.
0.2.0 — 2026-05-14
A backwards-compatible second release: two new shape kinds, an eraser tool, undo/redo with full history, satellite titles, edge-resize grabbers, polygon midpoint insertion, a visibility toggle, and a complete programmatic API so consumers can drive the layer without rendering its built-in toolbar.
Added
- Callout tool (
kind: "callout") — blueprint-style leader-line annotation: an anchor dot pointing at the image, a thin line to a resizable text bbox (with a horizontal underline spanning the bbox bottom). Text inside scales to fit the bbox. - Text tool (
kind: "text") — freestanding text label drawn as a click-drag bbox. Inline editor (<foreignObject>+<input>) opens on commit and on double-click for re-edit. Font scales with bbox height; bbox shrink-wraps to the text on release. - Eraser tool — press-and-drag wipes shapes by sweep. Each shape
the cursor crosses dims (
.is-erasing) for preview; release flushes them all as a single compound delete. Idle hover (eraser selected, no button held) previews the single shape under the cursor. - Optional title field per annotation — every kind can carry a
short label (
title varchar(200)). On rect/circle/polygon/freehand the title renders as a movable, resizable satellite group with a dashed leader line back to the parent's nearest perimeter point (leader auto-hides when the title is inside the parent). On callouts the title is the in-bbox content. Drag to move (persisted asmetadata.title_box), 4 corner handles to resize, double-click to inline-edit. - Edge-midpoint resize grabbers on rectangles — small rounded
rect handles on each side; drag a side to slide one edge while
the opposite edge stays anchored. Distinct visual +
ns-resize/ew-resizecursors so they don't conflate with polygon midpoints. - Polygon midpoint vertex insertion — every polygon edge now carries a "ghost" midpoint handle that lights up when the cursor is near. Grab it to insert a new vertex at that midpoint and place it via a vertex-style drag.
- Undo / Redo — toolbar buttons + ⌘Z / ⌘⇧Z / Ctrl+Y keyboard
shortcuts. 50-op session history covers geometry, style, metadata,
title text, and deletes (including bulk-delete from the eraser).
Delete recreation honors a new
restore: trueflag so the consumer can suppress its create-time UI (e.g. comment composer) on undo. - Visibility toggle — eye / eye-slash button above the pencil in Fresco's nav column. Hides/shows the entire SVG overlay with one click.
- Color picker — bottom-toolbar swatches; persisted as
style.coloron each annotation; vertex + title handles inherit the shape's color viacurrentColor(no more always-orange dots). Override the palette viawindow.Etcher.colorSwatches; initial color viawindow.Etcher.defaultColor. - Tooltip slot extension API —
window.Etcher.tooltipSlots = { header, body, footer }lets consumers replace the tooltip content per-slot while keeping the wrapper (trash button, pin/unpin, hover bridge) under Etcher's control. Default slots read genericmetadata.{title,body,subtitle}keys. - Complete programmatic control surface on
window.Etcher.layerFor(frescoId)so every built-in button is callable from outside. Methods grouped by mode, visibility, tool, color, history, and shape selection/edit. Consumers can render their own toolbar and drive the layer headlessly. - CustomEvents for state changes:
etcher:mode-changed,etcher:tool-changed,etcher:color-changed,etcher:visibility-changed,etcher:history-changed,etcher:tooltip-show / -hide / -pin / -unpin. - Restored comment threads on undo-of-delete — the etcher:created
payload for a restore carries
restore_from_uuidso consumers can re-link soft-deleted child rows (e.g. comments) to the new uuid the server assigns to the recreated annotation. appendNavButtonmutable handle (Fresco 0.1.2+) — Etcher's nav buttons can now update their icon / title in place (used by the visibility toggle to flip eye ↔ eye-slash).
Changed
- Default
:toolslist onEtcher.Layer.layer/1is now[:rectangle, :circle, :polygon, :freehand, :callout, :text, :eraser](all seven). Pass an explicit list to subset. - Text + title + callout bboxes shrink-wrap to the rendered text on every render; the stored geometry is rewritten to the shrunk dimensions on release so storage always matches what's visible.
- Vertex handles now inherit the shape's color (
currentColor) instead of hard-coded orange. CSS hover / drag fills usefill-opacityso they tint correctly with whichever color the shape carries. - Single-shape deletes now flow through the same compound
bulk_deleteundo op the eraser uses, so the tooltip trash button also gets redo support.
Fixed
- Tooltip stops hijacking hover state on a different shape while another shape's tooltip is pinned.
- Pinned shape keeps its
.is-selectedoutline when the cursor leaves it (was getting stuck visually deselected). - Tooltip
.is-hoveredno longer sticks after the cursor leaves a pinned shape.
0.1.0 — 2026-05-06
Initial release.
Added
Etcher.LayerPhoenix LiveView function component — attaches an annotation overlay to a named Fresco viewer and adds a pencil button to its nav column.Etcher.Storagebehaviour — pluggable storage adapter contract with four callbacks (create/1,list_for/2,update/2,delete/1).Etcher.Storage.Default— bundled implementation backed by theetcher_annotationstable. Reads the consumer's Repo fromconfig :etcher, repo: ….Etcher.AnnotationEcto schema for the bundled table (UUIDv7 primary key,target_type/target_uuid, four geometry kinds: rectangle, circle, polygon, freehand).mix etcher.gen.migration— generates theetcher_annotationstable migration into the consumer'spriv/repo/migrations/.- JS engine at
priv/static/etcher.js— registers theEtcherLayerLiveView hook, draws shapes as SVG overlays anchored to image coordinates, emitsetcher:created/:updated/:deleted/:selectedevents. - Bottom drawing toolbar with rectangle / circle / polygon / freehand
tools; pencil-button toggle integrated with Fresco's nav column via
handle.appendNavButton/3(Fresco 0.2+).