Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5ef3a60
feat(kernel): a label class accepts a set of geometries
JArmandoAnaya Aug 15, 2026
ef86a52
feat(annotator): a tool is resolved against the class's set, not deri…
JArmandoAnaya Aug 15, 2026
4a951ad
test(annotator): the tool resolution rule a geometry set made necessary
JArmandoAnaya Aug 15, 2026
6a2b189
test(annotator): a class that is both taggable and drawable arms rath…
JArmandoAnaya Aug 15, 2026
cc4824a
feat(ui): a class's geometries are a checkbox group, and a name that …
JArmandoAnaya Aug 15, 2026
bfa6942
feat(app): the demo, styleguide and browser suites follow the geometr…
JArmandoAnaya Aug 15, 2026
1bb6b48
docs: geometry sets, the rescue flow, and export unchanged
JArmandoAnaya Aug 15, 2026
b9ab6b6
test(ui): the retarget guard, which no fixture with a two-shape class…
JArmandoAnaya Aug 15, 2026
dfdf7d0
docs: the last two places that called a class's geometry singular
JArmandoAnaya Aug 15, 2026
c079415
feat(ui): one vocabulary for geometries, and it is not the wire's
JArmandoAnaya Aug 15, 2026
fe1b2e0
feat(ui): the armed class row is the shape picker
JArmandoAnaya Aug 15, 2026
2ef1734
fix(ui): the add-a-class dialog was undersized, and could hide its ow…
JArmandoAnaya Aug 15, 2026
7dfe292
docs: dialog widths, the armed row's shape picker, and the panel's br…
JArmandoAnaya Aug 15, 2026
6ad0feb
test(ui): arm a class by its name, not by the row's centre
JArmandoAnaya Aug 15, 2026
60d73db
test(kernel): pin the narrowing gate's over-refusal, and say so in th…
JArmandoAnaya Aug 15, 2026
9d271dc
fix(ui): a geometry set has no order, and the draft comparison has to…
JArmandoAnaya Aug 15, 2026
53e0184
test(cycle): the real-server walk posted the retired geometry key
JArmandoAnaya Aug 15, 2026
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
52 changes: 49 additions & 3 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,15 @@ base**, line-height 1.6. One scale — reuse it, don't invent sizes:
not qualify, however often it is visited.
- **Page widths**: lists/dashboards/detail `max-w-7xl`; forms/settings `max-w-3xl`;
centered, `px-4 md:px-6 py-6`.
- **Dialog widths**: the primitive's default `max-w-lg` for a confirmation or a form of
stacked single fields; **`max-w-2xl`** for a form whose fields sit side by side, because
`ClassFields`' grid splits on `md:` — a *viewport* breakpoint — so the box must be wide
enough for a split it cannot prevent; `max-w-3xl` for a dialog whose content is a grid
rather than a form (`FrameGallery`). Three sizes, and a fourth needs a reason written here
rather than picked by eye. **Any dialog whose content grows with the data carries
`max-h-[85vh] overflow-y-auto`** — `DialogContent` is centred with `-translate-y-1/2`, so
one taller than the viewport overflows off both edges and takes its own footer with it,
which is not a state a person can recover from.
- **Page header**: title + subtitle left, actions right, `border-b` below, `mb-8`.
- **Grids**: cards at `gap-6`, 2/3 columns by breakpoint; 16px is the default layout
unit, 24px separates page sections. Detail two-column: `1fr / 320px`, stacking below
Expand Down Expand Up @@ -916,7 +925,7 @@ The page the reference design shows (#56), with measurements verified in v1's so
class selection lives. It was the side panel's Labels tab, then a `Combobox` in the
centre of the top bar, and it is a **list** now — because what is being chosen between
is the ontology, and a picker keeps all of it one click away, so the answer to *what can
I draw here* was never on screen. Rows carry swatch · name · geometry · hotkey badge, in
I draw here* was never on screen. Rows carry swatch · name · geometries · hotkey badge, in
the **schema's authored order and only that**: a persistent list that reordered itself
by recency would move rows under the cursor, and the digits are schema positions, so a
recency-ordered list would print `3` against the row sitting first. `c` focuses its
Expand All @@ -935,6 +944,25 @@ The page the reference design shows (#56), with measurements verified in v1's so
a promise the page can keep. It stops at the job's edge, the same scope the clipboard
has (#123) and for the same reason — a paste and a drawing class both belong to one
pinned schema.

**On the armed row, the geometry words are the shape picker** (#584). A class accepts a
*set* of geometries, so arming one no longer picks the shape — and until this the only
place that answer lived was the tool strip at the far left of the canvas while the class
was chosen on the right: one decision split across the width of the picture, in a loop
repeated hundreds of times a job. The active shape is lit; pressing another switches the
tool and **never the class**, which is the same retarget rule the strip holds. Only the
armed row carries it, and the accessible answer and the density answer agree: a row
`<button>` cannot contain buttons, so a row offering a choice becomes a group — and an
unarmed row has no live choice, so fifty classes would otherwise mean fifty controls for
one decision. A class accepting a single shape shows nothing.

Geometry words are **display labels, never wire values** — `box`, not `bbox`; `tag`, not
`classification_tag`. One map, `GEOMETRY_LABELS`, shared with the tool strip, which
capitalises at its own control; lowercase, because the same word is read as a chip in a
row and inside a sentence. A set joins with `·`, which is what a set reads as at this
density. **The ceiling, stated**: at three or more shapes the class name truncates and the
full name is on hover — the row is 36px in a 288px panel and those characters come out of
the name.
- **Pinned version badge** (#229, made an answer by #368): `v{n}` in the left zone names
the version *this batch is judged against* — not the project's active one, since #229
made the pin movable. Pressing it opens a small panel that says whether that is still
Expand All @@ -952,7 +980,13 @@ The page the reference design shows (#56), with measurements verified in v1's so
how many it will publish (`Add 3 classes`). Opened from the class list's create row it
starts on the name that was typed; opened from the tool strip's `+` or the region's own
`+` it starts empty,
because that press means "I want a class", not a particular one. When it lands, the
because that press means "I want a class", not a particular one. **A name the published
version already declares is an offer, not an error** (#584): the dialog says what that
class accepts today and what publishing would add, and the primary reads `Add polygon to
"sign"`. It carries the existing class's colour and attributes, so a form opened to make a
new class cannot quietly overwrite what the old one declared. A name typed twice in one
sitting stays a refusal, because both are being written now and merging them would be
guessing which was meant. When it lands, the
**last** class written becomes the drawing class and a toast says so — a session
publishes one version and arms one class, neither of which anybody watched happen.
Cancelling with classes banked **asks**, and it asks on Escape and the overlay too:
Expand All @@ -974,7 +1008,14 @@ The page the reference design shows (#56), with measurements verified in v1's so
surface, `border`, 12px radius, 8px padding; 36px icon buttons; **active tool = primary
variant** (the near-black), inactive = ghost; a `h-px w-6` divider; help at the bottom.
Tooltips open right with the shortcut ("Select (V)", "Box (B)", "Polygon (P)").
Icons: MousePointer2 / Square / Spline; only tools the schema's geometries allow.
Icons: MousePointer2 / Square / Spline; only tools the schema's geometries allow — and,
**with a class selected, only that class's own** (#584). A class accepts a set of
geometries, so what can be drawn depends on which class is held: offering a polygon
button under a boxes-only class would answer *what can I draw here* with a lie. With no
class selected it is the schema's union, which is still the right answer to *what does
this project label*. Switching to a class that forbids the active tool never strands it —
`toolFor` resolves to the class's first allowed shape — and the route to a different
class's geometry is the class list, which is where choosing a class belongs.
**Last of them, below the `+`, the hand** (#576, `Hand`, `H`) — the one button here that
the schema does not gate, because it answers a question about the *device* rather than
about the project: a pan had exactly one spelling, a middle- or secondary-button drag,
Expand All @@ -997,6 +1038,11 @@ The page the reference design shows (#56), with measurements verified in v1's so
in constantly — every freshly opened frame — and a control that vanished and reappeared
as they worked would be worse than one that explains itself.
- **Side panel** (#126, reshaped by #368, split in two by #420): 288px (`w-72`) column,
**320px (`2xl:w-80`) from 1536px** — headroom for a class naming three shapes, not a fix,
and withheld below that on purpose: `ANNOTATOR_MIN_VIEWPORT_PX` is 768, where a collapsed
rail already leaves a 384px stage, and a width chosen on a large monitor must not be
charged to the smallest screen the editor opens on at all. `EditorNotice`'s clearance
arithmetic is stated at 1280px and is unaffected. Otherwise:
`muted` surface, `border`, 12px radius. **Two stacked regions, no tabs and no
splitter.** It was Objects | Labels tabs until #368, which sent class selection to the
top bar; #420 brings it back and deliberately does not bring the tabs with it. A tab is
Expand Down
14 changes: 9 additions & 5 deletions docs/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ before anything is stored, and the whole call rolls back on the first refusal.
| Refusal | When |
| --- | --- |
| `LabelClassNotInSchema` | The class is not in the pinned version. Matched by **exact** name. |
| `DisallowedGeometry` | The geometry is not the one that class declares. |
| `DisallowedGeometry` | The geometry is not one that class accepts. |
| `MissingRequiredAttribute` | A `required` attribute has no value. A `default` is *not* filled in. |
| `UnknownAttribute` | The annotation carries an attribute the class does not declare. |
| `InvalidAttributeValue` | Wrong type for the kind, or outside a `select`'s options. |
Expand All @@ -50,10 +50,14 @@ them. Catching the base is safe here in a way catching `DestructiveSchemaChange`
flag overrides any of these, so there is nothing to retry into a loop. The remedy is to fix
the annotation, or to write a schema version that describes it.

The geometry rule is **per class**, not per version: a `LabelClass` declares one `geometry`, so
this is an equality test. `SchemaService.allowed_geometries` is the union across a version's
classes - the right answer to "what may this project draw?" and the wrong tool here, where it
would let a polygon through under a bbox class.
The geometry rule is **per class**, not per version: a `LabelClass` declares a set of
`geometries` and this is membership in *that* set. `SchemaService.allowed_geometries` is the
union across a version's classes - the right answer to "what may this project draw?" and the
wrong tool here, where it would let a polygon through under a boxes-only class.

A class accepting more than one shape is ordinary, not a corner case: the same sign is worth
boxing at a distance and worth outlining close up, and it is one class either way. Which shape
a given label carries is the annotation's own business. See [schemas.md](schemas.md).

## The version is the batch's, not the project's

Expand Down
8 changes: 5 additions & 3 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,11 @@ for Pillow - it is not carrying a determinism argument about folds.)

## Why three classes for "two classes"

A `LabelClass` is bound to exactly one `GeometryType` - `geometry` is singular. Showing a
bounding box, a polygon and a whole-frame classification therefore takes three classes
(`stop-sign`, `lane-marking`, `weather`), not one class listing three shapes. Exactly one
A `LabelClass` accepts a *set* of geometries, so one class could carry all three shapes - and
these are three classes anyway, because they mean three different things rather than three
shapes of one thing. `stop-sign`, `lane-marking` and `weather` is what an ontology looks like;
a single class accepting a box and an outline is what one *object* looks like from two
distances. Exactly one
attribute is *required* (`occlusion` on `stop-sign`), which is what makes
`MissingRequiredAttribute` a live rule in the example rather than a paragraph.

Expand Down
10 changes: 6 additions & 4 deletions docs/mcp-walkthrough.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,15 @@ tool that reads the dataset on its own: `get_project` already carries `dataset_i

```
create_schema_version project="road-signs" classes=[
{"name": "sign", "geometry": "bbox"},
{"name": "empty-road", "geometry": "classification_tag"}]
{"name": "sign", "geometries": ["bbox"]},
{"name": "empty-road", "geometries": ["classification_tag"]}]
-> {"version": 1, ...}
```

A `LabelClass` is bound to exactly one geometry, so "a box round a sign" and "a tag on a picture
with nothing in it" are two classes, not one class with two shapes. The whole list is sent every
A `LabelClass` declares a **set** of geometries, so a class labelled as a box on some frames and
as an outline on others is one class — pass every shape it accepts. "A box round a sign" and "a
tag on a picture with nothing in it" are still two classes here, because they mean two different
things rather than two shapes of one thing. The whole list is sent every
time: a version is the complete contract, never a patch against the last one, which is what lets
[schemas.md](schemas.md) call removal *narrowing* and gate it.

Expand Down
14 changes: 12 additions & 2 deletions docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,16 @@ against one format's declaration and returns an `ExportCompatibility`:
}
```

**One row per `(label_class, geometry)`, not per class.** A class accepts a *set* of
geometries ([schemas.md](schemas.md)), and a format's answer can differ across it: a class
labelled both as boxes and as outlines is, to YOLO, one half written whole and one half
written reduced. It contributes two rows. A single row could carry only one of those verdicts
and would describe half its own output wrongly whichever it picked - the same defect
`compatible: bool` had before three statuses replaced it, one level down.

A class the schema declares and nobody used still gets a row per geometry, at zero. Zero
excludes nothing, so it never makes a report incompatible however unsupported its shape is.

### Dropped is not degraded, and one word for both was a lie

`status` has three values, and the reason is #158. Until then a class was `supported: true` or
Expand Down Expand Up @@ -361,8 +371,8 @@ many. A classification tag has no location at all and is dropped rather than giv

**v1 had two COCO exporters and neither described a dataset.** One skipped every annotation that
was not a box, the other every one that was not a polygon, so a project holding both - the
ordinary case, and the reason a schema declares a geometry per class - had to pick an export and
silently lose the other half. There is one exporter here: COCO has always carried both, and
ordinary case, and now expressible in a single class since a class declares a *set* of
geometries - had to pick an export and silently lose the other half. There is one exporter here: COCO has always carried both, and
`bbox` is a required field on every annotation whether or not it also has a `segmentation`.

**`area` is the polygon's own area, by the shoelace formula, not its bounding box's.** v1 wrote
Expand Down
Loading
Loading